From fc295bcc057818aab78b897e9076b8143e43d66c Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 22 Jun 2021 09:23:57 +0200 Subject: [PATCH 01/46] Add Prettier Autofix Workflow for Markdown Signed-off-by: Philipp Hugenroth --- .github/workflows/prettify.yml | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/prettify.yml diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml new file mode 100644 index 0000000000..fd6c403bd2 --- /dev/null +++ b/.github/workflows/prettify.yml @@ -0,0 +1,55 @@ +name: Prettier + +on: + push: + branches: + - master + +jobs: + autofix-markdown: + name: Autofix Markdown files using Prettier + runs-ons: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + # Fetch changes to previous commit - required for 'only_changed' in Prettier action + fetch-depth: 0 + + # Beginning of yarn setup, keep in sync between all workflows, see ci.yml + - name: use node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + registry-url: https://registry.npmjs.org/ # Needed for auth + - name: cache all node_modules + id: cache-modules + uses: actions/cache@v2 + with: + path: '**/node_modules' + key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }} + - name: find location of global yarn cache + id: yarn-cache + if: steps.cache-modules.outputs.cache-hit != 'true' + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: cache global yarn cache + uses: actions/cache@v2 + if: steps.cache-modules.outputs.cache-hit != 'true' + with: + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: yarn install + run: yarn install --frozen-lockfile + # End of yarn setup + + - name: Run Prettier + uses: creyD/prettier_action@v3.1 + with: + # Modifies commit only if prettier autofixed a Markdown file + prettier_options: --config docs/prettier.config.js --write docs/**/*.md + only_changed: true + commit_message: "Autofix Markdown files using Prettier" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + From ffddba6f42fa7bb4dce3be77cee6a7b3304b32a7 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 22 Jun 2021 10:37:00 +0200 Subject: [PATCH 02/46] Add autofix for root Markdown Signed-off-by: Philipp Hugenroth --- .github/workflows/prettify.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml index fd6c403bd2..fe8452e6df 100644 --- a/.github/workflows/prettify.yml +++ b/.github/workflows/prettify.yml @@ -43,12 +43,22 @@ jobs: run: yarn install --frozen-lockfile # End of yarn setup - - name: Run Prettier + - name: Run Prettier on docs uses: creyD/prettier_action@v3.1 with: # Modifies commit only if prettier autofixed a Markdown file prettier_options: --config docs/prettier.config.js --write docs/**/*.md only_changed: true + commit_message: "Autofix documentation Markdown files using Prettier" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Prettier on root + uses: creyD/prettier_action@v3.1 + with: + # Modifies commit only if prettier autofixed a Markdown file + prettier_options: --write *.md + only_changed: true commit_message: "Autofix Markdown files using Prettier" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c599fd104ea22ed896a65c8ba3712a6318d232b3 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 22 Jun 2021 10:45:45 +0200 Subject: [PATCH 03/46] Run prettier on prettier.yml,... Signed-off-by: Philipp Hugenroth --- .github/workflows/prettify.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml index fe8452e6df..36d3275f66 100644 --- a/.github/workflows/prettify.yml +++ b/.github/workflows/prettify.yml @@ -49,7 +49,7 @@ jobs: # Modifies commit only if prettier autofixed a Markdown file prettier_options: --config docs/prettier.config.js --write docs/**/*.md only_changed: true - commit_message: "Autofix documentation Markdown files using Prettier" + commit_message: 'Autofix documentation Markdown files using Prettier' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -59,7 +59,6 @@ jobs: # Modifies commit only if prettier autofixed a Markdown file prettier_options: --write *.md only_changed: true - commit_message: "Autofix Markdown files using Prettier" + commit_message: 'Autofix Markdown files using Prettier' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - From e6a53d79f0c8b0369c82613d27c73c27283af8eb Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Thu, 29 Jul 2021 12:05:37 -0400 Subject: [PATCH 04/46] add FireHydrant plugin Signed-off-by: Christine Yi --- plugins/firehydrant/.eslintrc.js | 3 + plugins/firehydrant/README.md | 66 +++++ plugins/firehydrant/api-report.md | 23 ++ plugins/firehydrant/dev/index.tsx | 19 ++ plugins/firehydrant/doc/firehydrant_logo.png | Bin 0 -> 5387 bytes .../doc/firehydrant_plugin_screenshot.png | Bin 0 -> 57062 bytes plugins/firehydrant/package.json | 67 +++++ plugins/firehydrant/src/api/index.ts | 137 +++++++++ plugins/firehydrant/src/api/types.ts | 33 +++ .../ServiceAnalytics.test.tsx | 42 +++ .../ServiceAnalytics/ServiceAnalytics.tsx | 164 ++++++++++ .../ServiceDetailsCard.test.tsx | 118 ++++++++ .../ServiceDetailsCard/ServiceDetailsCard.tsx | 279 ++++++++++++++++++ .../components/ServiceDetailsCard/index.ts | 16 + .../src/components/serviceAnalytics.ts | 51 ++++ .../src/components/serviceDetails.ts | 39 +++ plugins/firehydrant/src/components/types.ts | 29 ++ plugins/firehydrant/src/index.ts | 16 + plugins/firehydrant/src/plugin.test.ts | 22 ++ plugins/firehydrant/src/plugin.ts | 46 +++ plugins/firehydrant/src/routes.ts | 20 ++ plugins/firehydrant/src/setupTests.ts | 17 ++ 22 files changed, 1207 insertions(+) create mode 100644 plugins/firehydrant/.eslintrc.js create mode 100644 plugins/firehydrant/README.md create mode 100644 plugins/firehydrant/api-report.md create mode 100644 plugins/firehydrant/dev/index.tsx create mode 100644 plugins/firehydrant/doc/firehydrant_logo.png create mode 100644 plugins/firehydrant/doc/firehydrant_plugin_screenshot.png create mode 100644 plugins/firehydrant/package.json create mode 100644 plugins/firehydrant/src/api/index.ts create mode 100644 plugins/firehydrant/src/api/types.ts create mode 100644 plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx create mode 100644 plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx create mode 100644 plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx create mode 100644 plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx create mode 100644 plugins/firehydrant/src/components/ServiceDetailsCard/index.ts create mode 100644 plugins/firehydrant/src/components/serviceAnalytics.ts create mode 100644 plugins/firehydrant/src/components/serviceDetails.ts create mode 100644 plugins/firehydrant/src/components/types.ts create mode 100644 plugins/firehydrant/src/index.ts create mode 100644 plugins/firehydrant/src/plugin.test.ts create mode 100644 plugins/firehydrant/src/plugin.ts create mode 100644 plugins/firehydrant/src/routes.ts create mode 100644 plugins/firehydrant/src/setupTests.ts diff --git a/plugins/firehydrant/.eslintrc.js b/plugins/firehydrant/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/firehydrant/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/firehydrant/README.md b/plugins/firehydrant/README.md new file mode 100644 index 0000000000..98f36eb4c3 --- /dev/null +++ b/plugins/firehydrant/README.md @@ -0,0 +1,66 @@ +# FireHydrant Plugin + +## Overview + +The [FireHydrant](https://firehydrant.io) plugin brings incident management to Backstage, and it displays service incidents information such as active incidents and incident analytics. There are also quick action links that let you create and view incidents in FireHydrant. + +firehydrant plugin screenshot + +## Features + +- View total active incidents for a service declared in FireHydrant +- Quick links to the top 5 most recent active incidents in FireHydrant +- View incident metrics for a given service within the last 30 days, including: healthiness, total time impacted, total number of incidents, and MTT* (Mean Time To *) data such as MTTD (detect), MTTA (acknowledge), MTTM (mitigate) and MTTR (resolve). + +## Setup + +1. Install the plugin: + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-firehydrant +``` + +2. Add the plugin to `EntityPage.tsx`, inside the `const overviewContent`'s parent `` component: + +```ts +// In packages/app/src/components/catalog/EntityPage.tsx +import { FirehydrantPage } from '@backstage/plugin-firehydrant'; + +// Add to code as a grid item + + +; +``` + +3. Add proxy configuration to `app-config.yaml`: + +```yaml +proxy: +'/firehydrant/api': + target: 'https://api.firehydrant.io/v1/' + changeOrigin: true + headers: + # Supply the token you generated from https://app.firehydrant.io/organizations/bots + Authorization: Bearer ${FIREHYDRANT_BOT_TOKEN} +``` + +as well as the default url configuration to `app-config.yaml`: + +```yaml +firehydrant: + baseUrl: https://api.firehydrant.io +``` + +Note: if you are not using environment variables, you can directly type the API Bot Token into `app-config.yaml`: + +```yaml +proxy: +'/firehydrant/api': + target: 'https://api.firehydrant.io/v1/' + changeOrigin: true + headers: + # Supply the token you generated from https://app.firehydrant.io/organizations/bots + Authorization: Bearer fhb-e4911b22bcd788c4a4afeb0c111ffbfa +``` diff --git a/plugins/firehydrant/api-report.md b/plugins/firehydrant/api-report.md new file mode 100644 index 0000000000..d12c2499c9 --- /dev/null +++ b/plugins/firehydrant/api-report.md @@ -0,0 +1,23 @@ +## API Report File for "@backstage/plugin-firehydrant" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const FirehydrantPage: () => JSX.Element; + +// @public (undocumented) +export const firehydrantPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/firehydrant/dev/index.tsx b/plugins/firehydrant/dev/index.tsx new file mode 100644 index 0000000000..237cb2737e --- /dev/null +++ b/plugins/firehydrant/dev/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createDevApp } from '@backstage/dev-utils'; +import { firehydrantPlugin } from '../src/plugin'; + +createDevApp().registerPlugin(firehydrantPlugin).render(); diff --git a/plugins/firehydrant/doc/firehydrant_logo.png b/plugins/firehydrant/doc/firehydrant_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3db13826348372b0847b9a891ea855c15c1a57c4 GIT binary patch literal 5387 zcmZu#X*d+z`(_&1nHfxEnHgi3tt^8~gJF<;S12W0g|S2=uXQkDl%&WycG=Qv=QU}1&vmYIo@8rF952^-E*2IRUQ-jS?Wyno z&p6JWj&Gc6@=hIRpovQ`3k$cvf5yu4Dp&Y4$Qo>mGhnItB({7y0eb6O=(DiYr*j{A zu(7ZR7MNo7?Za8uUR$KPjy&%>X{vnt3NzqO@#1=ND{GGP06%vR@z?7M#wlmxfLv8K zjqj=LYR;sFQL|WZ=S6()_4Rsj)6fgIlaaG|MmcgBfbNOenis+AQMLOHEmu}L!mfNe z+H{kfI5bUQV=nts1o1{Z93_e3afK8_?j4QDgJRBuz;DVZL%TH zO;>9*fUO0N!6>n+=Tk6_*fJ~!3F{15~Jh509YZd&b*QClT7D?^yuhC@^n@NzHsj`n(Q&!LF@fUkR+Xl zP!SNSM?}AV-mtW9tdGEJ6gKHr&J~iXDJu_O6o877?LX~PAnpb`oJC;$J>;WeU7I;+~EUDE#vSC-)7wI5oVdgN~75V9D zsHxG3<##AM(j6Xh>#?0n%-vqQxJYoUdSFL=~Rz_y`B(>9|cQz&_xfsV;M0?Gx*47JjbO&3AAV$ zB{-(ip%0Oig?g;`DK)@u8J#9EjB8bm1vQRk&)UsPrE%p8;Ta2YQ?GLGK`zIB8#ZhX zIZzLZrRr3J-4)L-O@Eas$E@Ia-Suw8=9beC73r5H@tc#4OxMhiFiGbW2d9c{ zvkD$jsWk5tR79|M|LSB75S+9tDicJ$@^^jz*(bYb<%(JPi}*UVXnu(;WsnZy^50um z`uQKyXYgrC=NFR<#S%r(iMaL%kP@GC0D1fjom%)OFOSG{RHw6)cDCc2;M@Olk(xZ`{g*phLldtmgw|)tH6l3%+da;9oTviDr-@y1 z`=pn=ck@K8xvQPuQqn{(N~#MSWPB6$XULgAKHgFef8(yvY+7%;udJc(A817wFoHJc zMqVrmT8|ocE+q4hvKfR`&Ar?S5|#=(15m;E_Y5D)oh-C>S7I7Bk}ma$C(fgTO0KqS zz1%iBPQ#2pf!~hGFeeE#9Q4CV(`yU9h~5Q!tq@`~J0jig2A7mr$nfyXd$DDh*BVMdJyv@!Tzp~m6c@JD3W`raO z;)*(NXW)fr`JceGGehq@I5R5sOBZS*C=#8@!hj}8XjnxnDNfLR(!*)TAGxGmn|ZiFT%s?H@kqI09b0>IkRr_rItp?&FSS zt_BiBY?!aSb_9|r9^|u4AKbG>aGmBd(o-EG2OVk*p?(B8p2AsikfG)2Ff=8;fhNDj zFqEz3JtB1GRV*^?Ov3`&_etwu&#W)sAceeEA3a@$rg%T2$!(ZQED%COZC_Yke}aQO zEyGBH3C7*#Z{;c+;&u@ucAEyUXA%dcTHeKA_TS>-%)|9$Fzc1=5$uQ?9C^sHMS0U; z^Jb#t2YZ=dyK*M;`5=u#-~0x9Y6k>=$91Zh1-NP(73zO|{)<*loRFQ82QZNv*pl?8 zUhfb$7J3L|6;OB7e3e05_9OjACFurn=U_wLV)G|pel93mz!&kZ1bO zO%ljZ{UIUt+aEfl2mLl@PO+X>?-_p8G9|{z7q5Jj8zT}zpe9kQW>?>uSKa$S$A~yg zgA?MEqfDk1vdD4}9IQnzeB3$(Q_694EnHulfF=i6?Jttz_Rv99*+Bs$y`FRBn`|YG zWpMp!zEhNgg(t%w_XX!K3jXPiOuMl%sL?|lso-ChB}D?($H9yY-5&e5{&1cgsWhke z{8K`gH0Qn$7!VUZ9X%tt@NEyCsQDTWt;$YQt-co~Vqpt6I29e^3}Hjjm8K@JqQG+Z zDK#r+xRC$D&fsa4w1syAzyWZu`Wm?lV{kxv2OL|tmhrH~DZ}Kj?k!6AlF&(d%+pB{oZh3jmuv4kQ`2QCxu(~p z)3hNv67S2~HW59gZG^QTUF)L@fAq{A&>jj15sN7*Od-M_cM-f~1G}EAg%B5W^wlS0i1?AM#MvWiEFC z#v@AmAl*|<@{ZTv6TY?G?Py!nN{~CB{hsJHTus!ZsmVTS8LQ;4mjs2Hb|DabhPh6p zMwHi{&vbx2>(B*dX4+Cs$3A8%6w;Ry7$eV2T)Wt~WO%QpT39nBbyCT43eu+_$#Y5b z?f&Z=8lEeyyw3~GIl1LCNEjC|@qy8oKE2VS@pR!{+=m&IxEwsGblRhopcYKZy`}i^$2&E&p*5DA$$I$dCNg-IaBZ;-rM2fDU#o;+$bY*!wKx zgwCV4!G6Q)mmmHH2G=LLLXx7AhT6{XQ)*7r06JeT+VH+z8ZT^MPV0&;@DDCYl+}vA zG2?$f{M8*N_}t9kfdHeu?k@f^cB5a2M~L7-az3aFS>%)FJ1q@GG_?}5BE)C7PeKSu zLHh`oO97(UC+R0a8(Db`kM+t?aPs8cZbI|8hhGY*d_8P^Mfm7jwj`n9vS`?R^qh5? zWXL3ke42LQ*g(UK;VQS(qky0?-6kmerlxDQ-_I+95Sj_h<^Xg6;M_3et8Vpj47HEG zg%=};Qphg0dzLW7aCJAD+KR#Mk%UGEDrs9fGwQrt!&qq`*utLcM2$*l-h>YbB1C2< zI?k>v(5+FNWA_k?Qi_L-0`7`;|Ki6ZNt{a$v^vrDg3SCY7qK1*Bdc5vEESzNDe$E=*zR4*mJ(l&+-G%S96@vC%kbwO zwSNfB+we#1{JLvYvU6*!J#j7^gjdZv2T5X8N-JZ;u`h2#7q5Ti*tr#P015-x03H+; z=`vl#HoNMs6MGns=N;BHwBp(2Poh8W+FtHhS~hewK0PFrap!Ve9bMHk(`)(LgKBTt z!)x)VvwI+K0JtGvC%>`@EWxJ#i=z!oyiN9&4b=JNX_w<({`l8!D-muU4hv!lGH%G< zp2GeVy&vC1+X6~;OO*E=BmCJUy9xaWbAjiwji}K^lzY57UOlz-9G5HZbW<#!!idd| zG`^jsBDL)RR0gPs>OaeMwx&;3Cc}s7k=k0vK^Xwv83?ZYAX*zed9+Sa+vgk7Hy`cg71Mi4e6^C4)%RqP8rocraBVywB% zxL5p1im!dgzBZI*Gcjd4Dk^jwpB4@_*+MYEgXOKzK4?K1F`d!4gQ9tq>pbo%v66g&$&OoAe`fR=+%aqZ$p6|J`tYPkFbdHJ*vc2GY8TxPJ3`^*ip4J!jp* zYcZ;vDLr-4sMv4cjRN2S3K4zjqZ#hRm zb)?gT5%}3Px97jQviAGOG9<4bX$tL#>Su3$e7aATU(QRg#xgU$&+$zaNSf3S1msG> zn#qyp4U&t}C;2*>$h#_}H}`;)ijm*8c2yj0tOvFk%(ae>&%4vEr%~HR;NEs+$ z%i4&Ipr2P_RVxliT=`~7%U&{kUSA}VJ%CSl!~5@ZE%(7muKWGh?XMp6hRLL2df#)! zS2@;RWr%~B8T=lb{#_oxZMUbv4X2c;)B3_T%nT->xbGnk76BTmUmsMY{CP^<>Bw;mGiak*`vpH^!V{|67P|u~t&_T{A66Pi`WB`sLF!Ts_F=~El_BS41&OH5C ziWtx7VlC#Zs?*yQ!{j2`*{=%5@XS>z=e8 zOXTHI7Qn)HpezDZZf2NR#=LF{s?PUgb`z(N{;eNd&n;#?7E!xlwrj{6ym_SXf4%%h zKxS~88Vl~2xR_*SMlGO=Z86c@WRj=~5HV6DanofGrBk?0SSdM@_SkQvE)hg+k=}lh zIhX$z{Mwra`Q3~StAy*W?_v$xaiIG$b64vbCN1Qh>V_rcbdH6NZ0)Yghca%2ZT0x> z@(EVV&9#|a$`}qKv13|yxPVDV(6niPnM$GO`Q3tvQJ569H0)n9Aar`t&A*XbXJ?9rU~qCHTYs#hQiNMqF^r zHvxfk!v_qKEMMBW=u!UQ6=c9THMJKNp?uCy00y3CaeMT%gJ6zVKgg)6rgqDZgl>y= z;q_RW6)f3`>=Z9$Ys8q(r`&FH(BxNmbhXbn<-8pSWuj7MC%X^7&%1$<@ya!{m48t- zJ30Ix`Qr6|GLP4XZHtUWWlt}bKBTAWmH2_BEhjw#WnYkLSE4MmSA42()~B+S*R{l% zpA*{mB7#2qbBI@%$FfXNd7I7!DK-}U1wc#}t~^}_Hq7XbI<$iF&uJ)Xso*!YoAskN zM3yN&bqb_^Gk+I+8YZ#jYzYiKi1=V4b|Ez<)ADtOb?aKJM$A%ww~%7O3j{k_O*8^2 z>Jb?_aL|u4B^HBwAHGN5RyoV<*{SPilwp{M`ZOfIpGnS4F>O!Jt%?g<-e|RnFqDs9tg-Jd@vTgzxxFKyS&~=z8kf~QUFd2Os_Y`PW|O@l<{P@FJw|G z{SW@b^030HJZMehZ$@Lr^p|4qSr{@!MET3zW~gDK57JfazW(I5zN|Ee4exx57cORC zki5OF@v5#JRiA@sl`rCA~S~QPnk$Fd}I6t)o-fXm`c8V~TT0*ILg0_+O^ak4qB@mhN8P zyCAK$+AplThWqhGh4iuFQdGAnD<*njF}jFJij-rFrEpC4fISUlg8|Apj+yk;!CuN+ zo1??LKe>mrF0R{0Tlvc|DHTicH4|zWT)}}?-tDpZpQu}q@W|y literal 0 HcmV?d00001 diff --git a/plugins/firehydrant/doc/firehydrant_plugin_screenshot.png b/plugins/firehydrant/doc/firehydrant_plugin_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e79bb27339f22a0e199f0809dd1d1d53ff5d2d GIT binary patch literal 57062 zcmb@tWl)ga_XUciw35;wNOyNgcY}0ycc%i%wq{y*Gt z81cz-&U5zOYpuNuRge=$M!-dYfPg@jln_yZfOvBWesbQzf+O$Wu@0HW^= zekCWA8YHoDJYDGNjp5&J3PyWj2sqX^Z$3bhhtObFcXoY!^O5C3Z|jKwLUx3Mj>6~Z z;pK&znzt<;5hDJ(J+u74ut)fF%a*cePWdh?>5LN z*i{Z4^FqV4~iNBz$li8sDM_|Q

gH2c{@qG!3Q5{r*IOL2 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

7^9j0^lG5-`K>j@po`bNMRLvBJ+a3 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 + + getServiceDetails(options: { + serviceName: string; + }): Promise; + + getServiceIncidents(options: { + serviceId: string; + }): Promise; +} + +export const fireHydrantApiRef = createApiRef({ + id: 'plugin.firehydrant.service', + description: 'Used by FireHydrant plugin for requests', +}); + +export type Options = { + discoveryApi: DiscoveryApi; + proxyPath?: string; +}; + +const DEFAULT_PROXY_PATH = '/firehydrant/api'; + +export class FireHydrantAPIClient implements FireHydrantAPI { + private readonly discoveryApi: DiscoveryApi; + private readonly proxyPath: string; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + this.proxyPath = options.proxyPath ?? DEFAULT_PROXY_PATH; + } + + async getServiceAnalytics(options: { + serviceId: string; + startDate: string; + endDate: string; + }): Promise { + const proxyUrl = await this.getApiUrl(); + const response = await fetch( + `${proxyUrl}/metrics/services/${options.serviceId}?start_date=${options.startDate}&end_date=${options.endDate}`, + ); + if (!response.ok) { + throw new Error( + `There was a problem fetching FireHydrant analytics data: ${response.statusText}`, + ); + } + const json = await response.json(); + return json; + } + + async getServiceDetails(options: { + serviceName: string; + }): Promise { + const proxyUrl = await this.getApiUrl(); + const response = await fetch( + `${proxyUrl}/services?query=${options.serviceName}`, + ); + + if (!response.ok) { + throw new Error( + `There was a problem fetching FireHydrant data: ${response.statusText}`, + ); + } + + const json = await response.json(); + + const servicesData: ServiceDetailsResponse = { + service: {} as Service, + incidents: [] as Incident[], + }; + + if (response.ok) { + if (json.data?.length === 0) { + return servicesData; + } + + servicesData.service = json.data[0]; + + const incidentsJson = await this.getServiceIncidents({ + serviceId: json.data[0].id, + }); + + servicesData.incidents = incidentsJson; + } + return servicesData; + } + + async getServiceIncidents(options: { + serviceId: string; + }): Promise { + const proxyUrl = await this.getApiUrl(); + const response = await fetch( + `${proxyUrl}/incidents?services=${options.serviceId}&active=true`, + ); + if (!response.ok) { + throw new Error( + `There was a problem fetching FireHydrant incidents data: ${response.statusText}`, + ); + } + + const json = await response.json(); + return json.data; + } + + private async getApiUrl() { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return proxyUrl + this.proxyPath; + } +} diff --git a/plugins/firehydrant/src/api/types.ts b/plugins/firehydrant/src/api/types.ts new file mode 100644 index 0000000000..089c850c97 --- /dev/null +++ b/plugins/firehydrant/src/api/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Incident, Service } from '../components/types'; + +export type ServiceDetailsResponse = { + service?: Service; + incidents?: ServiceIncidentsResponse; +}; + +export type ServiceAnalyticsResponse = { + id: string; + mttd?: number; + mtta?: number; + mttm?: number; + mttr?: number; + count: number; + total_time: number; +}; + +export type ServiceIncidentsResponse = Incident[]; diff --git a/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx new file mode 100644 index 0000000000..82bbac57c1 --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { ServiceAnalytics } from './ServiceAnalytics'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('ServiceAnalytics', () => { + it('renders service analytics', async () => { + await renderInTestApp( + , + ); + + expect(await screen.findByText(/32/)).toBeInTheDocument(); + expect(await screen.findByText(/44/)).toBeInTheDocument(); + }); +}); diff --git a/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx new file mode 100644 index 0000000000..996d705a9a --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx @@ -0,0 +1,164 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import moment, { Moment } from 'moment'; +import Alert from '@material-ui/lab/Alert'; +import { Table, TableColumn, Progress } from '@backstage/core-components'; + +const useStyles = makeStyles({ + container: { + overflow: 'auto', + width: '100%', + '& h5': { + fontSize: '18px !important', + }, + '& td': { + minWidth: '145px', + }, + '& th': { + minWidth: '145px', + }, + }, +}); + +export const DenseTable = ({ + service, + startDate, + endDate, +}: { + service: object; + startDate: Moment; + endDate: Moment; +}) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { field: 'healthiness', title: 'Healthiness' }, + { field: 'impacted', title: 'Impacted' }, + { field: 'incidents', title: 'Incidents' }, + { field: 'mttd', title: 'MTTD' }, + { field: 'mtta', title: 'MTTA' }, + { field: 'mttm', title: 'MTTM' }, + { field: 'mttr', title: 'MTTR' }, + ]; + + return ( +

+ + + ); +}; + +export const secondsToDhms = (seconds: number) => { + const secs = Number(seconds); + const d = Math.floor(secs / (60 * 60 * 24)); + const h = Math.floor((secs / (60 * 60)) % 24); + const m = Math.floor((secs / 60) % 60); + const s = secs % 60; + + const dDisplay = d > 0 ? `${d}d ` : ''; + const hDisplay = h > 0 ? `${h}h ` : '00h '; + const mDisplay = m > 0 ? `${m}m ` : '00m '; + const sDisplay = s > 0 ? `${s}s` : '00s'; + return dDisplay + hDisplay + mDisplay + sDisplay; +}; + +export const truncateNum = (num: number) => { + const matcher = `^-?\\d+(?:\\.\\d{0,3})?`; + const re = new RegExp(matcher); + const match = num.toString().match(re); + const result = (match && match[0]) || '0'; // eslint-disable-line no-mixed-operators + return result; +}; + +export const calcHealthiness = ({ + mttm, + incidents, + range, +}: { + mttm: number; + incidents: number; + range: number; +}) => { + const num = (1 - (mttm * incidents) / range) * 100; + return `${truncateNum(num)}%`; +}; + +type AnalyticsDataType = { + id?: string; + count?: number; + mttd?: number; + mtta?: number; + mttm?: number; + mttr?: number; + total_time?: number; +}; + +export const ServiceAnalytics = ({ + value, + loading, + error, +}: { + value: AnalyticsDataType; + loading: boolean; + error: any; +}) => { + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + const startDate = moment().subtract(30, 'days').utc(); + const endDate = moment().utc(); + + // Transform and format the data to display in the table + if (value.id) { + const serviceData = { + healthiness: + value.mttm && value.count + ? calcHealthiness({ + mttm: value.mttm, + incidents: value.count, + range: endDate.diff(startDate, 'seconds'), + }) + : '100%', + impacted: value.total_time ? secondsToDhms(value.total_time) : '-', + incidents: value.count, + mttd: value.mttd ? secondsToDhms(value.mttd) : '-', + mtta: value.mtta ? secondsToDhms(value.mtta) : '-', + mttm: value.mttm ? secondsToDhms(value.mttm) : '-', + mttr: value.mttr ? secondsToDhms(value.mttr) : '-', + }; + + return ( + + ); + } + + return null; +}; diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx new file mode 100644 index 0000000000..1123f014da --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { fireHydrantApiRef } from '../../api'; +import { screen } from '@testing-library/react'; +import { ServiceDetailsCard } from './ServiceDetailsCard'; +import { Service, Incident } from '../types'; +import { renderInTestApp } from '@backstage/test-utils'; + +const mockFireHydrantApi = { + getServiceDetails: () => {}, + getServiceAnalytics: () => {}, +}; + +const apis = ApiRegistry.from([[fireHydrantApiRef, mockFireHydrantApi]]); + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntity: () => { + return { entity: { metadata: { name: 'service-example' } } }; + }, +})); + +describe('ServiceDetailsCard', () => { + it('renders service incidents', async () => { + mockFireHydrantApi.getServiceDetails = jest.fn().mockImplementationOnce( + async () => + [ + { + service: { + id: '12345', + name: 'service-example', + description: 'This is a sample description', + active_incidents: ['654321'], + } as Service, + incidents: [ + { + id: '654321', + active: true, + description: 'test incident', + name: 'incident name here', + incident_url: 'http://example.com', + } as Incident, + ], + }, + ][0], + ); + + mockFireHydrantApi.getServiceAnalytics = jest.fn().mockImplementationOnce( + async () => + [ + { + buckets: [ + { + metrics: { + 12345: { + count: 0, + mtta: null, + mttd: null, + mttm: null, + mttr: null, + total_time: null, + }, + }, + }, + ], + }, + ][0], + ); + + await renderInTestApp( + + + , + ); + + expect( + await screen.findByText(/View service incidents/), + ).toBeInTheDocument(); + expect( + await screen.findByText(/There is 1 active incident/), + ).toBeInTheDocument(); + expect(await screen.findByText(/incident name here/)).toBeInTheDocument(); + }); + + it('handles empty response', async () => { + mockFireHydrantApi.getServiceDetails = jest + .fn() + .mockImplementationOnce(async () => []); + + mockFireHydrantApi.getServiceAnalytics = jest + .fn() + .mockImplementationOnce(async () => []); + + await renderInTestApp( + + + , + ); + + expect( + await screen.findByText(/This service does not exist in FireHydrant/), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx new file mode 100644 index 0000000000..7ebbcd8351 --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx @@ -0,0 +1,279 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useEffect, useState } from 'react'; +import moment from 'moment'; +import { ServiceAnalytics } from '../ServiceAnalytics/ServiceAnalytics'; +import { + Box, + Button as MaterialButton, + Typography, + makeStyles, +} from '@material-ui/core'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import NotesIcon from '@material-ui/icons/Notes'; +import WhatshotIcon from '@material-ui/icons/Whatshot'; +import WarningIcon from '@material-ui/icons/Warning'; +import AddIcon from '@material-ui/icons/Add'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Incident } from '../types'; +import { ServiceIncidentsResponse } from '../../api/types'; +import { useServiceDetails } from '../serviceDetails'; +import { useServiceAnalytics } from '../serviceAnalytics'; +import { InfoCard, Progress } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +const useStyles = makeStyles(theme => ({ + infoCard: { + height: '342px', + }, + button: { + color: '#3b2492', + display: 'grid', + gridGap: '4px', + textAlign: 'center', + justifyItems: 'center', + width: '102px', + backgroundColor: theme.palette.type === 'dark' ? '#f1edff' : '', + '&:hover, &:focus': { + backgroundColor: '#f1edff', + color: '#614ab6', + }, + '&:active': { + color: '#3b2492', + backgroundColor: '#b2a6e3', + boxShadow: + 'rgb(59, 36, 146) 0px 0px 0px 1px inset, rgb(141, 134, 188) 3px 3px 0px 0px inset;', + }, + border: '1px solid #3b2492', + borderRadius: '5px', + padding: '10px', + }, + buttonLink: { + backgroundColor: '#3b2492', + color: '#FFF', + textTransform: 'none', + '&:hover': { + backgroundColor: '#614ab6', + }, + }, + buttonContainer: { + display: 'grid', + gridGap: '24px', + gridAutoFlow: 'column', + gridAutoColumns: 'min-content', + }, + icon: { + color: '#f1642d', + }, + link: { + textDecoration: 'underline', + fontSize: '16px', + lineHeight: '27px', + color: '#3b2492', + '&:hover, &:focus': { + fontWeight: '500', + }, + }, + linksContainer: { + borderBottom: '1px solid #d5d5d5', + padding: '10px 0px 10px 20px', + backgroundColor: '#f1edff', + marginBottom: '20px', + }, + table: { + width: '100%', + }, + warning: { + display: 'flex', + alignItems: 'center', + padding: '10px', + background: '#f1edff', + color: theme.palette.type === 'dark' ? '#3b2492' : '#fff', + }, +})); + +const ServiceWarning = ({ text }: { text: string }) => { + const classes = useStyles(); + return ( +
+ +   {text} +
+ ); +}; + +const ServiceAnalyticsView = ({ + serviceId, + startDate, + endDate, +}: { + serviceId: string; + startDate: moment.Moment; + endDate: moment.Moment; +}) => { + const { + loading: analyticsLoading, + value: analyticsValue = {}, + error: analyticsError, + } = useServiceAnalytics({ + serviceId, + startDate: startDate.format('YYYY-MM-DD'), + endDate: endDate.format('YYYY-MM-DD'), + }); + + return ( + + ); +}; + +export const ServiceDetailsCard = () => { + const { entity } = useEntity(); + const classes = useStyles(); + const [showServiceDetails, setShowServiceDetails] = useState(false); + const configApi = useApi(configApiRef); + + const BASE_URL = + configApi.getOptionalString('firehydrant.baseUrl') || + 'https://app.firehydrant.io'; + + const startDate = moment().subtract(30, 'days').utc(); + const endDate = moment().utc(); + + const { loading, value, error } = useServiceDetails({ + serviceName: entity?.metadata?.name, + }); + + const activeIncidents: string[] = value?.service?.active_incidents ?? []; + const incidents: ServiceIncidentsResponse = value?.incidents ?? []; + const serviceId: string = value?.service?.id!; + + useEffect(() => { + if (value?.service && Object.keys(value?.service).length > 0) { + setShowServiceDetails(true); + } + }, [value]); + + if (loading) { + return ; + } + + if (error) { + return ( + + ); + } + + const headerText: string = showServiceDetails + ? `There ${activeIncidents?.length === 1 ? 'is' : 'are'} ${ + activeIncidents?.length + } active incident${activeIncidents?.length === 1 ? '' : 's'}.` + : ''; + + const serviceIncidentsLink: string = `${BASE_URL}/incidents?search={"services":[{"label":${JSON.stringify( + value?.service?.name, + )},"value":${JSON.stringify(value?.service?.id)}}]}`; + + return ( + + {!showServiceDetails && !loading && ( + + )} + {showServiceDetails && ( + + +

{headerText}

+
+ + + View service incidents   + + + +
+ )} + {activeIncidents && activeIncidents?.length > 0 && ( + + {incidents && + incidents?.slice(0, 5).map((incident: Incident, index: number) => ( + + ))} + + )} + + View in FireHydrant + + + + Declare an incident + + + + View all incidents + + {showServiceDetails && ( + + + View Service Details + + )} + + + {showServiceDetails && ( + + + + )} +
+ ); +}; diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/index.ts b/plugins/firehydrant/src/components/ServiceDetailsCard/index.ts new file mode 100644 index 0000000000..33e68ecf3b --- /dev/null +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ServiceDetailsCard } from './ServiceDetailsCard'; diff --git a/plugins/firehydrant/src/components/serviceAnalytics.ts b/plugins/firehydrant/src/components/serviceAnalytics.ts new file mode 100644 index 0000000000..ba88af86da --- /dev/null +++ b/plugins/firehydrant/src/components/serviceAnalytics.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useAsyncRetry } from 'react-use'; +import { fireHydrantApiRef } from '../api'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useServiceAnalytics = ({ + serviceId, + startDate, + endDate, +}: { + serviceId: string; + startDate: string; + endDate: string; +}) => { + const api = useApi(fireHydrantApiRef); + const errorApi = useApi(errorApiRef); + + const { loading, value, error, retry } = useAsyncRetry(async () => { + try { + return await api.getServiceAnalytics({ + serviceId: serviceId, + startDate: startDate, + endDate: endDate, + }); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }); + + return { + loading, + value, + error, + retry, + }; +}; diff --git a/plugins/firehydrant/src/components/serviceDetails.ts b/plugins/firehydrant/src/components/serviceDetails.ts new file mode 100644 index 0000000000..b8cac2736a --- /dev/null +++ b/plugins/firehydrant/src/components/serviceDetails.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useAsyncRetry } from 'react-use'; +import { fireHydrantApiRef } from '../api'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useServiceDetails = ({ serviceName }: { serviceName: string }) => { + const api = useApi(fireHydrantApiRef); + const errorApi = useApi(errorApiRef); + + const { loading, value, error, retry } = useAsyncRetry(async () => { + try { + return await api.getServiceDetails({ serviceName: serviceName }); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }); + + return { + loading, + value, + error, + retry, + }; +}; diff --git a/plugins/firehydrant/src/components/types.ts b/plugins/firehydrant/src/components/types.ts new file mode 100644 index 0000000000..e607b2357d --- /dev/null +++ b/plugins/firehydrant/src/components/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type Service = { + id: string; + name: string; + description?: string; + active_incidents?: string[]; +}; + +export type Incident = { + id: string; + active: boolean; + description?: string; + name: string; + incident_url: string; +}; diff --git a/plugins/firehydrant/src/index.ts b/plugins/firehydrant/src/index.ts new file mode 100644 index 0000000000..3b9b584fb8 --- /dev/null +++ b/plugins/firehydrant/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { firehydrantPlugin, FirehydrantPage } from './plugin'; diff --git a/plugins/firehydrant/src/plugin.test.ts b/plugins/firehydrant/src/plugin.test.ts new file mode 100644 index 0000000000..ffde182e64 --- /dev/null +++ b/plugins/firehydrant/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { firehydrantPlugin } from './plugin'; + +describe('firehydrant', () => { + it('should export plugin', () => { + expect(firehydrantPlugin).toBeDefined(); + }); +}); diff --git a/plugins/firehydrant/src/plugin.ts b/plugins/firehydrant/src/plugin.ts new file mode 100644 index 0000000000..e4794ab4ce --- /dev/null +++ b/plugins/firehydrant/src/plugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { fireHydrantApiRef, FireHydrantAPIClient } from './api'; +import { + createApiFactory, + createPlugin, + discoveryApiRef, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const firehydrantPlugin = createPlugin({ + id: 'firehydrant', + apis: [ + createApiFactory({ + api: fireHydrantApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new FireHydrantAPIClient({ discoveryApi }), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +export const FirehydrantPage = firehydrantPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ServiceDetailsCard').then(m => m.ServiceDetailsCard), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/firehydrant/src/routes.ts b/plugins/firehydrant/src/routes.ts new file mode 100644 index 0000000000..eb2f8b6375 --- /dev/null +++ b/plugins/firehydrant/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + title: 'firehydrant', +}); diff --git a/plugins/firehydrant/src/setupTests.ts b/plugins/firehydrant/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/firehydrant/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; From d5a69640bca9495304751b5ea8d0ef54a9508fa6 Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Mon, 2 Aug 2021 12:07:31 -0400 Subject: [PATCH 05/46] update api-report, add FireHydrant to vocab.txt Signed-off-by: Christine Yi --- .github/styles/vocab.txt | 2 ++ plugins/firehydrant/README.md | 2 +- plugins/firehydrant/api-report.md | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 48740ef367..c3ad830034 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -83,6 +83,8 @@ facto failover Fargate Figma +firehydrant +FireHydrant Firekube Fiverr gitbeaker diff --git a/plugins/firehydrant/README.md b/plugins/firehydrant/README.md index 98f36eb4c3..572e873c83 100644 --- a/plugins/firehydrant/README.md +++ b/plugins/firehydrant/README.md @@ -4,7 +4,7 @@ The [FireHydrant](https://firehydrant.io) plugin brings incident management to Backstage, and it displays service incidents information such as active incidents and incident analytics. There are also quick action links that let you create and view incidents in FireHydrant. -firehydrant plugin screenshot +FireHydrant plugin screenshot ## Features diff --git a/plugins/firehydrant/api-report.md b/plugins/firehydrant/api-report.md index d12c2499c9..32b21b9906 100644 --- a/plugins/firehydrant/api-report.md +++ b/plugins/firehydrant/api-report.md @@ -8,9 +8,13 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; +// Warning: (ae-missing-release-tag) "FirehydrantPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const FirehydrantPage: () => JSX.Element; +// Warning: (ae-missing-release-tag) "firehydrantPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export const firehydrantPlugin: BackstagePlugin< { From cfe71d2d877acbf08a3f473195cfccb1492067d3 Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Thu, 5 Aug 2021 18:51:18 -0400 Subject: [PATCH 06/46] fetch firehydrant service by unique name Signed-off-by: Christine Yi --- .../components/ServiceDetailsCard/ServiceDetailsCard.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx index 7ebbcd8351..dba81fa88f 100644 --- a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx @@ -156,8 +156,14 @@ export const ServiceDetailsCard = () => { const startDate = moment().subtract(30, 'days').utc(); const endDate = moment().utc(); + // The Backstage service name in FireHydrant is a unique formatted string + // that requires the entity's kind, name, and namespace. + const fireHydrantServiceName = `${entity?.kind}:${ + entity?.metadata?.namespace ?? 'default' + }/${entity?.metadata?.name}`; + const { loading, value, error } = useServiceDetails({ - serviceName: entity?.metadata?.name, + serviceName: fireHydrantServiceName, }); const activeIncidents: string[] = value?.service?.active_incidents ?? []; From efabbe3f02f17378ba85560969b2d0fcb466e945 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 9 Aug 2021 13:37:11 +0200 Subject: [PATCH 07/46] Adjust workflows to ignore errors & autoformat ADOPTERS.md using prettier Signed-off-by: Philipp Hugenroth --- .github/workflows/ci.yml | 2 +- .github/workflows/prettify.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f013d10f8..8361654038 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,7 +109,7 @@ jobs: run: node scripts/verify-links.js - name: prettier - run: yarn prettier:check + run: yarn prettier:check '!ADOPTERS.md' - name: lock run: yarn lock:check diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml index 36d3275f66..fbf2519513 100644 --- a/.github/workflows/prettify.yml +++ b/.github/workflows/prettify.yml @@ -46,8 +46,8 @@ jobs: - name: Run Prettier on docs uses: creyD/prettier_action@v3.1 with: - # Modifies commit only if prettier autofixed a Markdown file - prettier_options: --config docs/prettier.config.js --write docs/**/*.md + # Modifies commit only if prettier autofixed the ADOPTERS.md + prettier_options: --config docs/prettier.config.js --write ADOPTERS.md only_changed: true commit_message: 'Autofix documentation Markdown files using Prettier' env: From 7e16ee33de7f55ef99010ffcc42668ea8a196439 Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Mon, 9 Aug 2021 10:21:00 -0400 Subject: [PATCH 08/46] bump dependencies Signed-off-by: Christine Yi --- plugins/firehydrant/package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index f4d2a3a8a3..08773dc972 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -20,11 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-plugin-api": "^0.1.3", - "@backstage/core-components": "^0.1.5", + "@backstage/core-plugin-api": "^0.1.4", + "@backstage/core-components": "^0.2.0", "@backstage/plugin-catalog-react": "^0.2.6", - "@backstage/theme": "^0.2.8", - "@material-ui/core": "^4.11.0", + "@backstage/theme": "^0.2.9", + "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "moment": "^2.29.1", @@ -33,17 +33,17 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.3", - "@backstage/core-app-api": "^0.1.4", - "@backstage/dev-utils": "^0.2.1", - "@backstage/test-utils": "^0.1.14", + "@backstage/cli": "^0.7.6", + "@backstage/core-app-api": "^0.1.6", + "@backstage/dev-utils": "^0.2.4", + "@backstage/test-utils": "^0.1.16", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", - "msw": "^0.21.2" + "msw": "^0.29.0" }, "files": [ "dist" From 055f8791b5c1311a3a948d00a4ca82cfb3d6f899 Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Mon, 9 Aug 2021 16:06:35 -0400 Subject: [PATCH 09/46] bump dependencies Signed-off-by: Christine Yi --- plugins/firehydrant/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 08773dc972..f2f2dfe84b 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-plugin-api": "^0.1.4", - "@backstage/core-components": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.2.6", + "@backstage/core-components": "^0.3.0", + "@backstage/core-plugin-api": "^0.1.5", + "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -33,9 +33,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.6", - "@backstage/core-app-api": "^0.1.6", - "@backstage/dev-utils": "^0.2.4", + "@backstage/cli": "^0.7.7", + "@backstage/core-app-api": "^0.1.7", + "@backstage/dev-utils": "^0.2.5", "@backstage/test-utils": "^0.1.16", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", From 84422ec82d16c98dc35c947a793a54bfb2df8044 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Aug 2021 04:11:54 +0000 Subject: [PATCH 10/46] chore(deps-dev): bump @types/react-lazylog from 4.5.0 to 4.5.1 Bumps [@types/react-lazylog](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-lazylog) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-lazylog) --- updated-dependencies: - dependency-name: "@types/react-lazylog" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 49a7a58059..ba4e4585e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7198,9 +7198,9 @@ "@types/react" "*" "@types/react-lazylog@^4.5.0": - version "4.5.0" - resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.0.tgz#1677ac520f29ada2e95b8cd5f8ec54b897227278" - integrity sha512-yg0IhKtDRGqUUtXJwi9Y/sSmgX/gmVP0vu0cr5uAECmJnRQbAgsGoVUntQGtwH0Z7Y2iiTh+QW5LkOFsmtIisg== + version "4.5.1" + resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.1.tgz#babb5d814f7035b5434518769975e12f299356a8" + integrity sha512-g4yeosa1zYhu2BUJmuu2H2o0dsdRj0o8Omw3pBiVHdLHJaeYIyArvyMRR3bI/MxZxG4EaiRl8AOQ6zeM8P46jA== dependencies: "@types/react" "*" immutable ">=3.8.2" From 40bb7e3b24274f53eae4e36c0b805c3781662b2a Mon Sep 17 00:00:00 2001 From: Christine Yi Date: Tue, 10 Aug 2021 15:25:42 -0400 Subject: [PATCH 11/46] address PR feedback: use luxon, remove unused route, update styling Signed-off-by: Christine Yi --- plugins/firehydrant/README.md | 23 ++-- plugins/firehydrant/api-report.md | 4 +- plugins/firehydrant/package.json | 3 +- plugins/firehydrant/src/api/index.ts | 23 ++-- .../ServiceAnalytics/ServiceAnalytics.tsx | 30 ++--- .../ServiceDetailsCard/ServiceDetailsCard.tsx | 116 ++++++++++-------- plugins/firehydrant/src/index.ts | 2 +- plugins/firehydrant/src/plugin.ts | 15 ++- 8 files changed, 117 insertions(+), 99 deletions(-) diff --git a/plugins/firehydrant/README.md b/plugins/firehydrant/README.md index 572e873c83..aaab8b7d0e 100644 --- a/plugins/firehydrant/README.md +++ b/plugins/firehydrant/README.md @@ -26,11 +26,11 @@ yarn add @backstage/plugin-firehydrant ```ts // In packages/app/src/components/catalog/EntityPage.tsx -import { FirehydrantPage } from '@backstage/plugin-firehydrant'; +import { FirehydrantCard } from '@backstage/plugin-firehydrant'; // Add to code as a grid item - + ; ``` @@ -38,19 +38,12 @@ import { FirehydrantPage } from '@backstage/plugin-firehydrant'; ```yaml proxy: -'/firehydrant/api': - target: 'https://api.firehydrant.io/v1/' - changeOrigin: true - headers: - # Supply the token you generated from https://app.firehydrant.io/organizations/bots - Authorization: Bearer ${FIREHYDRANT_BOT_TOKEN} -``` - -as well as the default url configuration to `app-config.yaml`: - -```yaml -firehydrant: - baseUrl: https://api.firehydrant.io + '/firehydrant/api': + target: 'https://api.firehydrant.io/v1/' + changeOrigin: true + headers: + # Supply the token you generated from https://app.firehydrant.io/organizations/bots + Authorization: Bearer ${FIREHYDRANT_BOT_TOKEN} ``` Note: if you are not using environment variables, you can directly type the API Bot Token into `app-config.yaml`: diff --git a/plugins/firehydrant/api-report.md b/plugins/firehydrant/api-report.md index 32b21b9906..6cd8270178 100644 --- a/plugins/firehydrant/api-report.md +++ b/plugins/firehydrant/api-report.md @@ -8,10 +8,10 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; -// Warning: (ae-missing-release-tag) "FirehydrantPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "FirehydrantCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const FirehydrantPage: () => JSX.Element; +export const FirehydrantCard: () => JSX.Element; // Warning: (ae-missing-release-tag) "firehydrantPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index f2f2dfe84b..a5531ac0bc 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -4,6 +4,7 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", + "private": false, "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -27,7 +28,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "moment": "^2.29.1", + "luxon": "^1.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^17.2.4" diff --git a/plugins/firehydrant/src/api/index.ts b/plugins/firehydrant/src/api/index.ts index e5e664aed8..b78e81a024 100644 --- a/plugins/firehydrant/src/api/index.ts +++ b/plugins/firehydrant/src/api/index.ts @@ -97,19 +97,18 @@ export class FireHydrantAPIClient implements FireHydrantAPI { incidents: [] as Incident[], }; - if (response.ok) { - if (json.data?.length === 0) { - return servicesData; - } - - servicesData.service = json.data[0]; - - const incidentsJson = await this.getServiceIncidents({ - serviceId: json.data[0].id, - }); - - servicesData.incidents = incidentsJson; + if (json.data?.length === 0) { + return servicesData; } + + servicesData.service = json.data[0]; + + const incidentsJson = await this.getServiceIncidents({ + serviceId: json.data[0].id, + }); + + servicesData.incidents = incidentsJson; + return servicesData; } diff --git a/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx index 996d705a9a..e5818ef484 100644 --- a/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx +++ b/plugins/firehydrant/src/components/ServiceAnalytics/ServiceAnalytics.tsx @@ -15,17 +15,18 @@ */ import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import moment, { Moment } from 'moment'; -import Alert from '@material-ui/lab/Alert'; -import { Table, TableColumn, Progress } from '@backstage/core-components'; +import { DateTime } from 'luxon'; +import { + ResponseErrorPanel, + Table, + TableColumn, + Progress, +} from '@backstage/core-components'; const useStyles = makeStyles({ container: { overflow: 'auto', width: '100%', - '& h5': { - fontSize: '18px !important', - }, '& td': { minWidth: '145px', }, @@ -41,8 +42,8 @@ export const DenseTable = ({ endDate, }: { service: object; - startDate: Moment; - endDate: Moment; + startDate: DateTime; + endDate: DateTime; }) => { const classes = useStyles(); @@ -60,7 +61,9 @@ export const DenseTable = ({
; } else if (error) { - return {error.message}; + return ; } - - const startDate = moment().subtract(30, 'days').utc(); - const endDate = moment().utc(); + const startDate = DateTime.now().minus({ days: 30 }).toUTC(); + const endDate = DateTime.now().toUTC(); // Transform and format the data to display in the table if (value.id) { @@ -140,7 +142,7 @@ export const ServiceAnalytics = ({ ? calcHealthiness({ mttm: value.mttm, incidents: value.count, - range: endDate.diff(startDate, 'seconds'), + range: endDate.diff(startDate, 'seconds').as('seconds'), }) : '100%', impacted: value.total_time ? secondsToDhms(value.total_time) : '-', diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx index dba81fa88f..b70c578e11 100644 --- a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { useEffect, useState } from 'react'; -import moment from 'moment'; +import { DateTime } from 'luxon'; import { ServiceAnalytics } from '../ServiceAnalytics/ServiceAnalytics'; import { Box, @@ -32,20 +32,22 @@ import { Incident } from '../types'; import { ServiceIncidentsResponse } from '../../api/types'; import { useServiceDetails } from '../serviceDetails'; import { useServiceAnalytics } from '../serviceAnalytics'; -import { InfoCard, Progress } from '@backstage/core-components'; +import { + InfoCard, + Link, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; const useStyles = makeStyles(theme => ({ - infoCard: { - height: '342px', - }, button: { color: '#3b2492', display: 'grid', gridGap: '4px', textAlign: 'center', justifyItems: 'center', - width: '102px', + width: '105px', backgroundColor: theme.palette.type === 'dark' ? '#f1edff' : '', '&:hover, &:focus': { backgroundColor: '#f1edff', @@ -59,7 +61,8 @@ const useStyles = makeStyles(theme => ({ }, border: '1px solid #3b2492', borderRadius: '5px', - padding: '10px', + padding: '8px 10px', + textTransform: 'none', }, buttonLink: { backgroundColor: '#3b2492', @@ -101,28 +104,18 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', padding: '10px', background: '#f1edff', - color: theme.palette.type === 'dark' ? '#3b2492' : '#fff', + color: '#3b2492', }, })); -const ServiceWarning = ({ text }: { text: string }) => { - const classes = useStyles(); - return ( -
- -   {text} -
- ); -}; - const ServiceAnalyticsView = ({ serviceId, startDate, endDate, }: { serviceId: string; - startDate: moment.Moment; - endDate: moment.Moment; + startDate: DateTime; + endDate: DateTime; }) => { const { loading: analyticsLoading, @@ -130,8 +123,8 @@ const ServiceAnalyticsView = ({ error: analyticsError, } = useServiceAnalytics({ serviceId, - startDate: startDate.format('YYYY-MM-DD'), - endDate: endDate.format('YYYY-MM-DD'), + startDate: startDate.toFormat('YYYY-MM-DD'), + endDate: endDate.toFormat('YYYY-MM-DD'), }); return ( @@ -153,8 +146,8 @@ export const ServiceDetailsCard = () => { configApi.getOptionalString('firehydrant.baseUrl') || 'https://app.firehydrant.io'; - const startDate = moment().subtract(30, 'days').utc(); - const endDate = moment().utc(); + const startDate = DateTime.now().minus({ days: 30 }).toUTC(); + const endDate = DateTime.now().toUTC(); // The Backstage service name in FireHydrant is a unique formatted string // that requires the entity's kind, name, and namespace. @@ -181,9 +174,7 @@ export const ServiceDetailsCard = () => { } if (error) { - return ( - - ); + return ; } const headerText: string = showServiceDetails @@ -197,9 +188,12 @@ export const ServiceDetailsCard = () => { )},"value":${JSON.stringify(value?.service?.id)}}]}`; return ( - + {!showServiceDetails && !loading && ( - +
+ +   This service does not exist in FireHydrant. +
)} {showServiceDetails && ( { className={classes.buttonLink} color="default" href={serviceIncidentsLink} + startIcon={} target="_blank" variant="outlined" > - View service incidents   - + View service incidents @@ -230,9 +224,14 @@ export const ServiceDetailsCard = () => { {incidents && incidents?.slice(0, 5).map((incident: Incident, index: number) => ( ))} @@ -240,34 +239,55 @@ export const ServiceDetailsCard = () => { View in FireHydrant - - - Declare an incident - - + + + + + Declare an incident + + + + - - View all incidents - + + + + + + View all incidents + + + {showServiceDetails && ( - - - View Service Details - + + + + + + View Service Details + + + )} diff --git a/plugins/firehydrant/src/index.ts b/plugins/firehydrant/src/index.ts index 3b9b584fb8..465e42f1ae 100644 --- a/plugins/firehydrant/src/index.ts +++ b/plugins/firehydrant/src/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { firehydrantPlugin, FirehydrantPage } from './plugin'; +export { firehydrantPlugin, FirehydrantCard } from './plugin'; diff --git a/plugins/firehydrant/src/plugin.ts b/plugins/firehydrant/src/plugin.ts index e4794ab4ce..4277c1833f 100644 --- a/plugins/firehydrant/src/plugin.ts +++ b/plugins/firehydrant/src/plugin.ts @@ -18,7 +18,7 @@ import { createApiFactory, createPlugin, discoveryApiRef, - createRoutableExtension, + createComponentExtension, } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; @@ -37,10 +37,13 @@ export const firehydrantPlugin = createPlugin({ }, }); -export const FirehydrantPage = firehydrantPlugin.provide( - createRoutableExtension({ - component: () => - import('./components/ServiceDetailsCard').then(m => m.ServiceDetailsCard), - mountPoint: rootRouteRef, +export const FirehydrantCard = firehydrantPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/ServiceDetailsCard').then( + m => m.ServiceDetailsCard, + ), + }, }), ); From 9b41ea27b628476e21fa0db861e3f93711f43023 Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Mon, 19 Apr 2021 13:18:18 +1200 Subject: [PATCH 12/46] Add repourlpicker and publisher for bitbucket cloud The bitbucket cloud api needs project key to create repo under a project. Signed-off-by: Mustansar Anwar ul Samad --- packages/integration/config.d.ts | 38 +++ packages/integration/src/bitbucket/config.ts | 31 +++ .../bitbucket-cloud-demo/template.yaml | 75 ++++++ .../template/catalog-info.yaml | 8 + .../sample-templates/local-templates.yaml | 14 + .../actions/builtin/createBuiltinActions.ts | 4 + .../builtin/publish/bitbucketCloud.test.ts | 187 ++++++++++++++ .../actions/builtin/publish/bitbucketCloud.ts | 198 +++++++++++++++ .../actions/builtin/publish/index.ts | 1 + .../actions/builtin/publish/util.ts | 39 +++ plugins/scaffolder/src/api.ts | 21 ++ .../ActionsPage/ActionsPage.test.tsx | 1 + .../TemplatePage/TemplatePage.test.tsx | 1 + .../RepoUrlPickerBitbucketCloud.tsx | 239 ++++++++++++++++++ 14 files changed, 857 insertions(+) create mode 100644 plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml create mode 100644 plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template/catalog-info.yaml create mode 100644 plugins/scaffolder-backend/sample-templates/local-templates.yaml create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts create mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerBitbucketCloud.tsx diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 501373de53..46e560a7c6 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -58,6 +58,44 @@ export interface Config { * @visibility secret */ appPassword?: string; + /** + * Bitbucket workspaces + * @visibility frontend + */ + workspaces?: Array<{ + /** + * The name of workspace + * @visibility frontend + */ + name: string; + /** + * The description for a workspace + * @visibility frontend + */ + description?: string; + /** + * Bitbucket projects in the the workspace + * @visibility frontend + */ + projects?: Array<{ + /** + * The name of the project in bitbucket + * @visibility frontend + */ + name: string; + /** + * The key for the project in bitbucket + * @visibility frontend + */ + key: string; + /** + * The description for the project in bitbucket + * @visibility frontend + */ + description?: string; + }>; + }>; + }>; /** Integration configuration for GitHub */ diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index b2ddfa3532..6706c86afc 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -60,8 +60,29 @@ export type BitbucketIntegrationConfig = { * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ */ appPassword?: string; + + /** + * Bitbucket workspaces, can be organization or user + */ + workspaces?: BitbucketWorkspace[]; + }; +export type BitbucketWorkspace = { + name: string; + description?: string; + /** + * Bitbucket projects in the organization to add repos to + */ + projects?: BitbucketProject[]; +} + +export type BitbucketProject = { + name: string; + key: string; + description?: string; +} + /** * Reads a single Bitbucket integration config. * @@ -75,6 +96,15 @@ export function readBitbucketIntegrationConfig( const token = config.getOptionalString('token'); const username = config.getOptionalString('username'); const appPassword = config.getOptionalString('appPassword'); + const workspaces = config.getOptionalConfigArray('workspaces')?.map(w => ({ + name: w.getString('name'), + description: w.getOptionalString('description'), + projects : w.getOptionalConfigArray('projects')?.map(p => ({ + name: p.getString('name'), + key: p.getString('key'), + description: p.getOptionalString('description'), + })), + })); if (!isValidHost(host)) { throw new Error( @@ -94,6 +124,7 @@ export function readBitbucketIntegrationConfig( token, username, appPassword, + workspaces }; } diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml new file mode 100644 index 0000000000..dc97ba9c6c --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml @@ -0,0 +1,75 @@ +apiVersion: backstage.io/v1beta2 +kind: Template +metadata: + name: bitbucket-cloud + title: Test Bitbucket Cloud RepoUrlPicker template + description: scaffolder v1beta2 template demo publishing to bitbucket cloud +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPickerBitbucketCloud + - title: Fill in some steps + required: + - name + - owner + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + allowedKinds: + - Group + + steps: + - id: fetch-base + name: Fetch Base + action: fetch:cookiecutter + input: + url: ./template + values: + name: '{{ parameters.name }}' + owner: '{{ parameters.owner }}' + + - id: fetch-docs + name: Fetch Docs + action: fetch:plain + input: + targetPath: ./community + url: https://github.com/backstage/community/tree/main/backstage-community-sessions + + - id: publish + name: Publish + action: publish:bitbucketcloud + input: + allowedHosts: ['bitbucket.org'] + description: 'This is {{ parameters.name }}' + repoUrl: '{{ parameters.repoUrl }}' + + - id: register + name: Register + action: catalog:register + input: + repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + catalogInfoPath: '/catalog-info.yaml' + + output: + remoteUrl: '{{ steps.publish.output.remoteUrl }}' + entityRef: '{{ steps.register.output.entityRef }}' diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template/catalog-info.yaml new file mode 100644 index 0000000000..875664d2a8 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template/catalog-info.yaml @@ -0,0 +1,8 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: {{cookiecutter.name | jsonify}} +spec: + type: website + lifecycle: experimental + owner: {{cookiecutter.owner | jsonify}} diff --git a/plugins/scaffolder-backend/sample-templates/local-templates.yaml b/plugins/scaffolder-backend/sample-templates/local-templates.yaml new file mode 100644 index 0000000000..58c7419716 --- /dev/null +++ b/plugins/scaffolder-backend/sample-templates/local-templates.yaml @@ -0,0 +1,14 @@ +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: example-templates-local + description: A collection of locally available Backstage example templates +spec: + targets: + - ./create-react-app/template.yaml + - ./docs-template/template.yaml + - ./react-ssr-template/template.yaml + - ./springboot-grpc-template/template.yaml + - ./v1beta2-demo/template.yaml + - ./pull-request/template.yaml + - ./bitbucket-cloud-demo/template.yaml diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 5a16ef9a55..eecda4e11f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -33,6 +33,7 @@ import { import { createPublishAzureAction, createPublishBitbucketAction, + createPublishBitbucketCloudAction, createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, @@ -78,6 +79,9 @@ export const createBuiltinActions = (options: { integrations, config, }), + createPublishBitbucketCloudAction({ + integrations, + }), createPublishAzureAction({ integrations, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts new file mode 100644 index 0000000000..dea10e6ae1 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts @@ -0,0 +1,187 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +jest.mock('../../../stages/publish/helpers'); + +import { createPublishBitbucketCloudAction } from './bitbucketCloud'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw } from '@backstage/test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../../../stages/publish/helpers'; + +describe('publish:bitbucketcloud', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + token: 'tokenlols', + }, + ], + }, + }), + ); + const action = createPublishBitbucketCloudAction({ integrations }); + const mockContext = { + input: { + repoUrl: 'bitbucket.org?repo=repo&workspace=workspace&project=project', + repoVisibility: 'private', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + const server = setupServer(); + msw.setupDefaultHandlers(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'bitbucket.org?repo=bob&workspace=workspace1' }, + }), + ).rejects.toThrow(/missing project/); + + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'bitbucket.org?repo=bob&project=project1' }, + }), + ).rejects.toThrow(/missing workspace/); + + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'bitbucket.org?workspace=workspace1&project=project1' }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should call the correct APIs when the host is bitbucket cloud', async () => { + expect.assertions(2); + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer tokenlols'); + expect(req.body).toEqual({ is_private: true, scm: 'git', project: { key: 'project' } }); + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/repo', + }, + ], + }, + }), + ); + }, + ), + ); + + await action.handler(mockContext); + }); + + it('should call initAndPush with the correct values', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + auth: { username: 'x-token-auth', password: 'tokenlols' }, + logger: mockContext.logger, + }); + }); + + it('should call outputs with the correct urls', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler(mockContext); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://bitbucket.org/workspace/cloneurl', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://bitbucket.org/workspace/repo/src/master', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts new file mode 100644 index 0000000000..db5aa3aff2 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { + BitbucketIntegrationConfig, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { initRepoAndPush } from '../../../stages/publish/helpers'; +import { getRepoSourceDirectory, parseRepoUrlForBitbucket } from './util'; +import fetch from 'cross-fetch'; +import { createTemplateAction } from '../../createTemplateAction'; + +const createBitbucketCloudRepository = async (opts: { + workspace: string; + project: string; + repo: string; + description: string; + repoVisibility: 'private' | 'public'; + authorization: string; +}) => { + const { workspace, project, repo, description, repoVisibility, authorization } = opts; + + const options: RequestInit = { + method: 'POST', + body: JSON.stringify({ + scm: 'git', + description: description, + is_private: repoVisibility === 'private', + project: { key: project }, + }), + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + let response: Response; + try { + response = await fetch( + `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, + options, + ); + } catch (e) { + throw new Error(`Unable to create repository, ${e}`); + } + + if (response.status !== 200) { + throw new Error( + `Unable to create repository, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } + + const r = await response.json(); + let remoteUrl = ''; + for (const link of r.links.clone) { + if (link.name === 'https') { + remoteUrl = link.href; + } + } + + // TODO use the urlReader to get the default branch + const repoContentsUrl = `${r.links.html.href}/src/master`; + return { remoteUrl, repoContentsUrl }; +}; + +const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => { + if (config.username && config.appPassword) { + const buffer = Buffer.from( + `${config.username}:${config.appPassword}`, + 'utf8', + ); + + return `Basic ${buffer.toString('base64')}`; + } + + if (config.token) { + return `Bearer ${config.token}`; + } + + throw new Error( + `Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`, + ); +}; + +export function createPublishBitbucketCloudAction(options: { + integrations: ScmIntegrationRegistry; +}) { + const { integrations } = options; + + return createTemplateAction<{ + repoUrl: string; + description: string; + repoVisibility: 'private' | 'public'; + sourcePath?: string; + }>({ + id: 'publish:bitbucketcloud', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + description: { + title: 'Repository Description', + type: 'string', + }, + repoVisibility: { + title: 'Repository Visiblity', + type: 'string', + enum: ['private', 'public'], + }, + sourcePath: { + title: + 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the respository.', + type: 'string', + }, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: { + title: 'A URL to the repository with the provider', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the root of the repository', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { repoUrl, description, repoVisibility = 'private' } = ctx.input; + + const { workspace, project, repo, host } = parseRepoUrlForBitbucket(repoUrl); + + const integrationConfig = integrations.bitbucket.byHost(host); + + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const authorization = getAuthorizationHeader(integrationConfig.config); + + const createMethod = createBitbucketCloudRepository; + + const { remoteUrl, repoContentsUrl } = await createMethod({ + authorization, + workspace, + project, + repo, + repoVisibility, + description, + }); + + await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + remoteUrl, + auth: { + username: integrationConfig.config.username + ? integrationConfig.config.username + : 'x-token-auth', + password: integrationConfig.config.appPassword + ? integrationConfig.config.appPassword + : integrationConfig.config.token ?? '', + }, + logger: ctx.logger, + }); + + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index a292438d3a..bdc1577ee5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -16,6 +16,7 @@ export { createPublishAzureAction } from './azure'; export { createPublishBitbucketAction } from './bitbucket'; +export { createPublishBitbucketCloudAction } from './bitbucketCloud'; export { createPublishFileAction } from './file'; export { createPublishGithubAction } from './github'; export { createPublishGithubPullRequestAction } from './githubPullRequest'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index e7d4e75f6a..1d74ab8328 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -65,3 +65,42 @@ export const parseRepoUrl = (repoUrl: string): RepoSpec => { return { host, owner, repo, organization }; }; + +export const parseRepoUrlForBitbucket = (repoUrl: string) => { + let parsed; + try { + parsed = new URL(`https://${repoUrl}`); + } catch (error) { + throw new InputError( + `Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`, + ); + } + const host = parsed.host; + + const workspace = parsed.searchParams.get('workspace'); + + if (!workspace) { + throw new InputError( + `Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`, + ); + } + + const project = parsed.searchParams.get('project'); + if (!project) { + throw new InputError( + `Invalid repo URL passed to publisher: ${repoUrl}, missing project`, + ); + } + + const repo = parsed.searchParams.get('repo'); + if (!repo) { + throw new InputError( + `Invalid repo URL passed to publisher: ${repoUrl}, missing repo`, + ); + } + + const organization = parsed.searchParams.get('organization'); + + return { host, workspace, project, repo, organization }; + +}; diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 37d78d0fda..47a1d8cc62 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -79,6 +79,10 @@ export interface ScaffolderApi { allowedHosts: string[]; }): Promise<{ type: string; title: string; host: string }[]>; + getIntegration(options: { + type: string; + }): Promise; + // Returns a list of all installed actions. listActions(): Promise; @@ -117,6 +121,23 @@ export class ScaffolderClient implements ScaffolderApi { .filter(c => options.allowedHosts.includes(c.host)); } + async getIntegration(options: { type: string }) { + switch (options.type) { + case 'azure': + return this.scmIntegrationsApi.azure.list() + case 'bitbucket': + return this.scmIntegrationsApi.bitbucket.list() + case 'github': + return this.scmIntegrationsApi.github.list() + case 'gitlab': + return this.scmIntegrationsApi.gitlab.list() + default: + throw new Error( + `No integration found for ${options.type}`, + ); + } + } + async getTemplateParameterSchema( templateName: EntityName, ): Promise { diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index 77c8240974..93bd5d3fd3 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -24,6 +24,7 @@ const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), getTemplateParameterSchema: jest.fn(), getIntegrationsList: jest.fn(), + getIntegration: jest.fn(), getTask: jest.fn(), streamLogs: jest.fn(), listActions: jest.fn(), diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index e42bcbdbdc..ef2acd6de3 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -40,6 +40,7 @@ const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), getTemplateParameterSchema: jest.fn(), getIntegrationsList: jest.fn(), + getIntegration: jest.fn(), getTask: jest.fn(), streamLogs: jest.fn(), listActions: jest.fn(), diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerBitbucketCloud.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerBitbucketCloud.tsx new file mode 100644 index 0000000000..51172c07c1 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerBitbucketCloud.tsx @@ -0,0 +1,239 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useCallback, useEffect } from 'react'; +import { Field } from '@rjsf/core'; +import { useApi, Progress } from '@backstage/core'; +import { scaffolderApiRef } from '../../../api'; +import { useAsync } from 'react-use'; +import Select from '@material-ui/core/Select'; +import InputLabel from '@material-ui/core/InputLabel'; +import Input from '@material-ui/core/Input'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import { BitbucketIntegration } from '@backstage/integration'; + +function splitFormData(url: string | undefined) { + let host = undefined; + let workspace = undefined; + let project = undefined; + let repo = undefined; + + try { + if (url) { + const parsed = new URL(`https://${url}`); + host = parsed.host; + workspace = parsed.searchParams.get('workspace') || undefined; + project = parsed.searchParams.get('project') || undefined; + repo = parsed.searchParams.get('repo') || undefined; + } + } catch { + /* ok */ + } + + return { host, workspace, project, repo }; +} + +function serializeFormData(data: { + host?: string; + workspace?: string; + project?: string; + repo?: string; +}) { + if (!data.host) { + return undefined; + } + const params = new URLSearchParams(); + if (data.workspace) { + params.set('workspace', data.workspace); + } + if (data.project) { + params.set('project', data.project); + } + if (data.repo) { + params.set('repo', data.repo); + } + + return `${data.host}?${params.toString()}`; +} + +export const RepoUrlPickerBitbucketCloud: Field = ({ + onChange, + rawErrors, + formData, +}) => { + + const api = useApi(scaffolderApiRef); + const allowedHosts: string[] = ['bitbucket.org']; + + const { value: integrations, loading: loadingIntegrations } = useAsync(async () => { + return await api.getIntegrationsList({ allowedHosts }); + }); + + const { value: data, loading: loadingData } = useAsync(async () => { + const bitbucketIntegrations: BitbucketIntegration[] = await api.getIntegration({ type: 'bitbucket' }) as BitbucketIntegration[]; + const bitbucketIntegration = bitbucketIntegrations.filter(element => element.config.host === 'bitbucket.org')[0]; + const workspaces = bitbucketIntegration.config.workspaces; + const projects = workspaces?.[0]?.projects; + + return { workspaces, projects }; + }); + + const { host, workspace, project, repo } = splitFormData(formData); + const updateHost = useCallback( + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => + onChange( + serializeFormData({ + host: evt.target.value as string, + workspace, + project, + repo, + }), + ), + [onChange, workspace, project, repo], + ); + + const updateWorkspace = useCallback( + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { + if (data) { + data.projects = data?.workspaces?.find(w => w.name === evt.target.value)?.projects; + } + onChange( + serializeFormData({ + host, + workspace: evt.target.value as string, + project, + repo, + }), + ); + }, + [onChange, data, host, project, repo], + ); + + const updateProject = useCallback( + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { + onChange( + serializeFormData({ + host, + workspace, + project: evt.target.value as string, + repo, + }), + ); + }, + [onChange, host, workspace, repo], + ); + + const updateRepo = useCallback( + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => + onChange( + serializeFormData({ + host, + workspace, + project, + repo: evt.target.value as string, + }), + ), + [onChange, host, workspace, project], + ); + + useEffect(() => { + onChange( + serializeFormData({ + host: (host === undefined ? (integrations?.[0]?.host) : undefined), + workspace: workspace === undefined ? (data?.workspaces?.[0]?.name) : undefined, + project: project === undefined ? (data?.workspaces?.[0]?.projects?.[0]?.key) : undefined, + repo, + }), + ); + }, [onChange, integrations, data, host, workspace, project, repo]); + + if (loadingIntegrations || loadingData) { + return ; + } + + return ( + <> + 0 && !host} + > + Host + + + The host where the repository will be created + + + 0 && !workspace} + > + Workspace + + + The workspace where the repository will be created + + + 0 && !project} + > + Project + + The bitbucket project this repo should be added to + + 0 && !repo} + > + Repository + + The name of the repository + + + ); +}; From b0a622490477626046f55a8b28c719511acd32e3 Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Fri, 30 Apr 2021 08:42:40 +1200 Subject: [PATCH 13/46] Use default RepoUrlPicker for bitbucket fields Also updated the default bitbucket publisher to handle workspace and project fields Signed-off-by: Mustansar Anwar ul Samad --- packages/integration/config.d.ts | 38 --- packages/integration/src/bitbucket/config.ts | 31 --- .../template.yaml | 15 +- .../template/catalog-info.yaml | 0 .../sample-templates/local-templates.yaml | 2 +- .../actions/builtin/createBuiltinActions.ts | 4 - .../actions/builtin/publish/bitbucket.test.ts | 47 ++-- .../actions/builtin/publish/bitbucket.ts | 39 ++- .../builtin/publish/bitbucketCloud.test.ts | 187 -------------- .../actions/builtin/publish/bitbucketCloud.ts | 198 --------------- .../actions/builtin/publish/index.ts | 1 - .../actions/builtin/publish/util.ts | 36 +-- plugins/scaffolder/src/api.ts | 21 -- .../ActionsPage/ActionsPage.test.tsx | 1 - .../TemplatePage/TemplatePage.test.tsx | 1 - .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 144 +++++++++-- .../RepoUrlPickerBitbucketCloud.tsx | 239 ------------------ 17 files changed, 209 insertions(+), 795 deletions(-) rename plugins/scaffolder-backend/sample-templates/{bitbucket-cloud-demo => bitbucket-demo}/template.yaml (86%) rename plugins/scaffolder-backend/sample-templates/{bitbucket-cloud-demo => bitbucket-demo}/template/catalog-info.yaml (100%) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts delete mode 100644 plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerBitbucketCloud.tsx diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 46e560a7c6..501373de53 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -58,44 +58,6 @@ export interface Config { * @visibility secret */ appPassword?: string; - /** - * Bitbucket workspaces - * @visibility frontend - */ - workspaces?: Array<{ - /** - * The name of workspace - * @visibility frontend - */ - name: string; - /** - * The description for a workspace - * @visibility frontend - */ - description?: string; - /** - * Bitbucket projects in the the workspace - * @visibility frontend - */ - projects?: Array<{ - /** - * The name of the project in bitbucket - * @visibility frontend - */ - name: string; - /** - * The key for the project in bitbucket - * @visibility frontend - */ - key: string; - /** - * The description for the project in bitbucket - * @visibility frontend - */ - description?: string; - }>; - }>; - }>; /** Integration configuration for GitHub */ diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index 6706c86afc..b2ddfa3532 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -60,29 +60,8 @@ export type BitbucketIntegrationConfig = { * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ */ appPassword?: string; - - /** - * Bitbucket workspaces, can be organization or user - */ - workspaces?: BitbucketWorkspace[]; - }; -export type BitbucketWorkspace = { - name: string; - description?: string; - /** - * Bitbucket projects in the organization to add repos to - */ - projects?: BitbucketProject[]; -} - -export type BitbucketProject = { - name: string; - key: string; - description?: string; -} - /** * Reads a single Bitbucket integration config. * @@ -96,15 +75,6 @@ export function readBitbucketIntegrationConfig( const token = config.getOptionalString('token'); const username = config.getOptionalString('username'); const appPassword = config.getOptionalString('appPassword'); - const workspaces = config.getOptionalConfigArray('workspaces')?.map(w => ({ - name: w.getString('name'), - description: w.getOptionalString('description'), - projects : w.getOptionalConfigArray('projects')?.map(p => ({ - name: p.getString('name'), - key: p.getString('key'), - description: p.getOptionalString('description'), - })), - })); if (!isValidHost(host)) { throw new Error( @@ -124,7 +94,6 @@ export function readBitbucketIntegrationConfig( token, username, appPassword, - workspaces }; } diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml similarity index 86% rename from plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml rename to plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml index dc97ba9c6c..fda1324e42 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml @@ -1,9 +1,9 @@ apiVersion: backstage.io/v1beta2 kind: Template metadata: - name: bitbucket-cloud - title: Test Bitbucket Cloud RepoUrlPicker template - description: scaffolder v1beta2 template demo publishing to bitbucket cloud + name: bitbucket-demo + title: Test Bitbucket RepoUrlPicker template + description: scaffolder v1beta2 template demo publishing to bitbucket spec: owner: backstage/techdocs-core type: service @@ -16,7 +16,12 @@ spec: repoUrl: title: Repository Location type: string - ui:field: RepoUrlPickerBitbucketCloud + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + - bitbucket.org + - server.bitbucket.com - title: Fill in some steps required: - name @@ -57,7 +62,7 @@ spec: - id: publish name: Publish - action: publish:bitbucketcloud + action: publish:bitbucket input: allowedHosts: ['bitbucket.org'] description: 'This is {{ parameters.name }}' diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml similarity index 100% rename from plugins/scaffolder-backend/sample-templates/bitbucket-cloud-demo/template/catalog-info.yaml rename to plugins/scaffolder-backend/sample-templates/bitbucket-demo/template/catalog-info.yaml diff --git a/plugins/scaffolder-backend/sample-templates/local-templates.yaml b/plugins/scaffolder-backend/sample-templates/local-templates.yaml index 58c7419716..5c13f0ab40 100644 --- a/plugins/scaffolder-backend/sample-templates/local-templates.yaml +++ b/plugins/scaffolder-backend/sample-templates/local-templates.yaml @@ -11,4 +11,4 @@ spec: - ./springboot-grpc-template/template.yaml - ./v1beta2-demo/template.yaml - ./pull-request/template.yaml - - ./bitbucket-cloud-demo/template.yaml + - ./bitbucket-demo/template.yaml diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index eecda4e11f..5a16ef9a55 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -33,7 +33,6 @@ import { import { createPublishAzureAction, createPublishBitbucketAction, - createPublishBitbucketCloudAction, createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, @@ -79,9 +78,6 @@ export const createBuiltinActions = (options: { integrations, config, }), - createPublishBitbucketCloudAction({ - integrations, - }), createPublishAzureAction({ integrations, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 180b6cc579..8efd644bbe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -50,7 +50,7 @@ describe('publish:bitbucket', () => { const action = createPublishBitbucketAction({ integrations, config }); const mockContext = { input: { - repoUrl: 'bitbucket.org?repo=repo&owner=owner', + repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&project=project&repo=repo', repoVisibility: 'private', }, workspacePath: 'lol', @@ -70,14 +70,21 @@ describe('publish:bitbucket', () => { await expect( action.handler({ ...mockContext, - input: { repoUrl: 'bitbucket.com?repo=bob' }, + input: { repoUrl: 'bitbucket.org?type=bitbucket&project=project&repo=repo' }, }), - ).rejects.toThrow(/missing owner/); + ).rejects.toThrow(/missing workspace/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'bitbucket.com?owner=owner' }, + input: { repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&repo=repo' }, + }), + ).rejects.toThrow(/missing project/); + + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&project=project' }, }), ).rejects.toThrow(/missing repo/); }); @@ -86,7 +93,7 @@ describe('publish:bitbucket', () => { await expect( action.handler({ ...mockContext, - input: { repoUrl: 'missing.com?repo=bob&owner=owner' }, + input: { repoUrl: 'missing.com?type=bitbucket&workspace=workspace&project=project&repo=repo' }, }), ).rejects.toThrow(/No matching integration configuration/); }); @@ -96,7 +103,7 @@ describe('publish:bitbucket', () => { action.handler({ ...mockContext, input: { - repoUrl: 'notoken.bitbucket.com?repo=bob&owner=owner', + repoUrl: 'notoken.bitbucket.com?type=bitbucket&workspace=workspace&project=project&repo=repo', }, }), ).rejects.toThrow(/Authorization has not been provided for Bitbucket/); @@ -106,22 +113,22 @@ describe('publish:bitbucket', () => { expect.assertions(2); server.use( rest.post( - 'https://api.bitbucket.org/2.0/repositories/owner/repo', + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Bearer tokenlols'); - expect(req.body).toEqual({ is_private: true, scm: 'git' }); + expect(req.body).toEqual({ is_private: true, scm: 'git', project: { key: 'project' } }); return res( ctx.status(200), ctx.set('Content-Type', 'application/json'), ctx.json({ links: { html: { - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, clone: [ { name: 'https', - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, ], }, @@ -138,7 +145,7 @@ describe('publish:bitbucket', () => { expect.assertions(2); server.use( rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos', + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Bearer thing'); expect(req.body).toEqual({ public: false, name: 'repo' }); @@ -169,7 +176,7 @@ describe('publish:bitbucket', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo', + repoUrl: 'hosted.bitbucket.com?type=bitbucket&project=project&repo=repo', }, }); }); @@ -259,7 +266,7 @@ describe('publish:bitbucket', () => { it('should call initAndPush with the correct values', async () => { server.use( rest.post( - 'https://api.bitbucket.org/2.0/repositories/owner/repo', + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (_, res, ctx) => res( ctx.status(200), @@ -267,12 +274,12 @@ describe('publish:bitbucket', () => { ctx.json({ links: { html: { - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, clone: [ { name: 'https', - href: 'https://bitbucket.org/owner/cloneurl', + href: 'https://bitbucket.org/workspace/cloneurl', }, ], }, @@ -475,7 +482,7 @@ describe('publish:bitbucket', () => { it('should call outputs with the correct urls', async () => { server.use( rest.post( - 'https://api.bitbucket.org/2.0/repositories/owner/repo', + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (_, res, ctx) => res( ctx.status(200), @@ -483,12 +490,12 @@ describe('publish:bitbucket', () => { ctx.json({ links: { html: { - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, clone: [ { name: 'https', - href: 'https://bitbucket.org/owner/cloneurl', + href: 'https://bitbucket.org/workspace/cloneurl', }, ], }, @@ -501,11 +508,11 @@ describe('publish:bitbucket', () => { expect(mockContext.output).toHaveBeenCalledWith( 'remoteUrl', - 'https://bitbucket.org/owner/cloneurl', + 'https://bitbucket.org/workspace/cloneurl', ); expect(mockContext.output).toHaveBeenCalledWith( 'repoContentsUrl', - 'https://bitbucket.org/owner/repo/src/master', + 'https://bitbucket.org/workspace/repo/src/master', ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 41d71c9ab5..092390d08e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -26,13 +26,14 @@ import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { Config } from '@backstage/config'; const createBitbucketCloudRepository = async (opts: { - owner: string; + workspace: string; + project: string; repo: string; description: string; repoVisibility: 'private' | 'public'; authorization: string; }) => { - const { owner, repo, description, repoVisibility, authorization } = opts; + const { workspace, project, repo, description, repoVisibility, authorization } = opts; const options: RequestInit = { method: 'POST', @@ -40,6 +41,7 @@ const createBitbucketCloudRepository = async (opts: { scm: 'git', description: description, is_private: repoVisibility === 'private', + project: { key: project }, }), headers: { Authorization: authorization, @@ -50,7 +52,7 @@ const createBitbucketCloudRepository = async (opts: { let response: Response; try { response = await fetch( - `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}`, + `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, options, ); } catch (e) { @@ -80,7 +82,7 @@ const createBitbucketCloudRepository = async (opts: { const createBitbucketServerRepository = async (opts: { host: string; - owner: string; + project: string; repo: string; description: string; repoVisibility: 'private' | 'public'; @@ -89,7 +91,7 @@ const createBitbucketServerRepository = async (opts: { }) => { const { host, - owner, + project, repo, description, authorization, @@ -258,7 +260,23 @@ export function createPublishBitbucketAction(options: { enableLFS = false, } = ctx.input; - const { owner, repo, host } = parseRepoUrl(repoUrl); + const { workspace, project, repo, host } = parseRepoUrl(repoUrl); + + // Workspace is only required for bitbucket cloud + if (host === 'bitbucket.org') { + if (!workspace) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + ); + } + } + + // Project is required for both bitbucket cloud and bitbucket server + if (!project) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing project`, + ); + } const integrationConfig = integrations.bitbucket.byHost(host); @@ -272,14 +290,15 @@ export function createPublishBitbucketAction(options: { const apiBaseUrl = integrationConfig.config.apiBaseUrl; const createMethod = - host === 'bitbucket.org' - ? createBitbucketCloudRepository - : createBitbucketServerRepository; + host === 'bitbucket.org' + ? createBitbucketCloudRepository + : createBitbucketServerRepository; const { remoteUrl, repoContentsUrl } = await createMethod({ authorization, host, - owner, + workspace: workspace || '', + project, repo, repoVisibility, description, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts deleted file mode 100644 index dea10e6ae1..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -jest.mock('../../../stages/publish/helpers'); - -import { createPublishBitbucketCloudAction } from './bitbucketCloud'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { msw } from '@backstage/test-utils'; -import { ScmIntegrations } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; - -describe('publish:bitbucketcloud', () => { - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - bitbucket: [ - { - host: 'bitbucket.org', - token: 'tokenlols', - }, - ], - }, - }), - ); - const action = createPublishBitbucketCloudAction({ integrations }); - const mockContext = { - input: { - repoUrl: 'bitbucket.org?repo=repo&workspace=workspace&project=project', - repoVisibility: 'private', - }, - workspacePath: 'lol', - logger: getVoidLogger(), - logStream: new PassThrough(), - output: jest.fn(), - createTemporaryDirectory: jest.fn(), - }; - const server = setupServer(); - msw.setupDefaultHandlers(server); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should throw an error when the repoUrl is not well formed', async () => { - - await expect( - action.handler({ - ...mockContext, - input: { repoUrl: 'bitbucket.org?repo=bob&workspace=workspace1' }, - }), - ).rejects.toThrow(/missing project/); - - await expect( - action.handler({ - ...mockContext, - input: { repoUrl: 'bitbucket.org?repo=bob&project=project1' }, - }), - ).rejects.toThrow(/missing workspace/); - - await expect( - action.handler({ - ...mockContext, - input: { repoUrl: 'bitbucket.org?workspace=workspace1&project=project1' }, - }), - ).rejects.toThrow(/missing repo/); - }); - - it('should call the correct APIs when the host is bitbucket cloud', async () => { - expect.assertions(2); - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (req, res, ctx) => { - expect(req.headers.get('Authorization')).toBe('Bearer tokenlols'); - expect(req.body).toEqual({ is_private: true, scm: 'git', project: { key: 'project' } }); - return res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/repo', - }, - ], - }, - }), - ); - }, - ), - ); - - await action.handler(mockContext); - }); - - it('should call initAndPush with the correct values', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler(mockContext); - - expect(initRepoAndPush).toHaveBeenCalledWith({ - dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/workspace/cloneurl', - auth: { username: 'x-token-auth', password: 'tokenlols' }, - logger: mockContext.logger, - }); - }); - - it('should call outputs with the correct urls', async () => { - server.use( - rest.post( - 'https://api.bitbucket.org/2.0/repositories/workspace/repo', - (_, res, ctx) => - res( - ctx.status(200), - ctx.set('Content-Type', 'application/json'), - ctx.json({ - links: { - html: { - href: 'https://bitbucket.org/workspace/repo', - }, - clone: [ - { - name: 'https', - href: 'https://bitbucket.org/workspace/cloneurl', - }, - ], - }, - }), - ), - ), - ); - - await action.handler(mockContext); - - expect(mockContext.output).toHaveBeenCalledWith( - 'remoteUrl', - 'https://bitbucket.org/workspace/cloneurl', - ); - expect(mockContext.output).toHaveBeenCalledWith( - 'repoContentsUrl', - 'https://bitbucket.org/workspace/repo/src/master', - ); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts deleted file mode 100644 index db5aa3aff2..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucketCloud.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { InputError } from '@backstage/errors'; -import { - BitbucketIntegrationConfig, - ScmIntegrationRegistry, -} from '@backstage/integration'; -import { initRepoAndPush } from '../../../stages/publish/helpers'; -import { getRepoSourceDirectory, parseRepoUrlForBitbucket } from './util'; -import fetch from 'cross-fetch'; -import { createTemplateAction } from '../../createTemplateAction'; - -const createBitbucketCloudRepository = async (opts: { - workspace: string; - project: string; - repo: string; - description: string; - repoVisibility: 'private' | 'public'; - authorization: string; -}) => { - const { workspace, project, repo, description, repoVisibility, authorization } = opts; - - const options: RequestInit = { - method: 'POST', - body: JSON.stringify({ - scm: 'git', - description: description, - is_private: repoVisibility === 'private', - project: { key: project }, - }), - headers: { - Authorization: authorization, - 'Content-Type': 'application/json', - }, - }; - - let response: Response; - try { - response = await fetch( - `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, - options, - ); - } catch (e) { - throw new Error(`Unable to create repository, ${e}`); - } - - if (response.status !== 200) { - throw new Error( - `Unable to create repository, ${response.status} ${ - response.statusText - }, ${await response.text()}`, - ); - } - - const r = await response.json(); - let remoteUrl = ''; - for (const link of r.links.clone) { - if (link.name === 'https') { - remoteUrl = link.href; - } - } - - // TODO use the urlReader to get the default branch - const repoContentsUrl = `${r.links.html.href}/src/master`; - return { remoteUrl, repoContentsUrl }; -}; - -const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => { - if (config.username && config.appPassword) { - const buffer = Buffer.from( - `${config.username}:${config.appPassword}`, - 'utf8', - ); - - return `Basic ${buffer.toString('base64')}`; - } - - if (config.token) { - return `Bearer ${config.token}`; - } - - throw new Error( - `Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`, - ); -}; - -export function createPublishBitbucketCloudAction(options: { - integrations: ScmIntegrationRegistry; -}) { - const { integrations } = options; - - return createTemplateAction<{ - repoUrl: string; - description: string; - repoVisibility: 'private' | 'public'; - sourcePath?: string; - }>({ - id: 'publish:bitbucketcloud', - description: - 'Initializes a git repository of the content in the workspace, and publishes it to Bitbucket.', - schema: { - input: { - type: 'object', - required: ['repoUrl'], - properties: { - repoUrl: { - title: 'Repository Location', - type: 'string', - }, - description: { - title: 'Repository Description', - type: 'string', - }, - repoVisibility: { - title: 'Repository Visiblity', - type: 'string', - enum: ['private', 'public'], - }, - sourcePath: { - title: - 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the respository.', - type: 'string', - }, - }, - }, - output: { - type: 'object', - properties: { - remoteUrl: { - title: 'A URL to the repository with the provider', - type: 'string', - }, - repoContentsUrl: { - title: 'A URL to the root of the repository', - type: 'string', - }, - }, - }, - }, - async handler(ctx) { - const { repoUrl, description, repoVisibility = 'private' } = ctx.input; - - const { workspace, project, repo, host } = parseRepoUrlForBitbucket(repoUrl); - - const integrationConfig = integrations.bitbucket.byHost(host); - - if (!integrationConfig) { - throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config`, - ); - } - - const authorization = getAuthorizationHeader(integrationConfig.config); - - const createMethod = createBitbucketCloudRepository; - - const { remoteUrl, repoContentsUrl } = await createMethod({ - authorization, - workspace, - project, - repo, - repoVisibility, - description, - }); - - await initRepoAndPush({ - dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), - remoteUrl, - auth: { - username: integrationConfig.config.username - ? integrationConfig.config.username - : 'x-token-auth', - password: integrationConfig.config.appPassword - ? integrationConfig.config.appPassword - : integrationConfig.config.token ?? '', - }, - logger: ctx.logger, - }); - - ctx.output('remoteUrl', remoteUrl); - ctx.output('repoContentsUrl', repoContentsUrl); - }, - }); -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index bdc1577ee5..a292438d3a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -16,7 +16,6 @@ export { createPublishAzureAction } from './azure'; export { createPublishBitbucketAction } from './bitbucket'; -export { createPublishBitbucketCloudAction } from './bitbucketCloud'; export { createPublishFileAction } from './file'; export { createPublishGithubAction } from './github'; export { createPublishGithubPullRequestAction } from './githubPullRequest'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index 1d74ab8328..c86c4d8f38 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -47,19 +47,12 @@ export const parseRepoUrl = (repoUrl: string): RepoSpec => { ); } const host = parsed.host; + const type = parsed.searchParams.get('type'); const owner = parsed.searchParams.get('owner'); + const workspace = parsed.searchParams.get('workspace'); + const project = parsed.searchParams.get('project'); - if (!owner) { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing owner`, - ); - } - const repo = parsed.searchParams.get('repo'); - if (!repo) { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing repo`, - ); - } + if (type === 'bitbucket') { const organization = parsed.searchParams.get('organization') ?? undefined; @@ -84,23 +77,30 @@ export const parseRepoUrlForBitbucket = (repoUrl: string) => { `Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`, ); } - - const project = parsed.searchParams.get('project'); if (!project) { throw new InputError( `Invalid repo URL passed to publisher: ${repoUrl}, missing project`, ); } + } - const repo = parsed.searchParams.get('repo'); - if (!repo) { + else { + if (!owner) { throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing repo`, + `Invalid repo URL passed to publisher: ${repoUrl}, missing owner`, ); } + } - const organization = parsed.searchParams.get('organization'); + const repo = parsed.searchParams.get('repo'); + if (!repo) { + throw new InputError( + `Invalid repo URL passed to publisher: ${repoUrl}, missing repo`, + ); + } - return { host, workspace, project, repo, organization }; + const organization = parsed.searchParams.get('organization'); + return { host, owner, repo, organization, workspace, project }; }; + diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 47a1d8cc62..37d78d0fda 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -79,10 +79,6 @@ export interface ScaffolderApi { allowedHosts: string[]; }): Promise<{ type: string; title: string; host: string }[]>; - getIntegration(options: { - type: string; - }): Promise; - // Returns a list of all installed actions. listActions(): Promise; @@ -121,23 +117,6 @@ export class ScaffolderClient implements ScaffolderApi { .filter(c => options.allowedHosts.includes(c.host)); } - async getIntegration(options: { type: string }) { - switch (options.type) { - case 'azure': - return this.scmIntegrationsApi.azure.list() - case 'bitbucket': - return this.scmIntegrationsApi.bitbucket.list() - case 'github': - return this.scmIntegrationsApi.github.list() - case 'gitlab': - return this.scmIntegrationsApi.gitlab.list() - default: - throw new Error( - `No integration found for ${options.type}`, - ); - } - } - async getTemplateParameterSchema( templateName: EntityName, ): Promise { diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index 93bd5d3fd3..77c8240974 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -24,7 +24,6 @@ const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), getTemplateParameterSchema: jest.fn(), getIntegrationsList: jest.fn(), - getIntegration: jest.fn(), getTask: jest.fn(), streamLogs: jest.fn(), listActions: jest.fn(), diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index ef2acd6de3..e42bcbdbdc 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -40,7 +40,6 @@ const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), getTemplateParameterSchema: jest.fn(), getIntegrationsList: jest.fn(), - getIntegration: jest.fn(), getTask: jest.fn(), streamLogs: jest.fn(), listActions: jest.fn(), diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 4287d1147b..8d936ebb46 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -28,36 +28,50 @@ import { Progress } from '@backstage/core-components'; function splitFormData(url: string | undefined) { let host = undefined; + let type = undefined; let owner = undefined; let repo = undefined; let organization = undefined; + let workspace = undefined; + let project = undefined; try { if (url) { const parsed = new URL(`https://${url}`); host = parsed.host; + type = parsed.searchParams.get('type') || undefined; owner = parsed.searchParams.get('owner') || undefined; repo = parsed.searchParams.get('repo') || undefined; // This is azure dev ops specific. not used for any other provider. organization = parsed.searchParams.get('organization') || undefined; + // These are bitbucket specific, not used for any other provider. + workspace = parsed.searchParams.get('workspace') || undefined; + project = parsed.searchParams.get('project') || undefined; } } catch { /* ok */ } - return { host, owner, repo, organization }; + return { host, type, owner, repo, organization, workspace, project }; } function serializeFormData(data: { host?: string; + type?: string; owner?: string; repo?: string; organization?: string; + workspace?: string; + project?: string; }) { if (!data.host) { return undefined; } + const params = new URLSearchParams(); + if (data.type) { + params.set('type', data.type); + } if (data.owner) { params.set('owner', data.owner); } @@ -67,6 +81,12 @@ function serializeFormData(data: { if (data.organization) { params.set('organization', data.organization); } + if (data.workspace) { + params.set('workspace', data.workspace); + } + if (data.project) { + params.set('project', data.project); + } return `${data.host}?${params.toString()}`; } @@ -84,18 +104,22 @@ export const RepoUrlPicker = ({ return await api.getIntegrationsList({ allowedHosts }); }); - const { host, owner, repo, organization } = splitFormData(formData); + const { host, type, owner, repo, organization, workspace, project } = splitFormData(formData); const updateHost = useCallback( - (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { onChange( serializeFormData({ host: evt.target.value as string, + type: (integrations?getIntegrationTypeByHost(evt.target.value as string, integrations): undefined), owner, repo, organization, + workspace, + project, }), - ), - [onChange, owner, repo, organization], + ) + }, + [onChange, integrations, owner, repo, organization, workspace, project], ); const updateOwner = useCallback( @@ -103,12 +127,15 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, + type, owner: evt.target.value as string, repo, organization, + workspace, + project, }), ), - [onChange, host, repo, organization], + [onChange, host, type, repo, organization, workspace, project], ); const updateRepo = useCallback( @@ -116,12 +143,15 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, + type, owner, repo: evt.target.value as string, organization, + workspace, + project, }), ), - [onChange, host, owner, organization], + [onChange, host, type, owner, organization, workspace, project], ); const updateOrganization = useCallback( @@ -129,12 +159,47 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, + type, owner, repo, organization: evt.target.value as string, + workspace, + project, }), ), - [onChange, host, owner, repo], + [onChange, host , type, owner, repo, workspace, project], + ); + + const updateWorkspace = useCallback( + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => + onChange( + serializeFormData({ + host, + type, + owner, + repo, + organization, + workspace: evt.target.value as string, + project, + }), + ), + [onChange, host, type, owner, repo, organization, project], + ); + + const updateProject = useCallback( + (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => + onChange( + serializeFormData({ + host, + type, + owner, + repo, + organization, + workspace, + project: evt.target.value as string, + }), + ), + [onChange, host, type, owner, repo, organization, workspace], ); useEffect(() => { @@ -142,13 +207,16 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host: integrations[0].host, + type: integrations[0].type, owner, repo, organization, + workspace, + project, }), ); } - }, [onChange, integrations, host, owner, repo, organization]); + }, [onChange, integrations, host, type, owner, repo, organization, workspace, project]); if (loading) { return ; @@ -179,6 +247,7 @@ export const RepoUrlPicker = ({ The host where the repository will be created + {/* Show this for dev.azure.com only */} {host === 'dev.azure.com' && ( The name of the organization )} - 0 && !owner} - > - Owner - - - The organization, user or project that this repo will belong to - - + {/* Show this for bitbucket.org only */} + {type === 'bitbucket' && ( + <> + {host === 'bitbucket.org' && ( + 0 && !workspace} + > + Workspace + + + The workspace where the repository will be created + + + )} + 0 && !project} + > + Project + + + The project where the repository will be created + + + + )} + {/* Show this for all hosts except bitbucket */} + {type !== 'bitbucket' && ( + <> + 0 && !owner} + > + Owner + + + The organization, user or project that this repo will belong to + + + + )} + {/* Show this for all hosts */} { - - const api = useApi(scaffolderApiRef); - const allowedHosts: string[] = ['bitbucket.org']; - - const { value: integrations, loading: loadingIntegrations } = useAsync(async () => { - return await api.getIntegrationsList({ allowedHosts }); - }); - - const { value: data, loading: loadingData } = useAsync(async () => { - const bitbucketIntegrations: BitbucketIntegration[] = await api.getIntegration({ type: 'bitbucket' }) as BitbucketIntegration[]; - const bitbucketIntegration = bitbucketIntegrations.filter(element => element.config.host === 'bitbucket.org')[0]; - const workspaces = bitbucketIntegration.config.workspaces; - const projects = workspaces?.[0]?.projects; - - return { workspaces, projects }; - }); - - const { host, workspace, project, repo } = splitFormData(formData); - const updateHost = useCallback( - (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => - onChange( - serializeFormData({ - host: evt.target.value as string, - workspace, - project, - repo, - }), - ), - [onChange, workspace, project, repo], - ); - - const updateWorkspace = useCallback( - (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { - if (data) { - data.projects = data?.workspaces?.find(w => w.name === evt.target.value)?.projects; - } - onChange( - serializeFormData({ - host, - workspace: evt.target.value as string, - project, - repo, - }), - ); - }, - [onChange, data, host, project, repo], - ); - - const updateProject = useCallback( - (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { - onChange( - serializeFormData({ - host, - workspace, - project: evt.target.value as string, - repo, - }), - ); - }, - [onChange, host, workspace, repo], - ); - - const updateRepo = useCallback( - (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => - onChange( - serializeFormData({ - host, - workspace, - project, - repo: evt.target.value as string, - }), - ), - [onChange, host, workspace, project], - ); - - useEffect(() => { - onChange( - serializeFormData({ - host: (host === undefined ? (integrations?.[0]?.host) : undefined), - workspace: workspace === undefined ? (data?.workspaces?.[0]?.name) : undefined, - project: project === undefined ? (data?.workspaces?.[0]?.projects?.[0]?.key) : undefined, - repo, - }), - ); - }, [onChange, integrations, data, host, workspace, project, repo]); - - if (loadingIntegrations || loadingData) { - return ; - } - - return ( - <> - 0 && !host} - > - Host - - - The host where the repository will be created - - - 0 && !workspace} - > - Workspace - - - The workspace where the repository will be created - - - 0 && !project} - > - Project - - The bitbucket project this repo should be added to - - 0 && !repo} - > - Repository - - The name of the repository - - - ); -}; From 0f101a87d9b5982790aa5f6ead9ef898dc75b278 Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Wed, 23 Jun 2021 10:57:15 +1200 Subject: [PATCH 14/46] Fix RepoUrlPicker to use integration api directly Signed-off-by: Mustansar Anwar ul Samad --- app-config.yaml | 5 +++ .../bitbucket-demo/template.yaml | 2 -- .../actions/builtin/publish/bitbucket.ts | 5 ++- .../actions/builtin/publish/github.ts | 8 ++++- .../actions/builtin/publish/gitlab.ts | 8 ++++- .../actions/builtin/publish/util.ts | 24 +++++++++---- .../src/scaffolder/tasks/TaskWorker.test.ts | 29 +++++++++++++-- .../src/scaffolder/tasks/TaskWorker.ts | 3 +- .../scaffolder-backend/src/service/router.ts | 1 + .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 36 +++++++------------ .../fields/RepoUrlPicker/validation.ts | 2 +- 11 files changed, 85 insertions(+), 38 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index f2df97b7b2..c1d0121dff 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -152,6 +152,11 @@ integrations: - host: bitbucket.org username: ${BITBUCKET_USERNAME} appPassword: ${BITBUCKET_APP_PASSWORD} + ### Example for how to add your bitbucket server instance using the API: + # - host: server.bitbucket.com + # apiBaseUrl: server.bitbucket.com + # username: ${BITBUCKET_SERVER_USERNAME} + # appPassword: ${BITBUCKET_SERVER_APP_PASSWORD} azure: - host: dev.azure.com token: ${AZURE_TOKEN} diff --git a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml index fda1324e42..87f4d449f5 100644 --- a/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/bitbucket-demo/template.yaml @@ -19,7 +19,6 @@ spec: ui:field: RepoUrlPicker ui:options: allowedHosts: - - github.com - bitbucket.org - server.bitbucket.com - title: Fill in some steps @@ -64,7 +63,6 @@ spec: name: Publish action: publish:bitbucket input: - allowedHosts: ['bitbucket.org'] description: 'This is {{ parameters.name }}' repoUrl: '{{ parameters.repoUrl }}' diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 092390d08e..1291684719 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -260,7 +260,10 @@ export function createPublishBitbucketAction(options: { enableLFS = false, } = ctx.input; - const { workspace, project, repo, host } = parseRepoUrl(repoUrl); + const { workspace, project, repo, host } = parseRepoUrl( + repoUrl, + integrations, + ); // Workspace is only required for bitbucket cloud if (host === 'bitbucket.org') { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 640ae5dfe5..f767d6d38c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -144,7 +144,13 @@ export function createPublishGithubAction(options: { topics, } = ctx.input; - const { owner, repo, host } = parseRepoUrl(repoUrl); + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError( + `No owner provided for host: ${host}, and repo ${repo}`, + ); + } const credentialsProvider = credentialsProviders.get(host); const integrationConfig = integrations.github.byHost(host); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index fbd2dcaaae..eead310777 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -84,7 +84,13 @@ export function createPublishGitlabAction(options: { defaultBranch = 'master', } = ctx.input; - const { owner, repo, host } = parseRepoUrl(repoUrl); + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError( + `No owner provided for host: ${host}, and repo ${repo}`, + ); + } const integrationConfig = integrations.gitlab.byHost(host); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index c86c4d8f38..0cc0830fd7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -16,6 +16,7 @@ import { InputError } from '@backstage/errors'; import { join as joinPath, normalize as normalizePath } from 'path'; +import { ScmIntegrationRegistry } from '@backstage/integration'; export const getRepoSourceDirectory = ( workspacePath: string, @@ -33,11 +34,16 @@ export const getRepoSourceDirectory = ( export type RepoSpec = { repo: string; host: string; - owner: string; + owner?: string; organization?: string; + workspace?: string; + project?: string; }; -export const parseRepoUrl = (repoUrl: string): RepoSpec => { +export const parseRepoUrl = ( + repoUrl: string, + integrations: ScmIntegrationRegistry, +): RepoSpec => { let parsed; try { parsed = new URL(`https://${repoUrl}`); @@ -47,10 +53,16 @@ export const parseRepoUrl = (repoUrl: string): RepoSpec => { ); } const host = parsed.host; - const type = parsed.searchParams.get('type'); - const owner = parsed.searchParams.get('owner'); - const workspace = parsed.searchParams.get('workspace'); - const project = parsed.searchParams.get('project'); + const owner = parsed.searchParams.get('owner') ?? undefined; + const organization = parsed.searchParams.get('organization') ?? undefined; + const workspace = parsed.searchParams.get('workspace') ?? undefined; + const project = parsed.searchParams.get('project') ?? undefined; + + const type = integrations.byHost(host)?.type; + + if (!type) { + throw new InputError(`Unable to find host ${host} in integrations`); + } if (type === 'bitbucket') { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index b5e8d19e62..6b18d0dac3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -41,6 +41,14 @@ describe('TaskWorker', () => { let storage: DatabaseTaskStore; let actionRegistry = new TemplateActionRegistry(); + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'token' }], + }, + }), + ); + beforeAll(async () => { storage = await createStore(); }); @@ -65,6 +73,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], @@ -90,6 +99,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ @@ -142,6 +152,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ @@ -331,6 +342,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ @@ -388,6 +400,12 @@ describe('TaskWorker', () => { organization: { type: 'string', }, + workspace: { + type: 'string', + }, + project: { + type: 'string', + }, }, }, }, @@ -396,7 +414,10 @@ describe('TaskWorker', () => { async handler(ctx) { ctx.output('host', ctx.input.destination.host); ctx.output('repo', ctx.input.destination.repo); - ctx.output('owner', ctx.input.destination.owner); + + if (ctx.input.destination.owner) { + ctx.output('owner', ctx.input.destination.owner); + } if (ctx.input.destination.host !== 'github.com') { throw new Error( @@ -410,7 +431,10 @@ describe('TaskWorker', () => { ); } - if (ctx.input.destination.owner !== 'owner') { + if ( + ctx.input.destination.owner && + ctx.input.destination.owner !== 'owner' + ) { throw new Error( `expected repo to be "owner" got ${ctx.input.destination.owner}`, ); @@ -425,6 +449,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index b4ba936719..d34979599d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -33,6 +33,7 @@ type Options = { taskBroker: TaskBroker; workingDirectory: string; actionRegistry: TemplateActionRegistry; + integrations: ScmIntegrations; }; export class TaskWorker { @@ -45,7 +46,7 @@ export class TaskWorker { // scary right now, so we're going to lock it off like the component API is // in the frontend until we can work out a nice way to do it. this.handlebars.registerHelper('parseRepoUrl', repoUrl => { - return JSON.stringify(parseRepoUrl(repoUrl)); + return JSON.stringify(parseRepoUrl(repoUrl, options.integrations)); }); this.handlebars.registerHelper('projectSlug', repoUrl => { diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index cee9d2cfb4..f2a9c5b708 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -90,6 +90,7 @@ export async function createRouter( taskBroker, actionRegistry, workingDirectory, + integrations, }); workers.push(worker); } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 8d936ebb46..278f1c3ae8 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -16,6 +16,7 @@ import React, { useCallback, useEffect } from 'react'; import { FieldProps } from '@rjsf/core'; import { scaffolderApiRef } from '../../../api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { useAsync } from 'react-use'; import Select from '@material-ui/core/Select'; import InputLabel from '@material-ui/core/InputLabel'; @@ -28,7 +29,6 @@ import { Progress } from '@backstage/core-components'; function splitFormData(url: string | undefined) { let host = undefined; - let type = undefined; let owner = undefined; let repo = undefined; let organization = undefined; @@ -39,7 +39,6 @@ function splitFormData(url: string | undefined) { if (url) { const parsed = new URL(`https://${url}`); host = parsed.host; - type = parsed.searchParams.get('type') || undefined; owner = parsed.searchParams.get('owner') || undefined; repo = parsed.searchParams.get('repo') || undefined; // This is azure dev ops specific. not used for any other provider. @@ -52,12 +51,11 @@ function splitFormData(url: string | undefined) { /* ok */ } - return { host, type, owner, repo, organization, workspace, project }; + return { host, owner, repo, organization, workspace, project }; } function serializeFormData(data: { host?: string; - type?: string; owner?: string; repo?: string; organization?: string; @@ -69,9 +67,6 @@ function serializeFormData(data: { } const params = new URLSearchParams(); - if (data.type) { - params.set('type', data.type); - } if (data.owner) { params.set('owner', data.owner); } @@ -97,11 +92,12 @@ export const RepoUrlPicker = ({ rawErrors, formData, }: FieldProps) => { - const api = useApi(scaffolderApiRef); + const scaffolderApi = useApi(scaffolderApiRef); + const integrationApi = useApi(scmIntegrationsApiRef); const allowedHosts = uiSchema['ui:options']?.allowedHosts as string[]; const { value: integrations, loading } = useAsync(async () => { - return await api.getIntegrationsList({ allowedHosts }); + return await scaffolderApi.getIntegrationsList({ allowedHosts }); }); const { host, type, owner, repo, organization, workspace, project } = splitFormData(formData); @@ -119,7 +115,7 @@ export const RepoUrlPicker = ({ }), ) }, - [onChange, integrations, owner, repo, organization, workspace, project], + [onChange, owner, repo, organization, workspace, project], ); const updateOwner = useCallback( @@ -127,7 +123,6 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, - type, owner: evt.target.value as string, repo, organization, @@ -135,7 +130,7 @@ export const RepoUrlPicker = ({ project, }), ), - [onChange, host, type, repo, organization, workspace, project], + [onChange, host, repo, organization, workspace, project], ); const updateRepo = useCallback( @@ -143,7 +138,6 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, - type, owner, repo: evt.target.value as string, organization, @@ -151,7 +145,7 @@ export const RepoUrlPicker = ({ project, }), ), - [onChange, host, type, owner, organization, workspace, project], + [onChange, host, owner, organization, workspace, project], ); const updateOrganization = useCallback( @@ -159,7 +153,6 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, - type, owner, repo, organization: evt.target.value as string, @@ -175,7 +168,6 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, - type, owner, repo, organization, @@ -183,7 +175,7 @@ export const RepoUrlPicker = ({ project, }), ), - [onChange, host, type, owner, repo, organization, project], + [onChange, host, owner, repo, organization, project], ); const updateProject = useCallback( @@ -191,7 +183,6 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host, - type, owner, repo, organization, @@ -199,7 +190,7 @@ export const RepoUrlPicker = ({ project: evt.target.value as string, }), ), - [onChange, host, type, owner, repo, organization, workspace], + [onChange, host, owner, repo, organization, workspace], ); useEffect(() => { @@ -207,7 +198,6 @@ export const RepoUrlPicker = ({ onChange( serializeFormData({ host: integrations[0].host, - type: integrations[0].type, owner, repo, organization, @@ -263,9 +253,9 @@ export const RepoUrlPicker = ({ The name of the organization )} - {/* Show this for bitbucket.org only */} - {type === 'bitbucket' && ( + {host && integrationApi.byHost(host)?.type === 'bitbucket' && ( <> + {/* Show this for bitbucket.org only */} {host === 'bitbucket.org' && ( )} {/* Show this for all hosts except bitbucket */} - {type !== 'bitbucket' && ( + {host && integrationApi.byHost(host)?.type !== 'bitbucket' && ( <> { try { const { host, searchParams } = new URL(`https://${value}`); - if (!host || !searchParams.get('owner') || !searchParams.get('repo')) { + if (!host || !searchParams.get('repo')) { validation.addError('Incomplete repository location provided'); } } catch { From dc962cab25c25365a4ba3e0ad8b1df6d0a5fc206 Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Mon, 9 Aug 2021 21:32:33 +1200 Subject: [PATCH 15/46] Rebase on upstream to get context for validator Signed-off-by: Mustansar Anwar ul Samad --- .../builtin/github/githubActionsDispatch.ts | 8 ++++- .../actions/builtin/publish/azure.ts | 5 ++- .../actions/builtin/publish/bitbucket.test.ts | 23 +++++++----- .../actions/builtin/publish/bitbucket.ts | 33 ++++++++++------- .../builtin/publish/githubPullRequest.ts | 8 ++++- .../actions/builtin/publish/util.ts | 36 ++++--------------- .../src/scaffolder/tasks/TaskWorker.test.ts | 4 +++ .../src/scaffolder/tasks/TaskWorker.ts | 3 +- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 32 +++++++++++++---- 9 files changed, 91 insertions(+), 61 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts index 7f48a155ed..5dfba7776e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -69,7 +69,13 @@ export function createGithubActionsDispatchAction(options: { async handler(ctx) { const { repoUrl, workflowId, branchOrTagName } = ctx.input; - const { owner, repo, host } = parseRepoUrl(repoUrl); + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError( + `No owner provided for host: ${host}, and repo ${repo}`, + ); + } ctx.logger.info( `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index f5c7879fa9..7e394a8d8b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -80,7 +80,10 @@ export function createPublishAzureAction(options: { async handler(ctx) { const { repoUrl, defaultBranch = 'master' } = ctx.input; - const { owner, repo, host, organization } = parseRepoUrl(repoUrl); + const { owner, repo, host, organization } = parseRepoUrl( + repoUrl, + integrations, + ); if (!organization) { throw new InputError( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 8efd644bbe..61421857de 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -50,7 +50,7 @@ describe('publish:bitbucket', () => { const action = createPublishBitbucketAction({ integrations, config }); const mockContext = { input: { - repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&project=project&repo=repo', + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', repoVisibility: 'private', }, workspacePath: 'lol', @@ -70,21 +70,21 @@ describe('publish:bitbucket', () => { await expect( action.handler({ ...mockContext, - input: { repoUrl: 'bitbucket.org?type=bitbucket&project=project&repo=repo' }, + input: { repoUrl: 'bitbucket.org?project=project&repo=repo' }, }), ).rejects.toThrow(/missing workspace/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&repo=repo' }, + input: { repoUrl: 'bitbucket.org?workspace=workspace&repo=repo' }, }), ).rejects.toThrow(/missing project/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'bitbucket.org?type=bitbucket&workspace=workspace&project=project' }, + input: { repoUrl: 'bitbucket.org?workspace=workspace&project=project' }, }), ).rejects.toThrow(/missing repo/); }); @@ -93,7 +93,9 @@ describe('publish:bitbucket', () => { await expect( action.handler({ ...mockContext, - input: { repoUrl: 'missing.com?type=bitbucket&workspace=workspace&project=project&repo=repo' }, + input: { + repoUrl: 'missing.com?workspace=workspace&project=project&repo=repo', + }, }), ).rejects.toThrow(/No matching integration configuration/); }); @@ -103,7 +105,8 @@ describe('publish:bitbucket', () => { action.handler({ ...mockContext, input: { - repoUrl: 'notoken.bitbucket.com?type=bitbucket&workspace=workspace&project=project&repo=repo', + repoUrl: + 'notoken.bitbucket.com?workspace=workspace&project=project&repo=repo', }, }), ).rejects.toThrow(/Authorization has not been provided for Bitbucket/); @@ -116,7 +119,11 @@ describe('publish:bitbucket', () => { 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Bearer tokenlols'); - expect(req.body).toEqual({ is_private: true, scm: 'git', project: { key: 'project' } }); + expect(req.body).toEqual({ + is_private: true, + scm: 'git', + project: { key: 'project' }, + }); return res( ctx.status(200), ctx.set('Content-Type', 'application/json'), @@ -176,7 +183,7 @@ describe('publish:bitbucket', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?type=bitbucket&project=project&repo=repo', + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', }, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 1291684719..daa6bc249b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -33,7 +33,14 @@ const createBitbucketCloudRepository = async (opts: { repoVisibility: 'private' | 'public'; authorization: string; }) => { - const { workspace, project, repo, description, repoVisibility, authorization } = opts; + const { + workspace, + project, + repo, + description, + repoVisibility, + authorization, + } = opts; const options: RequestInit = { method: 'POST', @@ -115,7 +122,7 @@ const createBitbucketServerRepository = async (opts: { try { const baseUrl = apiBaseUrl ? apiBaseUrl : `https://${host}/rest/api/1.0`; - response = await fetch(`${baseUrl}/projects/${owner}/repos`, options); + response = await fetch(`${baseUrl}/projects/${project}/repos`, options); } catch (e) { throw new Error(`Unable to create repository, ${e}`); } @@ -162,10 +169,10 @@ const getAuthorizationHeader = (config: BitbucketIntegrationConfig) => { const performEnableLFS = async (opts: { authorization: string; host: string; - owner: string; + project: string; repo: string; }) => { - const { authorization, host, owner, repo } = opts; + const { authorization, host, project, repo } = opts; const options: RequestInit = { method: 'PUT', @@ -175,7 +182,7 @@ const performEnableLFS = async (opts: { }; const { ok, status, statusText } = await fetch( - `https://${host}/rest/git-lfs/admin/projects/${owner}/repos/${repo}/enabled`, + `https://${host}/rest/git-lfs/admin/projects/${project}/repos/${repo}/enabled`, options, ); @@ -268,10 +275,10 @@ export function createPublishBitbucketAction(options: { // Workspace is only required for bitbucket cloud if (host === 'bitbucket.org') { if (!workspace) { - throw new InputError( - `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, - ); - } + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + ); + } } // Project is required for both bitbucket cloud and bitbucket server @@ -293,9 +300,9 @@ export function createPublishBitbucketAction(options: { const apiBaseUrl = integrationConfig.config.apiBaseUrl; const createMethod = - host === 'bitbucket.org' - ? createBitbucketCloudRepository - : createBitbucketServerRepository; + host === 'bitbucket.org' + ? createBitbucketCloudRepository + : createBitbucketServerRepository; const { remoteUrl, repoContentsUrl } = await createMethod({ authorization, @@ -333,7 +340,7 @@ export function createPublishBitbucketAction(options: { }); if (enableLFS && host !== 'bitbucket.org') { - await performEnableLFS({ authorization, host, owner, repo }); + await performEnableLFS({ authorization, host, project, repo }); } ctx.output('remoteUrl', remoteUrl); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 408ec243e7..e21ffc5cd0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -173,7 +173,13 @@ export const createPublishGithubPullRequestAction = ({ sourcePath, } = ctx.input; - const { owner, repo, host } = parseRepoUrl(repoUrl); + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError( + `No owner provided for host: ${host}, and repo ${repo}`, + ); + } const client = await clientFactory({ integrations, host, owner, repo }); const fileRoot = sourcePath diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index 0cc0830fd7..5c10e66e39 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -65,38 +65,19 @@ export const parseRepoUrl = ( } if (type === 'bitbucket') { - - const organization = parsed.searchParams.get('organization') ?? undefined; - - return { host, owner, repo, organization }; -}; - -export const parseRepoUrlForBitbucket = (repoUrl: string) => { - let parsed; - try { - parsed = new URL(`https://${repoUrl}`); - } catch (error) { - throw new InputError( - `Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`, - ); - } - const host = parsed.host; - - const workspace = parsed.searchParams.get('workspace'); - - if (!workspace) { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`, - ); + if (host === 'bitbucket.org') { + if (!workspace) { + throw new InputError( + `Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`, + ); + } } if (!project) { throw new InputError( `Invalid repo URL passed to publisher: ${repoUrl}, missing project`, ); } - } - - else { + } else { if (!owner) { throw new InputError( `Invalid repo URL passed to publisher: ${repoUrl}, missing owner`, @@ -111,8 +92,5 @@ export const parseRepoUrlForBitbucket = (repoUrl: string) => { ); } - const organization = parsed.searchParams.get('organization'); - return { host, owner, repo, organization, workspace, project }; }; - diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 6b18d0dac3..2c274df7fc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -22,6 +22,7 @@ import { RepoSpec } from '../actions/builtin/publish/util'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; +import { ScmIntegrations } from '@backstage/integration'; async function createStore(): Promise { const manager = DatabaseManager.fromConfig( @@ -188,6 +189,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ @@ -221,6 +223,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ @@ -254,6 +257,7 @@ describe('TaskWorker', () => { workingDirectory: os.tmpdir(), actionRegistry, taskBroker: broker, + integrations, }); const { taskId } = await broker.dispatch({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index d34979599d..819df56109 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -27,6 +27,7 @@ import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions/TemplateActionRegistry'; import { isTruthy } from './helper'; import { Task, TaskBroker } from './types'; +import { ScmIntegrations } from '@backstage/integration'; type Options = { logger: Logger; @@ -50,7 +51,7 @@ export class TaskWorker { }); this.handlebars.registerHelper('projectSlug', repoUrl => { - const { owner, repo } = parseRepoUrl(repoUrl); + const { owner, repo } = parseRepoUrl(repoUrl, options.integrations); return `${owner}/${repo}`; }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 278f1c3ae8..43ca5bd52e 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -100,20 +100,21 @@ export const RepoUrlPicker = ({ return await scaffolderApi.getIntegrationsList({ allowedHosts }); }); - const { host, type, owner, repo, organization, workspace, project } = splitFormData(formData); + const { host, owner, repo, organization, workspace, project } = splitFormData( + formData, + ); const updateHost = useCallback( (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { onChange( serializeFormData({ host: evt.target.value as string, - type: (integrations?getIntegrationTypeByHost(evt.target.value as string, integrations): undefined), owner, repo, organization, workspace, project, }), - ) + ); }, [onChange, owner, repo, organization, workspace, project], ); @@ -160,7 +161,7 @@ export const RepoUrlPicker = ({ project, }), ), - [onChange, host , type, owner, repo, workspace, project], + [onChange, host, owner, repo, workspace, project], ); const updateWorkspace = useCallback( @@ -206,7 +207,16 @@ export const RepoUrlPicker = ({ }), ); } - }, [onChange, integrations, host, type, owner, repo, organization, workspace, project]); + }, [ + onChange, + integrations, + host, + owner, + repo, + organization, + workspace, + project, + ]); if (loading) { return ; @@ -263,7 +273,11 @@ export const RepoUrlPicker = ({ error={rawErrors?.length > 0 && !workspace} > Workspace - + The workspace where the repository will be created @@ -275,7 +289,11 @@ export const RepoUrlPicker = ({ error={rawErrors?.length > 0 && !project} > Project - + The project where the repository will be created From e30646aebbecef7325c4c1e2d63f3e9426064e99 Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Tue, 10 Aug 2021 17:04:49 +1200 Subject: [PATCH 16/46] Update RepoUrlPicker validation for bitbucket fields Also, - Add and update tests - Add changeset Signed-off-by: Mustansar Anwar ul Samad --- .changeset/perfect-seals-burn.md | 6 + .../actions/builtin/publish/azure.test.ts | 6 +- .../actions/builtin/publish/bitbucket.test.ts | 46 +++--- .../actions/builtin/publish/util.ts | 4 +- .../fields/RepoUrlPicker/validation.test.ts | 153 +++++++++++++++++- .../fields/RepoUrlPicker/validation.ts | 44 ++++- 6 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 .changeset/perfect-seals-burn.md diff --git a/.changeset/perfect-seals-burn.md b/.changeset/perfect-seals-burn.md new file mode 100644 index 0000000000..10503363de --- /dev/null +++ b/.changeset/perfect-seals-burn.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +--- + +Add bitbucket workspace and project fields to RepoUrlPicker to support bitbucket cloud and server diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 0af8b7b975..cee99e1646 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -68,21 +68,21 @@ describe('publish:azure', () => { await expect( action.handler({ ...mockContext, - input: { repoUrl: 'azure.com?repo=bob' }, + input: { repoUrl: 'dev.azure.com?repo=bob' }, }), ).rejects.toThrow(/missing owner/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'azure.com?owner=owner' }, + input: { repoUrl: 'dev.azure.com?owner=owner' }, }), ).rejects.toThrow(/missing repo/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'azure.com?owner=owner&repo=repo' }, + input: { repoUrl: 'dev.azure.com?owner=owner&repo=repo' }, }), ).rejects.toThrow(/missing organization/); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 61421857de..4eee4b4b96 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -145,7 +145,13 @@ describe('publish:bitbucket', () => { ), ); - await action.handler(mockContext); + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo', + }, + }); }); it('should call the correct APIs when the host is hosted bitbucket', async () => { @@ -209,7 +215,7 @@ describe('publish:bitbucket', () => { expect.assertions(1); server.use( rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos', + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', (_, res, ctx) => { return res( ctx.status(201), @@ -219,7 +225,7 @@ describe('publish:bitbucket', () => { }, ), rest.put( - 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled', + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', (req, res, ctx) => { expect(req.headers.get('Authorization')).toBe('Bearer thing'); return res(ctx.status(204)); @@ -231,7 +237,7 @@ describe('publish:bitbucket', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo', + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', enableLFS: true, }, }); @@ -240,7 +246,7 @@ describe('publish:bitbucket', () => { it('should report an error if enabling LFS fails', async () => { server.use( rest.post( - 'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos', + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', (_, res, ctx) => { return res( ctx.status(201), @@ -250,7 +256,7 @@ describe('publish:bitbucket', () => { }, ), rest.put( - 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled', + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', (_, res, ctx) => { return res(ctx.status(500)); }, @@ -262,7 +268,7 @@ describe('publish:bitbucket', () => { ...mockContext, input: { ...mockContext.input, - repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo', + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', enableLFS: true, }, }), @@ -299,7 +305,7 @@ describe('publish:bitbucket', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/owner/cloneurl', + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', defaultBranch: 'master', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, @@ -310,7 +316,7 @@ describe('publish:bitbucket', () => { it('should call initAndPush with the correct default branch', async () => { server.use( rest.post( - 'https://api.bitbucket.org/2.0/repositories/owner/repo', + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (_, res, ctx) => res( ctx.status(200), @@ -318,12 +324,12 @@ describe('publish:bitbucket', () => { ctx.json({ links: { html: { - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, clone: [ { name: 'https', - href: 'https://bitbucket.org/owner/cloneurl', + href: 'https://bitbucket.org/workspace/cloneurl', }, ], }, @@ -342,7 +348,7 @@ describe('publish:bitbucket', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/owner/cloneurl', + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', defaultBranch: 'main', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, @@ -385,7 +391,7 @@ describe('publish:bitbucket', () => { server.use( rest.post( - 'https://api.bitbucket.org/2.0/repositories/owner/repo', + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (_, res, ctx) => res( ctx.status(200), @@ -393,12 +399,12 @@ describe('publish:bitbucket', () => { ctx.json({ links: { html: { - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, clone: [ { name: 'https', - href: 'https://bitbucket.org/owner/cloneurl', + href: 'https://bitbucket.org/workspace/cloneurl', }, ], }, @@ -411,7 +417,7 @@ describe('publish:bitbucket', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/owner/cloneurl', + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, defaultBranch: 'master', @@ -451,7 +457,7 @@ describe('publish:bitbucket', () => { server.use( rest.post( - 'https://api.bitbucket.org/2.0/repositories/owner/repo', + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', (_, res, ctx) => res( ctx.status(200), @@ -459,12 +465,12 @@ describe('publish:bitbucket', () => { ctx.json({ links: { html: { - href: 'https://bitbucket.org/owner/repo', + href: 'https://bitbucket.org/workspace/repo', }, clone: [ { name: 'https', - href: 'https://bitbucket.org/owner/cloneurl', + href: 'https://bitbucket.org/workspace/cloneurl', }, ], }, @@ -477,7 +483,7 @@ describe('publish:bitbucket', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, - remoteUrl: 'https://bitbucket.org/owner/cloneurl', + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, defaultBranch: 'master', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index 5c10e66e39..dea180dc04 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -61,7 +61,9 @@ export const parseRepoUrl = ( const type = integrations.byHost(host)?.type; if (!type) { - throw new InputError(`Unable to find host ${host} in integrations`); + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); } if (type === 'bitbucket') { diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts index d47285bf56..45e44b4072 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.test.ts @@ -16,6 +16,9 @@ import { repoPickerValidation } from './validation'; import { FieldValidation } from '@rjsf/core'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ApiHolder } from '@backstage/core-plugin-api'; describe('RepoPicker Validation', () => { const fieldValidator = () => @@ -23,30 +26,60 @@ describe('RepoPicker Validation', () => { addError: jest.fn(), } as unknown as FieldValidation); + const config = new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + }, + { + host: 'server.bitbucket.com', + }, + ], + github: [ + { + host: 'github.com', + }, + ], + }, + }); + + const scmIntegrations = ScmIntegrations.fromConfig(config); + + const apiHolderMock: jest.Mocked = { + get: jest.fn().mockImplementation(() => { + return scmIntegrations; + }), + }; + it('validates when no repo', () => { const mockFieldValidation = fieldValidator(); - repoPickerValidation('github.com?owner=a', mockFieldValidation); + repoPickerValidation('github.com?owner=a', mockFieldValidation, { + apiHolder: apiHolderMock, + }); expect(mockFieldValidation.addError).toHaveBeenCalledWith( - 'Incomplete repository location provided', + 'Incomplete repository location provided, repo not provided', ); }); it('validates when no owner', () => { const mockFieldValidation = fieldValidator(); - repoPickerValidation('github.com?repo=a', mockFieldValidation); + repoPickerValidation('github.com?repo=a', mockFieldValidation, { + apiHolder: apiHolderMock, + }); expect(mockFieldValidation.addError).toHaveBeenCalledWith( - 'Incomplete repository location provided', + 'Incomplete repository location provided, owner not provided', ); }); it('validates when not a real url', () => { const mockFieldValidation = fieldValidator(); - repoPickerValidation('', mockFieldValidation); + repoPickerValidation('', mockFieldValidation, { apiHolder: apiHolderMock }); expect(mockFieldValidation.addError).toHaveBeenCalledWith( 'Unable to parse the Repository URL', @@ -56,8 +89,116 @@ describe('RepoPicker Validation', () => { it('validates properly with proper input', () => { const mockFieldValidation = fieldValidator(); - repoPickerValidation('github.com?owner=a&repo=b', mockFieldValidation); + repoPickerValidation('github.com?owner=a&repo=b', mockFieldValidation, { + apiHolder: apiHolderMock, + }); expect(mockFieldValidation.addError).not.toHaveBeenCalled(); }); + + it('validates when no workspace, project or repo provided for bitbucket cloud', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation('bitbucket.org', mockFieldValidation, { + apiHolder: apiHolderMock, + }); + + expect(mockFieldValidation.addError).toHaveBeenNthCalledWith( + 1, + 'Incomplete repository location provided, workspace not provided', + ); + expect(mockFieldValidation.addError).toHaveBeenNthCalledWith( + 2, + 'Incomplete repository location provided, project not provided', + ); + expect(mockFieldValidation.addError).toHaveBeenNthCalledWith( + 3, + 'Incomplete repository location provided, repo not provided', + ); + }); + + it('validates when no workspace provided for bitbucket cloud', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation( + 'bitbucket.org?project=p&repo=r', + mockFieldValidation, + { apiHolder: apiHolderMock }, + ); + + expect(mockFieldValidation.addError).toHaveBeenCalledWith( + 'Incomplete repository location provided, workspace not provided', + ); + }); + + it('validates when no project provided for bitbucket cloud', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation( + 'bitbucket.org?workspace=w&repo=r', + mockFieldValidation, + { apiHolder: apiHolderMock }, + ); + + expect(mockFieldValidation.addError).toHaveBeenCalledWith( + 'Incomplete repository location provided, project not provided', + ); + }); + + it('validates when no repo provided for bitbucket cloud', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation( + 'bitbucket.org?workspace=w&project=p', + mockFieldValidation, + { apiHolder: apiHolderMock }, + ); + + expect(mockFieldValidation.addError).toHaveBeenCalledWith( + 'Incomplete repository location provided, repo not provided', + ); + }); + + it('validates when no project or repo provided for bitbucket server', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation('server.bitbucket.com', mockFieldValidation, { + apiHolder: apiHolderMock, + }); + + expect(mockFieldValidation.addError).toHaveBeenNthCalledWith( + 1, + 'Incomplete repository location provided, project not provided', + ); + expect(mockFieldValidation.addError).toHaveBeenNthCalledWith( + 2, + 'Incomplete repository location provided, repo not provided', + ); + }); + + it('validates when no project provided for bitbucket server', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation('server.bitbucket.com?repo=r', mockFieldValidation, { + apiHolder: apiHolderMock, + }); + + expect(mockFieldValidation.addError).toHaveBeenCalledWith( + 'Incomplete repository location provided, project not provided', + ); + }); + + it('validates when no repo provided for bitbucket server', () => { + const mockFieldValidation = fieldValidator(); + + repoPickerValidation( + 'server.bitbucket.com?project=p', + mockFieldValidation, + { apiHolder: apiHolderMock }, + ); + + expect(mockFieldValidation.addError).toHaveBeenCalledWith( + 'Incomplete repository location provided, repo not provided', + ); + }); }); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 02a910ff2d..85232b0588 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -15,16 +15,56 @@ */ import { FieldValidation } from '@rjsf/core'; +import { ApiHolder } from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; export const repoPickerValidation = ( value: string, validation: FieldValidation, + context: { apiHolder: ApiHolder }, ) => { try { const { host, searchParams } = new URL(`https://${value}`); - if (!host || !searchParams.get('repo')) { - validation.addError('Incomplete repository location provided'); + + const integrationApi = context.apiHolder.get(scmIntegrationsApiRef); + + if (!host) { + validation.addError( + 'Incomplete repository location provided, host not provided', + ); + } else { + if (integrationApi?.byHost(host)?.type === 'bitbucket') { + // workspace is only applicable for bitbucket cloud + if (host === 'bitbucket.org' && !searchParams.get('workspace')) { + validation.addError( + 'Incomplete repository location provided, workspace not provided', + ); + } + + if (!searchParams.get('project')) { + validation.addError( + 'Incomplete repository location provided, project not provided', + ); + } + } + // For anything other than bitbucket + else { + if (!searchParams.get('owner')) { + validation.addError( + 'Incomplete repository location provided, owner not provided', + ); + } + } + + // Do this for all hosts + if (!searchParams.get('repo')) { + validation.addError( + 'Incomplete repository location provided, repo not provided', + ); + } } + + // if (!host || !searchParams.get('owner') || !searchParams.get('repo')) { } catch { validation.addError('Unable to parse the Repository URL'); } From 56ce4e1a84737236d8ae3eba494c4f2f26a422d1 Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Tue, 10 Aug 2021 18:47:02 +1200 Subject: [PATCH 17/46] Remove local-templates.yaml as it is not in master anymore Signed-off-by: Mustansar Anwar ul Samad --- .../sample-templates/local-templates.yaml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 plugins/scaffolder-backend/sample-templates/local-templates.yaml diff --git a/plugins/scaffolder-backend/sample-templates/local-templates.yaml b/plugins/scaffolder-backend/sample-templates/local-templates.yaml deleted file mode 100644 index 5c13f0ab40..0000000000 --- a/plugins/scaffolder-backend/sample-templates/local-templates.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: backstage.io/v1alpha1 -kind: Location -metadata: - name: example-templates-local - description: A collection of locally available Backstage example templates -spec: - targets: - - ./create-react-app/template.yaml - - ./docs-template/template.yaml - - ./react-ssr-template/template.yaml - - ./springboot-grpc-template/template.yaml - - ./v1beta2-demo/template.yaml - - ./pull-request/template.yaml - - ./bitbucket-demo/template.yaml From 9ecb8a595e7d535d2881d835c262dbbc9d3b3e4a Mon Sep 17 00:00:00 2001 From: Mustansar Anwar ul Samad Date: Wed, 11 Aug 2021 08:04:13 +1200 Subject: [PATCH 18/46] Rebase for prettier issues and add Bitbucket to vocab Signed-off-by: Mustansar Anwar ul Samad --- .changeset/perfect-seals-burn.md | 2 +- .github/styles/vocab.txt | 1 + .../src/components/fields/RepoUrlPicker/validation.ts | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.changeset/perfect-seals-burn.md b/.changeset/perfect-seals-burn.md index 10503363de..8d3a1c7619 100644 --- a/.changeset/perfect-seals-burn.md +++ b/.changeset/perfect-seals-burn.md @@ -3,4 +3,4 @@ '@backstage/plugin-scaffolder-backend': minor --- -Add bitbucket workspace and project fields to RepoUrlPicker to support bitbucket cloud and server +Add Bitbucket workspace and project fields to RepoUrlPicker to support Bitbucket cloud and server diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6f28680ecb..fae8af80a0 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -15,6 +15,7 @@ Avro backrub Bigtable Billett +Bitbucket Bitrise Blackbox bool diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts index 85232b0588..24a1c2f5ce 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/validation.ts @@ -63,8 +63,6 @@ export const repoPickerValidation = ( ); } } - - // if (!host || !searchParams.get('owner') || !searchParams.get('repo')) { } catch { validation.addError('Unable to parse the Repository URL'); } From ff6fb715f6e893050569fc365fcdda53d472d00a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 04:11:02 +0000 Subject: [PATCH 19/46] chore(deps): bump @roadiehq/backstage-plugin-github-insights Bumps [@roadiehq/backstage-plugin-github-insights](https://github.com/RoadieHQ/backstage-plugin-github-insights) from 1.1.21 to 1.1.22. - [Release notes](https://github.com/RoadieHQ/backstage-plugin-github-insights/releases) - [Changelog](https://github.com/RoadieHQ/backstage-plugin-github-insights/blob/main/docs/releases-widget.png) - [Commits](https://github.com/RoadieHQ/backstage-plugin-github-insights/commits) --- updated-dependencies: - dependency-name: "@roadiehq/backstage-plugin-github-insights" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6568aed665..d7792ce7d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2357,6 +2357,51 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/core-components@^0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.2.0.tgz#d8c4f06b4b08d60292729b092ed743357802360f" + integrity sha512-pQipnJwZb7usojknLmY9vrMo4aSAo/r3ueGkdk1hPLRRP+8YI9XliMop2l558bHiLAqj7AjASD8ZGAfp8fpYog== + dependencies: + "@backstage/config" "^0.1.5" + "@backstage/core-plugin-api" "^0.1.4" + "@backstage/errors" "^0.1.1" + "@backstage/theme" "^0.2.9" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@testing-library/react-hooks" "^3.4.2" + "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^9.0.1" + lodash "^4.17.15" + pluralize "^8.0.0" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.15.4" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.3" + react-text-truncate "^0.16.0" + react-use "^17.2.4" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + "@backstage/core@*": version "0.7.14" resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.14.tgz#863844fe40bb6a29bcc2d297e42055633b0e886f" @@ -5128,15 +5173,15 @@ react-use "^17.2.4" "@roadiehq/backstage-plugin-github-insights@^1.1.20": - version "1.1.21" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.1.21.tgz#d43de9bc384c7a6e3d8ef13c77f4842e2ca69794" - integrity sha512-PctbxHscDmFK8jaM3MPUhoDw90TTQEydxTjiTQ+1+yY2KnlrEbxu5i8RgDk3UfRfZQIJw6JFDkEYGaTJIAv3Tw== + version "1.1.22" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.1.22.tgz#8d9a02d69ee55bfbf0c2ff84bf0579b372e6e8b1" + integrity sha512-KGLkJsVPKeF2hlBFNP0934mJ1kyB8Awx8bCKrZfoVw2mdBjkZqq5sB81WooMUBLpQ8QZJfCw+nvIQq22LA6cOg== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.1.3" + "@backstage/core-components" "^0.2.0" "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.3.0" + "@backstage/plugin-catalog-react" "^0.4.0" "@backstage/theme" "^0.2.7" "@date-io/core" "2.10.7" "@material-ui/core" "^4.11.0" From 82ac007e5377378265a0b1e5fdc0aae14e297cb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Aug 2021 04:19:52 +0000 Subject: [PATCH 20/46] chore(deps): bump luxon from 1.27.0 to 2.0.2 Bumps [luxon](https://github.com/moment/luxon) from 1.27.0 to 2.0.2. - [Release notes](https://github.com/moment/luxon/releases) - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/1.27.0...2.0.2) --- updated-dependencies: - dependency-name: luxon dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/integration/package.json | 2 +- plugins/auth-backend/package.json | 2 +- plugins/bitrise/package.json | 2 +- plugins/cost-insights/package.json | 2 +- plugins/fossa/package.json | 2 +- plugins/git-release-manager/package.json | 2 +- plugins/github-actions/package.json | 2 +- plugins/github-deployments/package.json | 2 +- plugins/ilert/package.json | 2 +- plugins/jenkins/package.json | 2 +- plugins/kubernetes/package.json | 2 +- plugins/pagerduty/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- plugins/scaffolder/package.json | 2 +- plugins/sentry/package.json | 2 +- plugins/splunk-on-call/package.json | 2 +- plugins/xcmetrics/package.json | 2 +- yarn.lock | 13 ++++--------- 18 files changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/integration/package.json b/packages/integration/package.json index 9aa4df761a..9ff31b9706 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -34,7 +34,7 @@ "git-url-parse": "~11.4.4", "@octokit/rest": "^18.5.3", "@octokit/auth-app": "^3.4.0", - "luxon": "^1.25.0" + "luxon": "^2.0.2" }, "devDependencies": { "@backstage/cli": "^0.7.7", diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c9219ddfae..2d8ce3657f 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -50,7 +50,7 @@ "jose": "^1.27.1", "jwt-decode": "^3.1.0", "knex": "^0.95.1", - "luxon": "^1.25.0", + "luxon": "^2.0.2", "morgan": "^1.10.0", "node-cache": "^5.1.2", "openid-client": "^4.2.1", diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 98cd862b0f..0e81d855c8 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -30,7 +30,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "cross-fetch": "^3.0.6", "lodash": "^4.17.21", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "qs": "^6.9.6", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index e46d154cf9..9304f6abe4 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -42,7 +42,7 @@ "@types/recharts": "^1.8.14", "classnames": "^2.2.6", "history": "^5.0.0", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "pluralize": "^8.0.0", "qs": "^6.9.4", "react": "^16.13.1", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 616e34234b..5c540d8405 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -41,7 +41,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "cross-fetch": "^3.0.6", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "p-limit": "^3.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 328798186e..780961966c 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -29,7 +29,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.5.3", "@types/react": "*", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "qs": "^6.10.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 791eacc9fe..7403a6f259 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -42,7 +42,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.5.3", - "luxon": "^1.27.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.3", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index cc45227627..f034213515 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -32,7 +32,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/graphql": "^4.5.8", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^17.2.4" diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index e849b8469c..0e60c73f8f 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -32,7 +32,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/pickers": "^3.3.10", "humanize-duration": "^3.26.0", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^17.2.4" diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 8040d6636b..829e407276 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -39,7 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "luxon": "^1.25.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 7e4a409c4b..3a7b67f939 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -43,7 +43,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "js-yaml": "^4.0.0", "lodash": "^4.17.21", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index bf3792597b..80b6e75f5c 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -40,7 +40,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "@types/react": "*", "classnames": "^2.2.6", - "luxon": "1.25.0", + "luxon": "2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 81f86a6f5b..c67d118d47 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -57,7 +57,7 @@ "jsonschema": "^1.2.6", "knex": "^0.95.1", "lodash": "^4.17.21", - "luxon": "^1.26.0", + "luxon": "^2.0.2", "morgan": "^1.10.0", "nunjucks": "^3.2.3", "octokit-plugin-create-pull-request": "^3.9.3", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 9227015bb0..f9394bf7a0 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -52,7 +52,7 @@ "immer": "^9.0.1", "json-schema": "^0.3.0", "lodash": "^4.17.21", - "luxon": "^1.25.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index a288fb6f93..c8f69b3873 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -39,7 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "luxon": "^1.27.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 4a78c1591b..c83c1fe788 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -39,7 +39,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "classnames": "^2.2.6", - "luxon": "^1.25.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 6e582d3686..8ebcf9a85e 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -27,7 +27,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "luxon": "^1.27.0", + "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^17.2.4" diff --git a/yarn.lock b/yarn.lock index 6568aed665..52f3875438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18654,15 +18654,10 @@ lunr@^2.3.9: resolved "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== -luxon@1.25.0: - version "1.25.0" - resolved "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" - integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== - -luxon@^1.25.0, luxon@^1.26.0, luxon@^1.27.0: - version "1.27.0" - resolved "https://registry.npmjs.org/luxon/-/luxon-1.27.0.tgz#ae10c69113d85dab8f15f5e8390d0cbeddf4f00f" - integrity sha512-VKsFsPggTA0DvnxtJdiExAucKdAnwbCCNlMM5ENvHlxubqWd0xhZcdb4XgZ7QFNhaRhilXCFxHuoObP5BNA4PA== +luxon@2.0.2, luxon@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" + integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== lz-string@^1.4.4: version "1.4.4" From e797e46368c706bc61535b5b2c2680953b640978 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 11 Aug 2021 10:40:44 +0200 Subject: [PATCH 21/46] Fix workflow Signed-off-by: Philipp Hugenroth --- .github/workflows/prettify.yml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml index fbf2519513..4a34ff0c88 100644 --- a/.github/workflows/prettify.yml +++ b/.github/workflows/prettify.yml @@ -8,7 +8,7 @@ on: jobs: autofix-markdown: name: Autofix Markdown files using Prettier - runs-ons: ubuntu-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: @@ -43,22 +43,12 @@ jobs: run: yarn install --frozen-lockfile # End of yarn setup - - name: Run Prettier on docs + - name: Run Prettier on ADOPTERS.md uses: creyD/prettier_action@v3.1 with: # Modifies commit only if prettier autofixed the ADOPTERS.md prettier_options: --config docs/prettier.config.js --write ADOPTERS.md only_changed: true - commit_message: 'Autofix documentation Markdown files using Prettier' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Run Prettier on root - uses: creyD/prettier_action@v3.1 - with: - # Modifies commit only if prettier autofixed a Markdown file - prettier_options: --write *.md - only_changed: true - commit_message: 'Autofix Markdown files using Prettier' + commit_message: 'Autofix ADOPTERS.md using Prettier' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From ce19580212676dc803887a977dbf5cf67512531b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Aug 2021 11:49:53 +0200 Subject: [PATCH 22/46] Prefer the API route in GithubUrlReader when using GitHub Apps based auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/rare-pugs-exercise.md | 5 +++ .changeset/silent-dragons-fold.md | 7 +++ .../src/reading/GithubUrlReader.ts | 13 ++++-- packages/integration/api-report.md | 6 ++- .../src/github/GithubCredentialsProvider.ts | 6 +-- packages/integration/src/github/core.test.ts | 43 +++++++++++++++++-- packages/integration/src/github/core.ts | 20 ++++++--- 7 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 .changeset/rare-pugs-exercise.md create mode 100644 .changeset/silent-dragons-fold.md diff --git a/.changeset/rare-pugs-exercise.md b/.changeset/rare-pugs-exercise.md new file mode 100644 index 0000000000..e4071ee59c --- /dev/null +++ b/.changeset/rare-pugs-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Pass on credentials to the integrations package, so that it can properly pick the API route when using GitHub apps based auth diff --git a/.changeset/silent-dragons-fold.md b/.changeset/silent-dragons-fold.md new file mode 100644 index 0000000000..df5c271fb8 --- /dev/null +++ b/.changeset/silent-dragons-fold.md @@ -0,0 +1,7 @@ +--- +'@backstage/integration': minor +--- + +`getGitHubFileFetchUrl` and `getGitHubRequestOptions` now require a `credentials` argument. This is needed to address an issue where the raw route was chosen by the `UrlReader` when using GitHub Apps based auth. + +Deprecated the `getGitHubRequestOptions` function, which is no longer used internally. diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 930c544287..40d36fb2a0 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -91,15 +91,20 @@ export class GithubUrlReader implements UrlReader { url: string, options?: ReadUrlOptions, ): Promise { - const ghUrl = getGitHubFileFetchUrl(url, this.integration.config); - const { headers } = await this.deps.credentialsProvider.getCredentials({ + const credentials = await this.deps.credentialsProvider.getCredentials({ url, }); + const ghUrl = getGitHubFileFetchUrl( + url, + this.integration.config, + credentials, + ); + let response: Response; try { - response = await fetch(ghUrl.toString(), { + response = await fetch(ghUrl, { headers: { - ...headers, + ...credentials?.headers, ...(options?.etag && { 'If-None-Match': options.etag }), Accept: 'application/vnd.github.v3.raw', }, diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 160a0a12ca..52663f55fa 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -165,20 +165,23 @@ export function getBitbucketRequestOptions( // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-forgotten-export) The symbol "GithubCredentials" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getGitHubFileFetchUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function getGitHubFileFetchUrl( url: string, config: GitHubIntegrationConfig, + credentials: GithubCredentials, ): string; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "getGitHubRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated export function getGitHubRequestOptions( config: GitHubIntegrationConfig, + credentials: GithubCredentials, ): RequestInit; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -228,7 +231,6 @@ export class GithubCredentialsProvider { // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" - // Warning: (ae-forgotten-export) The symbol "GithubCredentials" needs to be exported by the entry point index.d.ts getCredentials(opts: { url: string }): Promise; } diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index a3938b7ba3..c7ba6408d9 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -250,11 +250,7 @@ export class GithubCredentialsProvider { } return { - headers: token - ? { - Authorization: `Bearer ${token}`, - } - : undefined, + headers: token ? { Authorization: `Bearer ${token}` } : undefined, token, type, }; diff --git a/packages/integration/src/github/core.test.ts b/packages/integration/src/github/core.test.ts index 2dab4ab4d5..53733b40d0 100644 --- a/packages/integration/src/github/core.test.ts +++ b/packages/integration/src/github/core.test.ts @@ -16,8 +16,25 @@ import { GitHubIntegrationConfig } from './config'; import { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; +import { GithubCredentials } from './GithubCredentialsProvider'; describe('github core', () => { + const appCredentials: GithubCredentials = { + type: 'app', + token: 'A', + headers: {}, + }; + + const tokenCredentials: GithubCredentials = { + type: 'token', + token: 'A', + headers: {}, + }; + + const noCredentials: GithubCredentials = { + type: 'token', + }; + describe('getGitHubRequestOptions', () => { it('inserts a token when needed', () => { const withToken: GitHubIntegrationConfig = { @@ -30,10 +47,12 @@ describe('github core', () => { rawBaseUrl: '', }; expect( - (getGitHubRequestOptions(withToken).headers as any).Authorization, + (getGitHubRequestOptions(withToken, appCredentials).headers as any) + .Authorization, ).toEqual('token A'); expect( - (getGitHubRequestOptions(withoutToken).headers as any).Authorization, + (getGitHubRequestOptions(withoutToken, noCredentials).headers as any) + .Authorization, ).toBeUndefined(); }); }); @@ -41,7 +60,7 @@ describe('github core', () => { describe('getGitHubFileFetchUrl', () => { it('rejects targets that do not look like URLs', () => { const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect(() => getGitHubFileFetchUrl('a/b', config)).toThrow( + expect(() => getGitHubFileFetchUrl('a/b', config, noCredentials)).toThrow( /Incorrect URL: a\/b/, ); }); @@ -55,14 +74,16 @@ describe('github core', () => { getGitHubFileFetchUrl( 'https://github.com/a/b/blob/branchname/path/to/c.yaml', config, + appCredentials, ), ).toEqual( 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', ); expect( getGitHubFileFetchUrl( - 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + 'https://github.com/a/b/blob/branchname/path/to/c.yaml', config, + tokenCredentials, ), ).toEqual( 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', @@ -78,6 +99,16 @@ describe('github core', () => { getGitHubFileFetchUrl( 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', config, + appCredentials, + ), + ).toEqual( + 'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ); + expect( + getGitHubFileFetchUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + tokenCredentials, ), ).toEqual( 'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname', @@ -93,6 +124,7 @@ describe('github core', () => { getGitHubFileFetchUrl( 'https://github.com/a/b/tree/branchname/path/to/c.yaml', config, + tokenCredentials, ), ).toEqual( 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', @@ -108,6 +140,7 @@ describe('github core', () => { getGitHubFileFetchUrl( 'https://ghe.mycompany.net/a/b/tree/branchname/path/to/c.yaml', config, + tokenCredentials, ), ).toEqual( 'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname', @@ -123,6 +156,7 @@ describe('github core', () => { getGitHubFileFetchUrl( 'https://github.com/a/b/blob/branchname/path/to/c.yaml', config, + tokenCredentials, ), ).toEqual( 'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml', @@ -138,6 +172,7 @@ describe('github core', () => { getGitHubFileFetchUrl( 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', config, + tokenCredentials, ), ).toEqual('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'); }); diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index 4a069bcba4..49695ec5ac 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -16,6 +16,7 @@ import parseGitUrl from 'git-url-parse'; import { GitHubIntegrationConfig } from './config'; +import { GithubCredentials } from './GithubCredentialsProvider'; /** * Given a URL pointing to a file on a provider, returns a URL that is suitable @@ -32,6 +33,7 @@ import { GitHubIntegrationConfig } from './config'; export function getGitHubFileFetchUrl( url: string, config: GitHubIntegrationConfig, + credentials: GithubCredentials, ): string { try { const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); @@ -49,7 +51,7 @@ export function getGitHubFileFetchUrl( } const pathWithoutSlash = filepath.replace(/^\//, ''); - if (chooseEndpoint(config) === 'api') { + if (chooseEndpoint(config, credentials) === 'api') { return `${config.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`; } return `${config.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`; @@ -61,25 +63,31 @@ export function getGitHubFileFetchUrl( /** * Gets the request options necessary to make requests to a given provider. * + * @deprecated This function is no longer used internally * @param config The relevant provider config */ export function getGitHubRequestOptions( config: GitHubIntegrationConfig, + credentials: GithubCredentials, ): RequestInit { const headers: HeadersInit = {}; - if (chooseEndpoint(config) === 'api') { + if (chooseEndpoint(config, credentials) === 'api') { headers.Accept = 'application/vnd.github.v3.raw'; } - if (config.token) { - headers.Authorization = `token ${config.token}`; + + if (credentials.token) { + headers.Authorization = `token ${credentials.token}`; } return { headers }; } -export function chooseEndpoint(config: GitHubIntegrationConfig): 'api' | 'raw' { - if (config.apiBaseUrl && (config.token || !config.rawBaseUrl)) { +export function chooseEndpoint( + config: GitHubIntegrationConfig, + credentials: GithubCredentials, +): 'api' | 'raw' { + if (config.apiBaseUrl && (credentials.token || !config.rawBaseUrl)) { return 'api'; } return 'raw'; From bfe0ff93fb14f21464b78680529fdb5c4b3d767c Mon Sep 17 00:00:00 2001 From: Miguel Alexandre Date: Wed, 4 Aug 2021 15:26:48 +0200 Subject: [PATCH 23/46] Implement auth handler and sign-in resolvers for okta Signed-off-by: Miguel Alexandre --- .changeset/tidy-jokes-train.md | 5 + plugins/auth-backend/src/providers/index.ts | 1 + .../auth-backend/src/providers/okta/index.ts | 2 +- .../src/providers/okta/provider.test.ts | 104 ++++++++++ .../src/providers/okta/provider.ts | 185 ++++++++++++++---- 5 files changed, 263 insertions(+), 34 deletions(-) create mode 100644 .changeset/tidy-jokes-train.md create mode 100644 plugins/auth-backend/src/providers/okta/provider.test.ts diff --git a/.changeset/tidy-jokes-train.md b/.changeset/tidy-jokes-train.md new file mode 100644 index 0000000000..b9d0d43bea --- /dev/null +++ b/.changeset/tidy-jokes-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add Sign In and Handler resolver for Okta provider diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 6b262dcf68..4c093df8ce 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -16,6 +16,7 @@ export * from './google'; export * from './microsoft'; +export * from './okta'; export { factories as defaultAuthProviderFactories } from './factories'; // Export the minimal interface required for implementing a diff --git a/plugins/auth-backend/src/providers/okta/index.ts b/plugins/auth-backend/src/providers/okta/index.ts index 37abef181a..fd9ebf46da 100644 --- a/plugins/auth-backend/src/providers/okta/index.ts +++ b/plugins/auth-backend/src/providers/okta/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { createOktaProvider } from './provider'; +export { createOktaProvider, oktaEmailSignInResolver } from './provider'; export type { OktaProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/okta/provider.test.ts b/plugins/auth-backend/src/providers/okta/provider.test.ts new file mode 100644 index 0000000000..37d1713c93 --- /dev/null +++ b/plugins/auth-backend/src/providers/okta/provider.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OktaAuthProvider } from './provider'; +import * as helpers from '../../lib/passport/PassportStrategyHelper'; +import { OAuthResult } from '../../lib/oauth'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TokenIssuer } from '../../identity/types'; +import { CatalogIdentityClient } from '../../lib/catalog'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction< + () => Promise<{ result: OAuthResult; privateInfo: any }> +>; + +describe('createOktaProvider', () => { + it('should auth', async () => { + const tokenIssuer = { + issueToken: jest.fn(), + listPublicKeys: jest.fn(), + }; + const catalogIdentityClient = { + findUser: jest.fn(), + }; + + const provider = new OktaAuthProvider({ + logger: getVoidLogger(), + catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient, + tokenIssuer: (tokenIssuer as unknown) as TokenIssuer, + authHandler: async ({ fullProfile }) => ({ + profile: { + email: fullProfile.emails![0]!.value, + displayName: fullProfile.displayName, + }, + }), + audience: 'http://example.com', + clientId: 'mock', + clientSecret: 'mock', + callbackUrl: 'mock', + }); + + mockFrameHandler.mockResolvedValueOnce({ + result: { + fullProfile: { + emails: [ + { + type: 'work', + value: 'conrad@example.com', + }, + ], + displayName: 'Conrad', + name: { + familyName: 'Ribas', + givenName: 'Francisco', + }, + id: 'conrad', + provider: 'okta', + photos: [ + { + value: 'some-data', + }, + ], + }, + params: { + id_token: 'idToken', + scope: 'scope', + expires_in: 123, + }, + accessToken: 'accessToken', + }, + privateInfo: { + refreshToken: 'wacka', + }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual({ + providerInfo: { + accessToken: 'accessToken', + expiresInSeconds: 123, + idToken: 'idToken', + scope: 'scope', + }, + profile: { + email: 'conrad@example.com', + displayName: 'Conrad', + }, + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 9709b549d7..24ee14becc 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -35,8 +35,16 @@ import { executeFetchUserProfileStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory } from '../types'; +import { + AuthProviderFactory, + AuthHandler, + RedirectInfo, + SignInResolver, +} from '../types'; import { StateStore } from 'passport-oauth2'; +import { CatalogIdentityClient, getEntityClaims } from '../../lib/catalog'; +import { TokenIssuer } from '../../identity'; +import { Logger } from 'winston'; type PrivateInfo = { refreshToken: string; @@ -44,10 +52,20 @@ type PrivateInfo = { export type OktaAuthProviderOptions = OAuthProviderOptions & { audience: string; + signInResolver?: SignInResolver; + authHandler: AuthHandler; + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger; }; export class OktaAuthProvider implements OAuthHandlers { private readonly _strategy: any; + private readonly _signInResolver?: SignInResolver; + private readonly _authHandler: AuthHandler; + private readonly _tokenIssuer: TokenIssuer; + private readonly _catalogIdentityClient: CatalogIdentityClient; + private readonly _logger: Logger; /** * Due to passport-okta-oauth forcing options.state = true, @@ -67,6 +85,12 @@ export class OktaAuthProvider implements OAuthHandlers { }; constructor(options: OktaAuthProviderOptions) { + this._signInResolver = options.signInResolver; + this._authHandler = options.authHandler; + this._tokenIssuer = options.tokenIssuer; + this._catalogIdentityClient = options.catalogIdentityClient; + this._logger = options.logger; + this._strategy = new OktaStrategy( { clientID: options.clientId, @@ -117,18 +141,8 @@ export class OktaAuthProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); - const profile = makeProfileInfo(result.fullProfile, result.params.id_token); - return { - response: await this.populateIdentity({ - profile, - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - }), + response: await this.handleResult(result), refreshToken: privateInfo.refreshToken, }; } @@ -144,52 +158,157 @@ export class OktaAuthProvider implements OAuthHandlers { this._strategy, accessToken, ); - const profile = makeProfileInfo(fullProfile, params.id_token); - return this.populateIdentity({ - providerInfo: { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, - }, - profile, + return this.handleResult({ + fullProfile, + params, + accessToken, + refreshToken: req.refreshToken, }); } - private async populateIdentity( - response: OAuthResponse, - ): Promise { - const { profile } = response; + private async handleResult(result: OAuthResult) { + const { profile } = await this._authHandler(result); - if (!profile.email) { - throw new Error('Okta profile contained no email'); + const response: OAuthResponse = { + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }; + + if (this._signInResolver) { + response.backstageIdentity = await this._signInResolver( + { + result, + profile, + }, + { + tokenIssuer: this._tokenIssuer, + catalogIdentityClient: this._catalogIdentityClient, + logger: this._logger, + }, + ); } - // TODO(Rugvip): Hardcoded to the local part of the email for now - const id = profile.email.split('@')[0]; - - return { ...response, backstageIdentity: { id } }; + return response; } } -export type OktaProviderOptions = {}; +export const oktaEmailSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Okta profile contained no email'); + } + + const entity = await ctx.catalogIdentityClient.findUser({ + annotations: { + 'okta.com/email': profile.email, + }, + }); + + const claims = getEntityClaims(entity); + const token = await ctx.tokenIssuer.issueToken({ claims }); + + return { id: entity.metadata.name, entity, token }; +}; + +export const oktaDefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { + const { profile } = info; + + if (!profile.email) { + throw new Error('Okta profile contained no email'); + } + + // TODO(Rugvip): Hardcoded to the local part of the email for now + const userId = profile.email.split('@')[0]; + + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: userId, ent: [`user:default/${userId}`] }, + }); + + return { id: userId, token }; +}; + +export type OktaProviderOptions = { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + /** + * Maps an auth result to a Backstage identity for the user. + * + * Set to `'email'` to use the default email-based sign in resolver, which will search + * the catalog for a single user entity that has a matching `okta.com/email` annotation. + */ + signIn?: { + resolver?: SignInResolver; + }; +}; export const createOktaProvider = ( _options?: OktaProviderOptions, ): AuthProviderFactory => { - return ({ providerId, globalConfig, config, tokenIssuer }) => + return ({ + providerId, + globalConfig, + config, + tokenIssuer, + catalogApi, + logger, + }) => OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const audience = envConfig.getString('audience'); const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const catalogIdentityClient = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const authHandler: AuthHandler = _options?.authHandler + ? _options.authHandler + : async ({ fullProfile, params }) => ({ + profile: makeProfileInfo(fullProfile, params.id_token), + }); + + const signInResolverFn = + _options?.signIn?.resolver ?? oktaDefaultSignInResolver; + + const signInResolver: SignInResolver = info => + signInResolverFn(info, { + catalogIdentityClient, + tokenIssuer, + logger, + }); + const provider = new OktaAuthProvider({ audience, clientId, clientSecret, callbackUrl, + authHandler, + signInResolver, + tokenIssuer, + catalogIdentityClient, + logger, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From 7315ee644dabe19e55059d887773f0867288dbdb Mon Sep 17 00:00:00 2001 From: Miguel Alexandre Date: Tue, 10 Aug 2021 10:42:38 +0200 Subject: [PATCH 24/46] Add new contract to API report Signed-off-by: Miguel Alexandre --- plugins/auth-backend/api-report.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 9fb9e29a4a..af4375f5d7 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -98,6 +98,13 @@ export const createMicrosoftProvider: ( options?: MicrosoftProviderOptions | undefined, ) => AuthProviderFactory; +// Warning: (ae-missing-release-tag) "createOktaProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createOktaProvider: ( + _options?: OktaProviderOptions | undefined, +) => AuthProviderFactory; + // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -303,6 +310,21 @@ export type OAuthState = { env: string; }; +// Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const oktaEmailSignInResolver: SignInResolver; + +// Warning: (ae-missing-release-tag) "OktaProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type OktaProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver?: SignInResolver; + }; +}; + // Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From b8d1a989e1c77bc1025892c98648d5d0e28ed76a Mon Sep 17 00:00:00 2001 From: Miguel Alexandre Date: Wed, 11 Aug 2021 11:55:29 +0200 Subject: [PATCH 25/46] Fix prettier error Signed-off-by: Miguel Alexandre --- plugins/auth-backend/src/providers/okta/provider.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/providers/okta/provider.test.ts b/plugins/auth-backend/src/providers/okta/provider.test.ts index 37d1713c93..939981e81c 100644 --- a/plugins/auth-backend/src/providers/okta/provider.test.ts +++ b/plugins/auth-backend/src/providers/okta/provider.test.ts @@ -21,10 +21,10 @@ import { getVoidLogger } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient } from '../../lib/catalog'; -const mockFrameHandler = (jest.spyOn( +const mockFrameHandler = jest.spyOn( helpers, 'executeFrameHandlerStrategy', -) as unknown) as jest.MockedFunction< +) as unknown as jest.MockedFunction< () => Promise<{ result: OAuthResult; privateInfo: any }> >; @@ -40,8 +40,9 @@ describe('createOktaProvider', () => { const provider = new OktaAuthProvider({ logger: getVoidLogger(), - catalogIdentityClient: (catalogIdentityClient as unknown) as CatalogIdentityClient, - tokenIssuer: (tokenIssuer as unknown) as TokenIssuer, + catalogIdentityClient: + catalogIdentityClient as unknown as CatalogIdentityClient, + tokenIssuer: tokenIssuer as unknown as TokenIssuer, authHandler: async ({ fullProfile }) => ({ profile: { email: fullProfile.emails![0]!.value, From 4221759954305e1794009df73ae0ab7fe93f69fa Mon Sep 17 00:00:00 2001 From: ImgBotApp Date: Wed, 11 Aug 2021 14:43:26 +0000 Subject: [PATCH 26/46] [ImgBot] Optimize images *Total -- 83.48kb -> 70.61kb (15.41%) /plugins/firehydrant/doc/firehydrant_logo.png -- 5.26kb -> 2.99kb (43.14%) /microsite/static/img/xcmetrics-icon.png -- 22.50kb -> 17.63kb (21.63%) /plugins/firehydrant/doc/firehydrant_plugin_screenshot.png -- 55.72kb -> 49.99kb (10.28%) Signed-off-by: ImgBotApp --- microsite/static/img/xcmetrics-icon.png | Bin 23035 -> 18052 bytes plugins/firehydrant/doc/firehydrant_logo.png | Bin 5387 -> 3063 bytes .../doc/firehydrant_plugin_screenshot.png | Bin 57062 -> 51194 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/microsite/static/img/xcmetrics-icon.png b/microsite/static/img/xcmetrics-icon.png index 225ee6e147c1c68d03b90d9db08874d933c6de34..09f3ea1e68231e0a181587aa6adc2a545ba18116 100644 GIT binary patch delta 17961 zcmV*AKySbMvjK#S0gzyS24YJ`L;(K){{a7>y{D4^000SaNLh0L04z!X04z!YcY1V9 z00001Z*opl0neg$ng9SqW=TXrRCwC$y=QbB*Oe~%ovP~Gjc()&0?e5dsl*&bib|Gc zOS0rJ<22*(y)*aDdh^~|GjH9uX0Dg-j6EKEY&nlDD_J=wDvP3j5`&mC2_om%-RM~9 zydOYAjYf5-4nPAqAJmo)ed3={X}cLo#(FSyE1#gYV4Sft-xE$sTdnkKQk{aaMz& zsHB)0IDWjh6glp1Q&pJ}c#dJDy=hRE{nAH||7d8q>6ntD$xP!sk3A;QV@}O#G6RT9 z7B1Y!UGuj;C@-q4tu_eOHJs6~iV%Jg$MK5+m;i9#h!K>3GGa2sqfMAnN&$qas&uGw z_`D>G=R*U%XFYvweV@MY^RAu~`^6LqOf{O;G3oftc}?I{15xCQ|K}ZN*@E>eOjhd- zLbwea&o2c~0uUP;jSN}iHB|!uhNvR9%i+MMK2QHA{g;ni{_xlTPxr`3my&?6$=Z~t zGRXttj(@g)@1I|{RW4XlX)h?enec|&If1{HQaIT#W0*opOR_ATlLDT1`WsKYe*Wzz zn=c-C$UjChHHM^W6am$k&yfP5GbS;a2J%ialkfYb&^CYyW=Ves}W)ag1yc6I<0Fu$q9#StBu-V)Bl@w|+hM*Z=Uw z8k61iL&9-;*zjPOF+>(aXL_$3`{^tH@wJ1Y-Zn9Yuu2Sh)i~-5#E)+4A~BgF;*Jye z?q8jME2*rT|9!$4{}D`a8D=KbkZG-?s%%4GKu-A zBJYY7ck}oB_RwtwMHRm$oNzZNBQL{@1rdzag>|cU+}Uv5t)9!rUT;!@J{s*B0^pEh z@HP&A$W#+JE_v7g)$dH3|LTAJEpN2^kPxzecPM2$0Bk=g+ zU;M7E-db4k2qBET40GI^=6Fll{59JsRgZL?`^XLLDJGHz&$6PxlWgI+# z62k2Rn?emXFw9sO%mqufeed^wZ+2BUFuHc#1i(1f3}a1*YTOuyxFlvb(W}7ma+}GW4_w+U zm@K~`6lQkG7#JqPWOFX4TyjJ3!n+ThPoTWfkBN9U;j?rJ9C^;&{nL}>1;v$*0jOY? zih)5JwxX&PA@A^^zQ&{8C_gxV5ni>QWl+?0I!@!Ul?1h!}eN87m@$_5`X%JR_O+aK#Qu8{< zJ3)cJ^Vr~>yutMM?2<4r=rE;u!R9ENCtW%8`~@nB@su{+BZ(!p9s-X?&+hu+!7@kL zyniQz%wv~?fk7Y4_QF#CK*x!J=96w{m*7cOj%S?ILW<;_@LqBKs@)vNZJq(!r!p|) z9Smk$*|Oc=+}xKHyaWK{QzY=X`mVqIpG`GuxBeVVeHj?ihRNb| z^*5dT%+qr!k;?SXc;IUN?f-1Fx=QbXO4+m#149NN0I6EBWs9w_{E&ZtxHn=V8^#_5j26h_ z$DV7|pe6u_p5zO%c+Y)x1l&H>{R|A5!{R8dU%2Jo1qswwh@--h7FU5aCz)+d*K$g@ zWh{2Zz>qn3qs3LT>ZVnvUjD_UF`~**M2)Dh5=B@bW|2cGQRwl;3zpjmTWvIZ%{l@s zE|Os8N3AuL0$!1SgQyVrMGhkp2g+{VSw%_x>T3bK7-K?5*O^xEd7WuQky%aE2L=G_ z`G+@M0Jnl&!PyN(0d_11!px6eXIUL3vO)n#0lZ-jLm?gmejY<%9z#J6o)8a(4U4p3 zG}}unmu)TWzHqQ7N_nHzII_GjvZrx$AWIceG@nUPO-%!TC16uWvlx^DSB3?=aM6Y@ zEggYHpx}9cMsC^y;4&$wE091bg-QuT1qet4T|NOFBL;N&1oQ<3s4Cm4IVG!yH(Kk~ z?ym2?aBv_>d4-r3QDQ`y)S3!R;+fAzYe5wODP{ie>_@9dz$=$+*fZY`r%C;sz!S($ zK|o>R0TT~@xJ)vZ6^G#s6AXt0G<%F_A2FamAV5+Wnd7@H99P<~b^(ABm>8sr4P5b2 z3OugDDVxwnlWhYiWlILLIV^9Mxel`U|vBOL5ZM0DBw)L z8I5i;!ZKkVmBJBwP9H^2BSoUn6Q-F24f0NU?Qj*9QcBGr zv%jMWM(0CS<}XY@v)h0dE*Ibni#)IhYT5*Uj=UxS7PHm?EuuR3t^e9xr7 zmRa=N)H+s%@eCjCbz0?vp2r)SbCH!n&|I71# z1bdeouxFV8RwIFdCwTI+5d2{##h;T8xH4oySa#x$MegyyRwgEOo-D~Eg4@_a~B3K!FwE_T~cRKFpH`R(XMtm)0LNC z$6^C+TWN|LpS!Wa08yrR`dAoYiIF(koNo4*@Lrn>d+LW^;?%Uw=h)Xg&%z0tf6^YQR0~ zlLyzgE;7Q%6Fhb}$joUipI9owtqTTW=9P>I%yAryMk5FzkR)ls+;Vi|D8=>~5vuCM z#X-x=PYh9&R26u_=Ufu?oxMKq5+m+jm-@`ujq?q-=UOw&0-LjpMcasf39mI3AtaNe z$sMsojD8OQ3%j1owA~shHV17Yh=DQ^m6?lTmCVHLfdf+kZdFI*;P8;wq% z3GcKNVOQNC%z~OSfg`4~s;UXqI+2=hDD8l(RH5DP7>ftZc+zUUo@6NtNtG9$=Wxwr z0Ty^*?+O$4t~8BL6{U-L*U1cHL%y_4@1Y2qb;V?)E2^>+~$Y-jm>Q>)q60fkeK~WU61&e2v5>rj_Ok05`5HjX%@a=`DbVb-Ky5F=wwgiM$4DF-+)@-} zg4wJ`b>kglpdwaT8=RMUXUY2~Wr`;c2N7mIag0xF z!GVraY^~~lg&Yb@B5$UIjdu*Fi239UBm(r)s#)2$DJ$?y5nq7=b}l#JE7x16eEIUW zC1XnbF$-+PqTOpks}M$!A(%DNrt&$ch(+`;B)S2^q+;$8c&3o|mNjO4VXbA_Y7cB! zXavs#4<7U*%zWZ;EVH(vfE$7=XWwK_<@0$s0*~Q;R;c;EFG2RB&|}Ha`AuG);O13k z>|J4=oFF>IxW2&%MFAfE$j{8_F%)qFkYjS2Qz34)2u`R7E9r-iFVEkAfqtUT9tAEh z;BdnNBdn(U@YC1V8xRmJc;*Q6i2*=SBuJ{1bD}2toEXGew=s~uX{0R;o<1DF zx3<`S;Id8s+@Db(*tOgYi;>{rk63itEtKcqd9ZN(Fd#go#HEyif?(LP0y4~)ZI$Iy zBd`+5qRH=RBuat9S49Lxif?VT=M9N(S#5!T%fjK|5B+mCIz6P8pqXEX`N|=vvY0ay zI`$Ys2<#>GXcDf^@44VKC3x1A|Dym-w@Y~9kUy`M{B2xh!hIX8FwN2R#2f+EEplPy zie=z9PIG;4vh}~oj)|mZM$$70A*igZ#OlRWsIiUA%6^@diy0_@!;N7K`BeO0w-msC zY{(DNY+q)EQ6PBeL*JY-r#S-Lvqr@73I!aghRJBar=Na`P$;BJWh3vW0zwEXDk@N4 zUk^$tt}kgrU&w}_EX=OH)kTX)S*#h*CUJU!yyy@_bi%j^=Hv6a&&!GNH zw{ZaOSSeyfg^b9Gn6k1mY}&L59LG(6QrKvfjaJpzN*~F{TU}j^dGqFtlio-q+)&&K zfvEY}S83YNDHE8|nK_0|w@dirL0?{x`1%DV{N+|Vj0Q5R`D+p=?pPVdHC56i1uZEl znMC4fvZf0{2&$^8P+3_CLdXOKuP}R1YaPzd{-RT&z%#E=mEufm7=Ql6mv>5k{Ef{v zm;}toeofYRLUHrbFxFH_3F74`FSU1dc$HyphL> ziVAGsz8z+>Ib-rxRaK$7x*8mR$BmOVcD=F7;Kw3IAIL1(svva}dWH*yssg9lL_Bz4 z1cM{_rNsAdw8Ls5vqF4L0>$J?DZD`y!+sUPEJTxcEH~rpo9!?eW_BNcTLg+73&L1m zD^4J>1g@K4z;^+ZK)O?F%FS2t=lN9JMUJ;o}3azsUMyzxdHI&=u3P$*^c*45Q%-W;@4KZ0_z8%0JR211S* zJ)URXdf-%l%b_b_4Efynzi)S?OycG~3BP>Hi(b#?I}n^|6LF?p#P@a=VB_M<{J2}M zvB72I@uveLGpg=1M}XUx2eGDF8q3uq6NssY%=72ZhuLh#fddDB5|TJV-rCw)-Q)!T z6A5FHtsDI_oWPv!kv~fsr4*OCMEv3nPfBEI?vwE2SKa7!%aguKRViLM?uRdwIq5bn zHsiZH#{ye3pdUF<+_o%$wbkPIJZ~y~ZzA(L;uFU+rz06wH8nLUna%N9naKl(5X_5y zRC7*XIy*Cm&a{Pp@yjy31 z16W-prbNtC$qWFpr4HWWT~Ih{j0?meY+nXfN#*$eNw*0^LY<{QKRPo_2J2YNFe~{B zN`aFtVf^_+g0F6IpfF|uM01~nAHC$pNGNd+D-;lw>~+$ArX^-5D#gPe>24f+PA(=0wNzdO5swibHp>7DmUNC=4BkSssMK8Q?LkgkPwEJmkkUMXjTAx=GCC zM8)N+%>%eRQVCI=nJJb!&zjC!5+7;|AtZ39PT zs8Rb6bOu~dDX^}nWde!y64=a#QEeGSv%hS%`@nQo=j>9I;#5l*e|*=2fe{7GeG-25 zni~TiIqClu2*KXf)|^xW*|fxruWq)(X5#Xqp9GG7;)dD~R#obsD$?4GF#6FOB6#k& zfcD|ZdEzRBNxAOb0Vj?R%|lq>HIp{ty%*hh$fz;*?0ZE94~(3Iuws~<3arSp zpS;l|zSJeAL|y>6<2oDe+33jmI^Ru8E%@q|=?!dcZU|yaeGo=Ixyq&*tfG`6BoRDv z$bic|yzW>Fr38IJCq5lm1eKDMmL|r&N1&pA!ZbL&<1V)*v=a4aSOX{o9=Q1$E53BY z)a^;TbEOsE-dQj$8CzQ#0=S_r2#%y4T~18A(PKgq@W^2Udb|WQgY)YRIq`A-Qiv*_ zRuzwO(-3A_7MA`X7Avke+G#w&E$eLf%EqZr0@%FN0swgUfEWJI)C9I}TI|Plb&;fh z3VqpCiH>y#2_8FaM2|N+g<*O_E*u(IigkroVC6%)%;-|XAjnw#tZ9LoV**o^u@?Z$ zMuI!8v*VU^wrM+u2#Nprt_LA8cO*9O6zgk3xTZRkGBHz~D)NLlynK30d9yf2*&lY` z*zh7;Uw9ryPEMJ?JW)_$97bQbaJEK&p&0~@;EroY$vfS(tu`;U;47PQTP|p0eE?hM z2f%YwcS1&@&)h+RXOA0jrH{*cegUNf{hd00LF>IT;}E8AAKhFpW?%x0 zLOX{IODuU)m~Q(@3v4EWKfdcmD2vf)fv4Ei5Ww|yLGYY*v>8BJ3hW7Sc=e2b0j-04 zPLqRrLPbyktS)GTg$pHA-^hCoegr&GAkUzrj?|*iN~EOD-2T_^k@4{Rp1j%2+awTd zUS`49wmD%lk*xeifup#tHh}AE1KeO-)IixgC6_6gb&`9Kr*y4`JA+ioEI zUH}}SX`9Ge1os39UOsI?`w%ypvoIysi=a1DgwNc|QzEg6gka*rGbsqIXE6gap+^TJ zaIz_c-@olfAe=wxjhmNQ@%8P_jC|q^^ZeL2F8~9lCRN#F3LK4)MDXW-qZVB0<)=KS ze$*#+;j`i8@k)89;-XPX2ti6_Ac^(R!XbRy+&kyF+LW5naZ1~P@7fc5I z<#{&N`*2-tV5~|-;<;SC^SpmPYC(^OgF21ZtUp|c!$YefDv6cY!TV-XO*=hhWm)n@ zYxTmS^qP-=(fyrd2@qqT6UGmHmb8^U*Q zDnyYZ^}Q?oAr+4w^x%_A0R+RNQQ9|-k6_noJMO%}0f8qOkr)7f{_E}GgwbgOPjOAP zAJub0cKaWg2oaN4^Rs~qQHh{>I%l)^wna8RMpnXwP4 zE+3fb?&PLY;CNFAzk6#KLn$uk_lHzG^qw2P(4YrCt}Bf;qp5ipQU93gXfC-API852<+geK$TlAAqW=n&o+3J2sM) zT@0^VWWhIgj@A5%`WpgKv8Kw0YpVTmqs{4>(Lssez$GJpT88-1?7cj_W`m*&XGazz zpqM5kckqzFMJiZKWl0@%3KWG;ASiQw(?W}NOa=6wYh3W5Q#1Q&cu zAS#Ba^}*7Aj2d@>-a*Qc_#St>DTrUZJcz%#d7@A352<+SLl53L<%g1_^o~g&Fd4F! z<+^Ll=!&UF54ch93Se`?2n0^eXgzPNFC3ORymi5XGhIf^l(cyYg1&GmPO7W1#(ok4 z5y3+Oq3Vno>pIHH(tO|FJ8zWucvAqsdxPTd?<|3T*+3vG6c4{Qj1Q71ZvZSEZwL^ z2e_htQoMN5hV$LVQ9o{mUb7*o7{|TqAW;JZNHZoIYj*8+oM(6T6HP&U?^m5EFErS^ z+=g%6R0xM9yNkrSy1LNP(xPk4ZZZ=Z0D>a$!f6MZ2Mn{~nhnV%I6bm<_W8gHl&r4V zoJ$C%xbR_hy(R19rIcb|U;wSHt&<3w3PBTp5j-Swc>AIiEd#==8XH4$;iN#_RNe4% zufUAMIl-0{HhgPOQP#=Z+1ZJ<&&E7&NFq3R#e%DS2CyP6(;map5rvMVVliY5>lRw^cej^+ zz+uh4^0u_JBvjI53asTXhhz@>&)IQhTzT1mm~JDQmDHIgEUhu&?`|*7I(hs0`q0+a z7FT)W`FF{V4M`l{xoC?bFGD_nmDHIgSWFxYLU!+;-QC@2X=#BZNeO*ey^1WWbWC|o zjJ)|J!1^}$X4fuzIMEouPoC?;KYXcw6lP=QMDFeFMPp;5uIOnhWnmSS;*~Q_T*I4_`wss8JC7_Z*RxNix(%D&B@H-RLtw32)umSiHkiZcFl6! za=$+?Lr!q9J%nGsI*7qE_7+xE6+JyYxO(;KB%e3aMfoZvIMnLEgu4_EjZsjgpr`a zF4Rmmp=Eaca!g{1yiPF+iT))(g zZ|^OJ*~o6)xgP|%-d>c&FLJJb#BU!T!Jt>h_iimkiBpJ6-kzQwT)K1#ilR(X-b_{% zaU6&G`g)X?mxB-jlL6SadUPq`AKn;%FQ~E$HB(J!7KNS~5+Az~z;9mb$Mx?>Y6Hw{SK{T2qCDes~ab8G`6j_BP^+S@U6j|?!`2xuYE3mP3ZBzz}nPA zr>_L?>zDg67-ddVO3~fjjSCkpAQ%j4Ds!qPbL@MB5HvJ2psK2B5_u!ov%Ub|yR{_a z;_6zpMGfW4(oF(*=IaR?@ScH<91V^E~FypO31lD&6O_ZS`o~ z>R-Iro7G(rnHQW(0<-Hhr62^DSLnlz`MqP+BawD=IriAdJa1)xWo6puw`qkPl0xyD zm-`rrr!k79uc#fvl%c91h;0o$FeWHKmrP}~j^#KGb#-;9si{fo^CrWFWp?<33LbfT z5Wz4rr>6rfh?RHBOHp6w!%d4iVG@*#qRa83&DkI?Fj{x|)*D>-o7>A^x3Hl!4Fb$e z=)58?M^WqaV`F`PAB?<$*gC$M%=Z#PFn|7hR99DLg}l+&zQ%#Dq~eeJ`w`kH10hMpA75vGfvu@xFmqh;01ZW6>|WRj zlb~j_H&N_kBw$rnS2tz9<>u?0_-|h*XUQJSotWf;wMdlNy0yWoVWf_07iyEg7n;SY|c_Y}pt^hm- z{OX0CjFuvrR-FW2G?*W1>9ZqzYCWhd%p3xUcC>g0@clb0Q0B^yRQDZg3lJ7nJoH9CMgr-TcaaB5VcB#5LTT@`{~o!g5}~uJ zL8UDAFU{rlq7eaRErYEiDavdCY^d#m%_xo&RqHX)XIjT7D#7UvCq8R+LXrtSyXeEO zUg*JppjS?NJSi%L*RNn`M9xZ0tedZQ;jeEmA1$GqM*qp3)fjpHUML<{e%y`oU~AFT z3CwC@WsE{|2wUnqP-qROOweRxj^J#U1BY8&kQHv+S~_;ghd;d9msSAD7gF)efg$|U zf3@N7A85f39_qlyXGfr9Sk-;UH3hhTS4rA`b+F~0O88#78>**}4JdskG>bxKf^xeb zTN~P8H_77xt8Dna6dcZW7T}|%;z@A|N`Zr?Js9vv_?K_j!kQv!Jbnc~exeg6uK35_ z>-R`F+Y-Q6cbDS6-NoQC$maUewjvZ2@c8BPJ!#!s!p#j(p8Oh|UwI6?b9h=MI3GBF z!KFrDW&)?%3N1nGTF?TUQOd|@6P)gJ;#ga11Nhi~B_DqDSZ6|?*yC65T zZ7GDLkd)3vRVj{M^x^j}_n_Z1nZ$0tg8%)eHXOg~OZr`5F?#`#9qXL<{#e?7WjcK< zbyvXq!k3{8Pq7k@7a+{uc$u19Xl4?p6m2fG`LU_K4Hko#mdeH@uL_*$a^X~`3zEX= zG|?zNJnh8;&vs29vD>fU$B%X3d`mFtcbE(uR?N3$l?%JCbK$?=Rhf2lT5O#M_mf|R zGVGdi4$qt@G{EvWQUReTv-z=qrM?w5Ls&OK6DhEw0;f6(akRB)Jld>#(5b*@7kzl( z*)H_CCAj?xe){CZfK}Xq!V&CV@5H<^Q`Wm}=Q=0;>h=matZ5{ls2t?(O1Pi8AFAIv z#SEVQ@6)#)n2|U>cgd*Fdwop@tcEbOk~C6H))@W1&)-uiaJJip9=%Exi zdSL`VeNqJla4H4z@;t$=^#%CLedWl}a$@Ye(FK!{;DKkm(kjm*byi{c(Z7J}-Y38p zx~Ig3&WAEnsm}v5@+L%y)rXCBEpSiUQa)uLU-v zsH@O=Drg>b;KRmpge7jue-}qaqs4#si+c*xi0=`=8@~fZf*?;_vRBI`WPy z@p>ml0y2L8Qa6+|=0_Dz0p#uq@GkchFnD?;J+<|~(NQ;sak#8OY;9&P5Y3i2*a@^?0ZUO~aW{V=aO1H$Jxfj>{gOrHtO6c_;DFoke^ZOhnHQRY$R@o2LOTpcXH zq2@A(GB+)w#AO$-@5Z8OBkzAc(SeUndmza=e><#N$}GzZc)k+qIdDbP4jfJ4m{2uu9* z?oX@&N6veH@uP>^(eII_jJ!X4q67O64eLK=qQbi22pqc(0A%Vc7CNKW12Y!GVG3eH zZ4*juBN_X-v4NS!J|_-0S3;CYUi?-{DUMw5;1^GJ!WYW@V(?!+-GTRy4MUbyedkIj zZ0nE1x$n&>C2wvRoSp`!o{6QZ$XOarp2KPkV`E)^Bb?@-=6c@A*OsPyO!J@vA2(G& zrTNvjlmds(d-1=1+kzkdO#`f^?8iI9qKaQU(}}kZ>tEMP2!&nLmF7ng(4s+FCxgkUPKo z^<0x5|MpNT{>Oc_D0Lb#{yvXi!5?4h!oid7v}DY@@&X*Y4}dd=GrK`49H=aSGl|n5 zh0e7P9GSSDE3L@l!{&LHGy_(dD6p&o7ki6;aJZ!sVTqp+eW9uphtG}RKOb+$K!$-W zk6*!WpYOt3M}{FuRQI`<7B!;a=64|!WmZmyhK=xjd?N<_@C``a<@zn?Z1;0c6?l%p zVhCehb<-Gmvpdh*GUUXG_6mrKzIj&8Ac1pw#u60Lz1#$lcql$QRMD*4oIZMf1iyWM zt_y#CR~1TK`jt2&QT*ze4jeq`fh_C)4Tg$d6n^;?@Gehg zMDbrd2_fgJm=YlOJ2R+=EkZsU;j{#BZS_?+O@72%5}3?H)(Tcp3UqnwIMPz3J4u7H z1yOkC>o6^71|e$t5m!n;JVwZ8>oM?u=`P5AhrTgJfe(%kBN$fjkKddJyP4NPUKPK8 zz66-S4^)LD+fNAE%Dw;u&(3#NWY%Y!JEj;fTi zA5OC$Yb%=JGW(Fpdfv&$0$pA^4mZ~zEbCKV-Wo*l*Pnr@p#|V*M&pAE@DRd(6sEc> zpbC$^k8es#i4ULi;$I(Z#s9p&HYFtvNh<#1u{L~o(v#LW;$0q;eEn${%K9@5$`J%l zu7dleo1p|J{%*0o5`BO8I!eCr6of)gN_5KyPUu??Jd2yd7{uDjW|Zid=S?*#-Q}_4 z)23SZ!UlcQoScH9uRROXg67PB$P0i3IS5@wknrdwx7S}n$@TAp5GAc-kHhD@_}Sy_ z2&9m)^@}Gu@ZM23B#G+&TZB_k@~y{VsO-ylu2*cSf&0~6Q2Z9{rHJj77uWEe#Oh6H^-C)2dLy6pjac!)m3jn3LDxBP)&NR>zx_DOi<&aN zOOqf6V}}Vujxz5FMZuzTDBtlaKvbla>~YS6AN;WyfjAX3!lH^FKhlaf4-KYuT_Sy8 ztnACU55mp0=>OyWP`r+We^ctHK;Q4b26@;y#azgB6q?dm8Ev*3gVC=ofBt&4WRfN&%m@eOXO`c0b12O2!*}=GD>cEKh5YgaQLhj|M);7 zd@#q zo~=8pSPfxZUvm)_LntkRCL(yJy8wrp>qe_b>hbp+J`~;aDopc#TQm2M{2UCOMj*+W zV&}4xD8BYX5Fx#y?Pr_(_~C;s=y#7!F?oD49(cL~2T!CI)#YsgIQPAml|Bf!R%76i zFG2Cz^_kn973hEXo`fXMgCMlt#SF7YdR=B8Zmd0rf-&XI);uqzKLmDhgL0MbX+%K{#1=Us9Fg$XPFb`dAy9I)eDk zGaY#Q&=4d^)%{(ZB?#9YZ^63uTxNYRro6+??}OA;mev@N+AGlir+Xm}xu$>*Ih{s| z?2R-#rjd-ijFshA$H<#rlv(>2pxf)fvG!^NBz^OwoI!$r>x*x|y7~eLuVzNxfB>Oe z0FhI_KNC)cbJ;0KUMIX)mV>JLqU8~vxNlfQdv8epdR~k!$amiK0jz7!g9w?~eNulh zhM&C|;r7bRzNfnsgHPOn;`?5LP~?f~=4|hbl*{c!XHgW|ZV2GI>I*2gcrx;HwS1y3 zPXP|K%tJtbXSA8O22gbGtFWy*oAJQq0z8B+AtmLVWV)I{DBJiRTq{nb6`fX9ps6F6 zUbNXL!nN;xIJO;0+ms(4t{w-5p1T>rD-D@1uT(gM8t0+^!FwQemjaQz^%+;xZWiQ1 zTa6(sFKt1IbvWTXul^!*1U=pY9BHdXFf8cmCt{R;PI`JerXqcpI(zRaF-T|+bhuj z;N4?8KxCP`xlW~=$=Yk=Wvr{dfKr<$B|lzIie&$Y4If-x2uabGR!)o(iod)c_Ulh) z{QIQdV)Q@!C5XMn5POTjnS&_3_LGcEQO*>9M)}sa075a+v?wNn?&tj+40SO!kQx~hvPwhrqG zP^DsCcYB=pv}OM2Mq_$*JFo>%bk|$3uDO`;#S67mq3@x)pm=PPs67ib$U!jaZ`_`@b$=fv3pD7y0<*snX2akM$qT!n!@-wv^}OuNF8!lVDt zCiH*0CF2`~U>`x%O|QXP+nSYYV_kC*g?GIT!8wu{c@-X>1J`5twe3)+v6AP1H1c9lGu&K_!KZG4)Ki@B z9H_!!`0N^(O8VejoSEVaHXll^Jp|crM|hw(eV1lII|^?*07J1mGxEwD{HK<}{pKc6 zX?kjC=L2S46xwF+V|~?G6lo}buiof#?6E_BJC3#0BPi*MHgmQBO74FZj?KriLf-yA z-v+U>G{u3=qxXYd7(TNGRLZP=gQdJ1RlD}XQr?|jFtNH1<$w7i4CR9vN1J_zS7YGM zw~VjpovYB5$dT_Ek`_Y{OG=w2`MlYhD*CLw0p6fR*Etb`gyOs3f^E%zrHn6@*j0w1 zSGGg!Db{rkifBOJq0I<%*JK_PEFM6`_IDvTyt>Z8P|=5yd-sEPjbu#T;Q9F&esd#K z(J-qP*5p)yX`Xu#HlrU)ORk{8-mfd0DwPbZLBAbGTNl6`u;@A`VvtaB|7)>2LFeUX*|51(+H-Gb1l0@Sk3S zfv5M5Ei#x*%UW|vU?uOP%`3|;p~BujiG#e=;cV=!*+iE&c`-SI043c)#-5Ny2_n(4!K}W`aE8J0r-rs#0 z@=(F7EGv<7ADHHUq0?X>BGy-(m=v(ehTl6BaNxtMOA(UvEeYgIVU&JlKkV0Lo}{68 zZRq~>JrH|}vf6K=*MW}xw;LsYv2SiW?5hWrC)mu=J}l&{|$Mt06o9` zGNb|L?5$&*)7=U45CSJ-ZRKecm`2i8*?7ke1swRSbpZl@qDfz8nnV=d`XOxVG7nfu zy+!Ex-Iowi(zYw$5624BsfE6%i&~?4gF8J=znYygh?;Ss{bP_VO9tZt)9B*HUk&s1~XF>!O#djQp>!!n*D{oIR zdLOwJp{7dYIyr$-Ak#6_!rPs6yn9fl=cnMd6RT4`AfOSW z=qjpzot-9ac2Z8hT)6%~q&GXQ) z|7O(QvLC$OO~VLr(yX6%;tPp2!M$)vy5r&+b5f;KICV9SpvEr zLlkC-yq&+e8==PPyeL~Vd}alxETC%Ffs~f|sV*5(g_v=@L#lEZcxF31?_UF|&b`Hd z2_avP-c6%u$^;IC{Jjo~8YYA@PbqnciZzv|P+}cO7+gzrqp|4!GaM|yv9{$1>0Ov$ z5>b5HCs`t|5#7JL1Hmgbc{P}ZFDwCPkWjh(V8RNSs$GFpE@q`z>CkH%;Qk;&-Z==W z(kNoaKBrpbO_{(%4uuJbJe4XOi8dF1$02VzO9La1BOxnJbu2Z&7hQ0AVVpWvJwUzdh>b=y?!I8Ja?A`hJD>> zZM&FC_fxOCWe8Lir;@yDD$k(Y(Ub7zp3Z!)CuqgV&c*PCY`T5kqT4@)bK9wZjEk?S z0V8@J*#rNXhWs6`6zD&`8eIoAO}f9RHU&zV1Zv3qg3BiM;xBxJqP<5mSKeVe zdLG@2z?p`e7GKK>6b?gYm&53P@}gw@DG)-TniVJ&St9R|rRaI+W~dU6xeZGF0~g%6 z3u1XZvZ_kwIG$US%b#K3Wh^SWicy!me~QiVg$$2WjCh2dIz0dl#23{{|+MTr#- z!KNzoJ+%|6C}6J77b#-EErtfeF&&}h_fGPMQy!7B96V~|On2t;ho~w+ueTI^BPAJq z&*!nL&j(phGQ$_P>i;fYL8!ABUBACIb20=?2sT%ypLR1{B$jw|zPlCf<_2)iP!?_q z1i_|i$ZjhbfRL}fMT4k+83(Z@{3qGpbKGcjN&qqB>`tWe7z`BU*Wi_T1kN_h`9Vfy z9=^-QqG0+kc`x31PL! zO99Z7P3Wti{G_+=%UfG{o?`@NV8|MQ!-3)MqkYg`<4==W$H@qP9QB1hS&;$P(kV16&xolW}VN#kQwCAa9``0U%w-ThC&#fn^4A@Jl3R`$LP-#07^=k zSdquR#uq(uXz;CnSy@$#HP)uM1-;G9f#EA&jPGa_y}Q-;Yc=NegeD{-ZR~r)y@x(2 zZ@8-igw(KmC<8;f#Qj*;Tp?InaWt!0;kfkCJG`nsAs&;H>Ei z1f>clN1r2q=CT;|JknH5j6%!t2po^Z+*srN1IGiU^>-BWf^{9cEDQ{(s&CuXmSg|^ z9#y4i<&|SvEOBEMI6`9%g(ZOH-Ydjc4-(G5<^wQP}jNNKks*6 z8XEIql_&y7o6eF(i#4IuR0&Mts4y3FUkuwz)^?kJtd+X}FjJa=ApwTk-+R03;zOq~ zF<_M_aYIaELhGs^rWGYqu6*`S@5|!wtL*YHFvN!x80c&}`^!TbJW}E`8509ss>W+I zUM&-%@tTWK;L)hGqHto!cfnbH?RF5pkXitnmm{|Z4|4(@qFND zvv~r4v6B&)Abv!8oX8>Xm9Su~xM(eC*hUD)$}BK2M98bkQg7o^&keM`aut*3cePCC z=m@kJL*i(~RuMA~oQS{#@#lsK0M6U<$$+hJMX%XfxjC0rI~W*d40%CSPxr?kw4MIx zAq`VlLHvM~i1<pGh86)p20Gt4U0A-^Bp4m5K?ujLBg5SD zc|n6Cm(R7G`j0olfev4c^2%|{XEBZf%Ni!Ms)xYwR5(_VM?Y63gnVs{mVySK!Bn_^ z9$+M9m`i3eXn3UMO6$pgeAu%Tg zdc?u5w=NWxZ#D=<*DA`Cm|<>6FAa?}U1<93pPmi-+DA0}UOC2>m10P%rMyy%a!*+7 zkSc+ZK#jEtuBsU5eEW>OaK(thT(**b048>Y8D=keRe7ZU&_}Ije)Oj3@AD(^G?5&~ zY>r3dNSm}eNe_XE?xxBZ>UirCZ?Ijmx)yJo>fH?)7^X&eRb`<0g_m1T{rDqA^ott7 zs$>M#GMy!j09N!AR7wO+7R&fw_TuIBJa4ULmz-g?Knf3k^mLqi z;PJkur!Qi1##F5ON*anQCh>n2OwPd4ENYi-Fe{ml3;-nTYYq-~zWtHGQrTy;RxBeN zZ(*08VWvV=rLec}(}T@N|G(!0?sG%XPI=Tam7}B1TEvx;sBhc_{i&)j5*_1?xuPROD=c8)hTmsyHY zV6EvQ5!#Cx)C2@h8l+03!jo1ejj1_5_FNciMvSgfiaeUeo*Q}IVJ%zum9>RcJH8~C zidPcC32b;U*7<|KQ`lhTnHG4@K{)Xuf-}9!?o|;HOrU8 zRy@!x|Nc9->&m%*xNR ztJ~gvZ`QrAF+k=L)2-7CkC%FMd7jC%bo{^-*uRUDM`d1Em*46qi#cQz3r!~a2CnZ- z{$Kt@VxIXfFY~J+Q*tAdOLZepOLJ#eo=adi-Y8jgDk?NA)KI1SF!RBO+~RwkkI%NP zzsr6rG_)stx#cbW-sy>=`o}Hf-fFMiWSO<*=A1tMIbHfTEAytc2}vi_JUYqpZx1H} zOLunU#>FpYSyg|0wbae~t_C2SC1tG!uFN5^=UH&{7@b6r?*-~DFA+}ljrottbP zg{z+LkdXP%_UG-p10P;GMwYZ2R;mlF`WSOH!)pptwxESs#M00OwqcdRuJepPH9nqb z5%|?hSIAws{-%Iyf-bAs5dn)&HBNK0c(on0FHBo^^`L>brR?*q*UaW^6g642c})_h zj(R}C+D9dwHF2Sj@2Jh$Jz;5|f}!jpw~4NumSL-#cUV00d}8QSquRA4hKcF8w$p?& zstGf0Ke;NkSmajs$K=IFH?s77Ef!U|kt%S@+Aw31nUs@Q$L#u^BQbOSOW(DOxzaDN z$Yb>pjq;L~*+(YK=>AnLJl#S~T}S!T9c5i10hO@Q$1B%QWAZNSknv4eo2e${oHFad zolQ;;TvYnPuDbj;zhqb=#M)<)w}yGkBzGZA1LK8~lO{%;e%|Z;;KtKhV3^K*el;po z#P>#os9&09%H5W}+I7e3CsKm5vYiETBG942N$+io>rm$w(spUTRa)d0V9l5yvs>UWIzlQn-mxxuN5EB#NBqt={Xo$92nWT33i?Av(Yq>%Zev&_ehSBLV}hp%4Bn#g6~kE`8|E$Ycf|jU$<~k ztgb@RcU`|H-4X3Ic>*Q#h2XNTKqdZZX~n4} z43-9wJ@;k=nP|22{h~>FVdQ I&MBb@0G9_UUYz#6C004k3`&sJiOMCk7c!m0M7FFAie`%1-BtQ}X zKy?h}gCWYxHI4D-uOI-xg8=~W`wjryy`cQ|000*b0N}s?01!+80Eq1~nw5oLZhSY< zlr;r`0L(A&D*y@t9subDLU`E$2xNeN(q16I7X5dTYknuGLz@CygP?}(K0r9rp~7rb zc8+F1&JQ0x06DmTTwH7~6l_iqTW3ReHd`mU{}J;4%8@d0GIq4Eceb#zrTHh<(8$ik zS%jAMpQ8V6|I<%r3)BD8Wb5?5X1y2){C5Y)$<6`%uk06A;eS{`MMn#h7tR0Vi*gG8 zhw}f2`(Hi6z<OaFs?F;x^p82I1ECW=9-zm5g~hy!G$BvjoIj$6^3b!T1p zn$1wTk+3}II$mEf5XBuFHg5jM3cc+k=W}$IrLHJRj&Ih_C{h!4PbA^^oPOT!E+uRKP z83b7l8BNe^xDUe=5ngoHfg2l^?ZgGAlri_gBDHIi@Xk_jx6^1%z`IKu8N?mysh8 zLECcQlH-flH_Vjf5I@la-h3g)yZ??fX}<1=(m_k(-K6#`e>5;L9inxT`W8Xt)$~<9 z4mTYxuWkK=d`f$ogk%9a*5@DXH-)(u#dfV9O2eYqZSz|)n~r854xg7bv}9P>@bz)EELUgLIGr0awv0%}g?4kR zV?{`M;9(l2|>x52U)@ehCzl<|@x11|O>$W=X`HuhJJD*|%cw9SnNb2sHo=7;5F6xVU}StElD* z=Llb2X1{Rql%Iv7HVACv1)wXj`}E}5mWaVNi<(7PKL<5SGrhlTf_OhnXu;L>xgq<0 z7T9`hW%Y&z(*VJ|8WTnQ()EXiA11TwP1oMC@~VLEB~3(^(SlrdK_f3f?DTO{t%jKO zsd?n&PDf#WqOqXIO1d8b{kwOdUge4B8TIp12luM&?%6B*Ak=w9aq^g43`E_Z8)z8D zuez&UCe1SN&NFf5DJ$9ygJ}(a7t#A$&?1B^sx-TfyIkwy z(=LvmIE92rzO@kxT@$bR+1&FQw`iRf4=%SwpZ@y#y}edF>7WusC{ z^_7nY8;Bb4jMW9&S_|Fiq^|f5omnhsUMACIJsUMN;`q{o9lWY}${-V`By0BUhJ&M9oGgEaB)AgKwUckOmFaVl7Jk; z<)~Y~n=l}hW|0wwuVFrvh~#lry(7r;z1a*FRD>J@-3#U?pr61|J^XC+9gN!;rfmA{ znK*JsHlk0qFs4&KLg51}my)}_Q^FWdJ#5x6T2Q&tHQd_i@}b#x4ZXBwUa=V)epdUy ziaz`bT^xUwbt_nVY8GqSoqwL3jS?$pZ&a5c|K$r(ezcICAyFIP9Sxovw#8OCu{I| zdLp2O#QuYBO*%q4S?ZfNCCmi6kP`Oz)vgrZja<9#HfeZqPzLgdL4-~-H+P;_ zwb_faqir2HJY|B5tiu6p`wEAUZtx~QhJmc=h}7aL^b8;ThN5q1zU99Xe_rPL=NaF{ z0kS=_HMq>QV5!T`F46OTdG4q=(?}Yy=pH$=j?gmfD!aRlXOBqQ`rGO$)`))&RF+;z zRkWqz@lWUVz4qw+CsMVJjlG{F)6_m5Z>((T0P5`ooX7Wdea)il_RY`Qjx|O9xQgiw zoVEywMzu)-UJh=Vt6;+Vf{zME%DSdT$+x1YX&FlN9 zlEVKe>&QSp!@#ggwtX7bj;3#^-cT+(5g!=L15wrJ6pi$s#h8Q1t-#5JWp_0M(IjDg zx0<~#!HB;*o`KT*PT72OT@}=HR7o*C(jUQS&L5~7i?;TMecd|BrJ7gwOEwuoaiq;H zWj5G=?zl>lS)XO;A2R_S6H`TnPV;3a{czIfdqk+a6jC5!&Dp!D4tVpZs-||mpixa_ z_~qOO`u_~e*ev9m_`cr|D?6~~joYfo$a_H1+)sLgAaZSz7(i0F@v>PxBQghxq0zG0 z93=#W+8F1XL8=840(tw=HX*{GTTWl^BVQKt{`KMj_3!aFlm330z5quu^#^A4;u2fR zzkknBNVNI2n#TZa`T+ovj_9@@oE%-5cpby|ylxL8EG86gQJ1LFw37+JjRQxzT*EZc zw7mTz$l|?5GJaimGDlO#PD;D6fz2{Er1dJ)Ep5S8cZ= zb13o`Sr<5%m#7$KXydHZyw z*3;cP_7EcL#`0){r2z}XUt?v6Mn6yckdR9JllKY9G!q4ns{6Lzr*^fIY1%~jYB5hP z=^hN_?fxa0po|D9nxdpas`_f@x6m8aIRfZY*?B!tGKf1f#&-bUR`$-0AR`^4ToImG zvaI=vS6v3p{9{|GUu%dSu|XroDvO#|NHrZnEc$wr}w#J!r>yUh+wU42KZZJH)0 zKH0rY5)Ii~$q%ePsZ|v!i0cWom;y@Dd(BFPu}ERceKJqsbwab+%%i+@uHeu96o3Wc zkr;|h5Z^z_mz)3C!K|&(Jho98AXpF-1rR6+IzJ+3osUVluXUA@ILb9sAe>~>xaec3 z#|w0W3Ur{22v;(7kG_gY580EfjOaZ2$xg+ccW;wggpBdUiug>EK1TAbh&c*WQVqMz zVqKL?4ashV(rs)0y!(PIUSzMCl#URuiDd4t1p_bI`T>^df16Z*u{N+LrKPbfHw~;H8q@gU>4LB5Z3+6Z zaVjVJ)b(!$Q58vQ@JwILWEf+8uaT6Ehf)N&qrUdebQZoI%7v}U`%3P^%P`Tp{X_PO zH1J@Z3NPFDw`7`im^f1A8VD4Mg5y_5_5_Zee>Zg)3+o)J%ujTWb4w)2MIZ*>Kb;!K zC56f6{9Kg@f`1XZ!@oP|HgV9jCl|xbW`57S?=j)ocOetcAb;%Gcz9v$Aewj`4jabc`*&w{bDS zp9<_UE4?NMymRq&Y_mY@`S;0}y$tnlSg1HgZW09_Yi$&8rU^YQ6s_LZ;WX9G^d`2oO9(}dhW=>d!Xv(BDhhPcNeBh;FoalH(U>qU!FsXgJ z&{Qv!w9DzPN9L{!x7z)vZ|h-Q8RM12kW`;D-iyHB>ecqcR&P$G2l@@!7`06M`?#2; zuu#MA=6J(D-)C3q@hqvBa~6}4%^@zax(|XJWfr7WllRyPf-5H(>Rl9OKBPOzs`dxR zh_={`c^h&i{%XAzrC!yOds$Yx&a2wjjc?mnMnCbhJ6eM87_XvYXD7Njmk>T{&h}gl zED~K=(y040MS6&}l6%1)!nMd@+G8@&4;+bvIivx=yTW=7DK{WyY*gX5Xtzct9z9nt zL-4TXwdN6^dM+rOx~Sd+Q{74mR1W}xDTy7BMp1gpjLffUTXCx6PrH^n37 ziOJS;K|r;8o4Gd}6X3~uSZu8#QG!V53i*z98EIi4V%~@8?Y06CyCgFld1pp+_jg67leChMbO)!)56tUxu zkpwGVI65@De#&)X15cNQw{FDzSsF)4^~+_;pGJhU>T28&&*E)!0gadUU+w!UJ6v< z;0YlM3D+7ix6z5u`l5=$GFa{=r)NYc|I42~mk8lB^8iC2#w;CSNeJ=>;}DvfVl$+- zwN-I}PwfH86iE0{pt1CZT1jB`@F#B8IkctXBT_Y0l?VTevtrTaCsevz-{?)b=?#y7 zY6`|2gbdz(jGY(y>(8H8>1Xiuf!B=|0M>eXA8BAypV~6wW`Y2vO#xwQI^*JqT=^&S zR!?UVHTmgN4HwP=)y8Kada6eh?V%NDRpxr#A4!&si0uz7a_#-xL56`>oaQ7I8QN*? zW$5Ti%AjgY^K~?QCvwCpO2*%PT%in{(oeaIUMzNF?x%N%lg2CG)Vc-XBXahp^?p>l z^A=W_?(*7^bS1Fe)b(DBg}*fHWGK}b`g1>BP&$}D$HgB+n<;j0ZiSWCWjSd#gXq+R zh03!FDOt=E=4lrKT(MHn2VJEb-vauY$I!oysM!tktu{}9uI)I)3{7ek_<;`aIxbuf zJURK96afZ4zncxe5Ni-_vx~k^Ha3+I072ZyRcqDe)0VpdVY}-3T41#_V(p+y@Sg{N zbBrkkbP$D%*g+?=?N4nzYy{2C?rdd*o}~}g4jdkKdfNqPcKsDQQ5xIFL(2<-aNx?d zcDe`w!p|n%HO^`=&5v&E*7U@7SAM;h_m2x-V*x6Pao<-IVG6y09OPCT+YrXO2kihS z$95!Aw9Xv~n z*y+NsA-I540)Q!(fa#XW^|z>SqebP7bnhZME;HTKf{3nHes#?Xgxe96>J1gHle@NS zCyVcV?dmH@rn_NU_&OebJXbCIUsqnK354)g(dy!SZ51MniF6xqDYOY!xb>98@ ziB`Fi8P2G7Be@{w{nr!1kED>gUF*MqJ5A^6AlGSZ8wl93D5KEmgh0CvD(Y^bGwF&N z)^8Fm74cdPakJwM<3!Hb#WQx+OydUywdPglgx(h|E=EEx#iMrl{v?N^85_lv2clB) z8jOe-Q-R4su@MEX4@!Z`ps-BaEmFf6p(}@p8u=1WmCOX39O5w*&vH&qWNHj}+m8K4 z7dn7lUKmj7Q|ulew0M(Zbi!gV)zQd_yf(-6^)*XELDrkcaJNs7@EJ*F7U-RouO{P4 zonSVG`|K-+}#q~8qNH7RzQFu!Rdk{aG&*MoX8Op zj!rMc5FMN6WoONOytqwwMCy)RWRqK-I>er=x!1xUT|;`4{D4{9vC`R|_aXibG&Y%P zxJ2mKp6crrLD_ot)6nQXU*ugXZ!e$>+uwg}X)lYRY0|g{#wjQi-y6)}o#hv)Y?O!;eH-28CVBM&Q z_{L+%)&_ey#+!XjIo{ULS20@+|}sW_cc~8P)|Tq^2P3l1zR2 zcU6Qsp|}(_txd!TMX1%WYfW6TU9!k(!X1fl zcBsjJnKf<7w$*>s#`SeJaBQUwPnNZD0{VyoCWp6~Wx84x+ZZ!Z#$ zd$#_@aeCD2s~~{UJXbBLZ46G09x&;0rd1+U+>YiXZf~L83H~qAE4Ud3kfB^Z=})+J z->|XR?s}hDlt8ft1#p$45K^2zxi}r?nG^^&?ycJgf{`3%#ciX)+U@1y>H~-6y&V&Y zmv<=Ta?_GMt2oz6qBL-f%!h3j>y-`P`cU;ll{;C)L7#|;U6o|1UaM^>q|X3M+YS~? z#!+((-;!99MHaI5$cYRu?`OAXrZT)fecE|iuPPV`si{(}OK`LHhF~gMuLs1uXWc|Z zg(;xI3T}Ol-=$C$ZOmArU*y#1Ij=w=wJwM1lW8PtJ-OX#p#4>e6*F923wK5 z`kjhFRyN1%`^|&=Qor)df<<39Bkzd6W-|< z7CqKWX4YS>HdFY4qlGI!-#nVGPA$2HHQRv?@(zimT1EhP9f_eOiNr6FN4jlu*vr?F z$HrMK3_n6#LBS;I7DZnB=;4ufT{-~E$C#yYxeLE6QyH`&>2UbQmCsy4MC3B>M0D8Q zdf~gOVd|dQ*d`*r>RFe)aL4if4ewr(G3IXes=8zY$185mYyAkeoY!|BVy2xe@V}G@ z8fJK|9vEtiCHvNVHjaKGG$_cmyo(?P&eWcsEVm>CXm$wsK0P@2O(PLn^qo5`elpiG z#B`B&0FYB5SxJ5x%4WdPKiD@F^S<5|{b+#T{=9#yZ+W==u}zTlJEY*g3F#xxUd!1~ zC7%MFtNDgllZ&&FTyl))s;>RHGp@IFVlvt*_0`*xW$sB8#k4mlfvTO%3e~K$!wJMM%2F zkR^)Yx{|%umNrqV;eB@QjBtlQn~WtS`Wi1P)94%~zi7WR)-xYA1SvjPUJ5$L&4#U3 zj}tc#sWhE1(RSRIn|WIJ^|cIO`B>sNsF)5}@6Z>#vAU9385}v`nSsDGIpwX)?jcw9G?=60UoEB^Ie;R=UWNty+Qq*T7ctfO=${xm|VTK&@(h&@%8+fEO^hc z1JZ{H&xpHKoXhvy(2RA1x9AHb`P)kfwo@Wcg@vBK4|ru4Kz_QTV0nbkRKAEW(R07| z=JU7{r{cY$S%-7sY??s{#MWwibiSZ96Q3zINxA9m^V`tGxci(Y_jMM2-`#GAk<%G5 zJ3HF<&WS3;QlLId|BD*6K@;d6+Sjri`1W8=u^f`=MLgR|F5-ND2RE*>9^9X?%I$me zGt6P;){?X^u(M@Mh94f7YSz!9%uiw>Pk`!6;`iD8&TGj=bR$P@c}_YXrSKteByOZE zY!wHXr^7sIZmjM7ZtEw%M>~cY^{N)-8jgScn@Y1$i-xz@6l7|WAM_Gf317p=YbHN+ zM6klz+hRpMF-OT1Pd}`Vuwd45-c2vLHkpib*p!p+$Q|{GzcmHh>Pbi<5^0op$G zP%&m&1Th3vIs{U4T2Zs-;|L~Oh@C|VPocdpwyne3h!L5nJ7M2URR9zYSoVD}c-7%W zKW~ow%~&KB!RPq2!b;>~pPUiKZ}Bs%-NR}?ZN9qBB`(=kmt#1Me_;ywRdv$$B+hWr zpOz^r?82g=ho=!ZnNx$t`oUCRWe+XR6DK?=Ss+{ogCDW_$#3aBEti%pw$p7Pm60J| z)A)N^#y$((oeb}CjTo(AEPC=0P3&r(E7Z^bNh8^bTYg+ns2b>hzS{l>?Hc4yOKe(J z14KC|sD*cXgxFlng|MSmN{Gbg{ffj}c@%m7 z64sGU{o(xkd_Vn1uKuI^nfuPp(_!vf<{qbA$pT_`_M){9(5=LI`c&rtT5D6Cocud? zS&2Y))LiKIxKTy3=Hbd~A+f;4^Q@f@6x^;u1Gct(p2FlrEd4=2FZ6yDE!$>5O#NUxK_#d1*WyB!m$-MAx5KSuKWs(}z z-C4BqoAfB*LErN~Z<~WU54Wf{h!S3PzfvS%c#21zfjnD*5i)Jk=WJ`X3Mckb8Y4vy zv?z5LKQ(%N{~ivG^Zp!_b?&+em|>0z*THEvLnG~8^(Yt`Q|=zHL}{5JnwYg^nLL@} zy3evLCZB5lJFdgr+d*1+uUTRE;@XNd~bA zgVdK@jb(F$Mx0e=S;)IKupbwWU>OYabxEzd7+L;6s~Hh{?)ZqkWW%SO6t;DEnmqp0 z#tSv!;tV}M*3Syd`X$U@HgN#^OX6=i(<>35x=75`p~bfbh(#+$QWs8fZWl_C4PkR z^@iMS)wOT@P?S~7BK)4@VhKenrUS^BJUG{AH- zb}AF#>T&rEfzsrZ;g-)|#0*P(>F@IX$90$5w!Jsmj8|5Gdbs4Js>NY2LYcSsWVXgY zYBJwAN@s+aeW#q5&j(50$1r9aC+S#(`+jnf3w4Z_Upb4teW;i>V^VzB;z!l+U+Q4$ z?Ol2_AOJJaEei{Z7Y!T7?8DeoO;6}df7tvnB4|R#_M@IfNg)JvX1sUfv6*zwddq+( z>f&y!SM+?&`Fomn%v;hNY-(XK`PC;7BesLajnJqIynE_R(D;N(zy0wauVyRbtC;t$ z&d#ykjj5gKnUvoa&@$7J8CHEfi((aOt9Dv3N`b{&7CcZPC0VpP{;xC}TXY^vQ3~)Z zhIdO$GlZgDWSU{dTG*cfuxA>#!smy3kZiK!)UWz7_FrN zcg}jLchFpkP36;flWni4ukastK66gHDYTZFEBUnUzg8cQ^T+mpWw??f+P@-)7HZBi zY5a=j)u%Jn)03F#jBuNopO;N$*P95>K_sLq29%r~LcE1{ypMl`u)BWZmnJFRR9)MD z{b>|Q(bbcG=1awooR@-FZhpEPE5Z<-BBIXhi%EI3rD??2RmH_&WMpJU)L_EzOfp=4 zw7T0GkbCdf9+#?bGwrek@zslBa06Q>RR&04rCMv5vDeKN(Y`_JS4V-w#`@;^TB}m) z(xrYSuHGrCJXAPI;VYs#9cw+`6YaHmjM0DbhnQ@9p^RK8Cr*NUe#M__ocieN0wyH% zjP#Y6zJVVBD0zUrP{a=RGy(@)Dxmzs^u>CJ?YUfE#0>-XI8;4Q$>c|>h=`VH<@PuA zJ5plLZDIbv2Cl<38#}w%=8-TSATnErdJwx$SP{^maQ0=CqU!V3o3v}=afqTSz>pm3 z4ie9AiB5rR0_VT)AFVAc#(C^siqoJU@-2u(mIBBH*Z6L+_k+qKhxN@P_|v~9x|1CM zPR}1kgc(W(RRosU$EO{Myy}f%neO=CIOj?Eb3khq(PIx>CK3x?R73F*|RqTMTZmtV3 zq)lU$kLdQT`g`9(hqa>a)_YmEfQ89}Zab0DwGl|1)Kr2eFuj*YExg(zgFXU_{Ugv& zSC{xVTTJXV6i|r!&-YZfh$M&KA_|)84{enoW4ZnONW#5s$yQ+Bl=E&?M6{nVMe>4} zy0qX+U@{^AItC?X*A4Zb-b*3sKmkV1sU0{rnZM*o1kB89Z1KxH9M(^pTtVQ0AI!}! zQElzY>i0HPKeXk2aO?EQZdhJBLoDmZQ{znHH4xuoBu__rq@{nJZ--UGqQy%_tE#65 z;xA*(tz6M6ek2PTG@EA>M_f?L+?nJp^Dn=}JpaRsk0jt3RXMePfHRT3ssmEXhl8=j zgKf|ECkt;ijV^{tEcL&a+WwG$CHG^W&P-1$gFsP9!RZYGcDpP-22bbP3V%MB3~ePL zZ#2X=n$(*9+zh9W$O6EhYV_~cF)1%D@5eH!o;?7f6IHL3Zub)jJ$m}7a1tZQnU zfNnZ}j>jXlwjd`&-w4?er-J#s$9Ly-d{aR)@zSqnxYxM=p*S3&zNam(v*ySS6_DID zcSRs)@33)j!0-ax*9qahn+Wd2Y}!&zJHug7QFYxv#N4cz*Rv!pylSRj)9kT|S-9Fp zB56WWlU$7GbNo~Kx>tSc=?dV@EiHBVwAsKrAu*4fUY58lnI&f6!Kxt}`pRnbAp*>`PS3PSF#@ zeZj<(LJMV&Uud{+Pm5jQD#hA4?f^7*VxWGROS+mq-PLhfn1kJ=n{Nb&X}qE(&Yeh+ z3;h?P%qHL^hW$og|0n)4pmjcIJFwQtVR5f@#CQ!w$#4Tf1*Q7?A;cu}rL^NbvcoG) zu*MSLbJOCA*}adU9F@$_hJ{2D)yb8>=>LJftJYhYr72!lyfEogM_MQbFe#j4QP^$; z`oma1EFp9SS`r%Up3n!>KimK(S2a(DZSllC&AgkD`obSoYvVQUL20$J_!9i#gvIEY zyrUkIBG978!1pyb8|E`Zg7)KFE3A4NlRox%*Ay`gUAw=1O#L>Y78A@{RTpQxk)99* z)XN4BiovyJ71+`EEF!ZBQQvI3#PxvLe%_!<3r9jY4N*M#Mxrl!UTXM?;0-OOKsP@V z?&s8uBtwACq5wmE>RJeXlKRD@%eheTSlBsK<1c$2GGf1~>Nbi&GMf(H+cjxX4mW*` zK@n7;N^pZ#&7^W%%6)nP1|ODz98$U`wP}-8M95wNN;y;t-#Z};P*RW{D)SKJwm*@t z-JrFEFYRW{w6iMrdQuTw%TPZ&Bp`yUV{C99|JNf@x(s5c6V3_r;WRvw4i~|dmcR?@ z1T&OzSnz7?R2<5^tVDBKTZTJI#io`~)NEeHauN~0Zc(Zp3BFKl`B zc(D$l0QTm;2{x|QOye_Q?dS5PybM_e>*Ra#M5ms)`cdZiCH~8nBhZc{V#Bs;Y*D4B%{CpIk;t#^eEG{QbZ~`oL%;+Wa5;D zx>6V#Mk|4Tnwo=0&hF3Ez$GD_`ZSQcC|ueKFubF(f`E~h_m#~yl0%m7;0A}*XOucJ zapFYV={r#rFOl_lP<_^gNyndbwY~iGU4E6H5tW|b1!UU8JznN@ceN~~xlb}AP-;Eq zeDSeQ)R|=O%!QXu;wm~_em{PEdmNr^y%gp$i*0gYc5UdImaa>7%rwW7jG6_BmGm~* zSAX9EpD6k`;Zdk1Xbez*<)xa&6`0kYc~RSxx5#g2;g$y=c1x)yN-}2|kMvxu_i$zL zgtsYp5np9f{d)Fl6DddkB>_&!T3818UQ=UV%;M<%vJ>`7T@m8Tc_gaRX0N%*oJ|@d zc7^8yELH-sNBD1!kO)p9o&s{_9}4N(J4O9?F*65-D&Ldk+C-2Q_fvi=CqKR`*+bPut#I+qoHUujkPFo;<9hPQwBn9h4@o;aud`M~Rp1Q|qawe0JW8}|B4>l-I! zMsIb5{@sSn^u_HWGh{~iu{!T2^ce0}w)9*^IF^>NZMOt<`|^2_G8Q0apZ3QgDE0Iu2jbM9Xd|UJ`m& z$>yd0U0+{Xzm)+Z`r2cN3~;iO8*4=XcGm!NRrXZ8TXuYZ|JO=xzg>(&r-g(@U$pok zfUTw&mJH5Of=y3$--s757p9)_gmldGoJwS#xtLY0tMz%G$)}k{Nr($`gKg3M<2adwmF|@zI9paX-N9s7B5`1OtLwV zc7u)H?DnS!kW0$aVSgke+0TV*#yF4;GgEBYp6AbfPwXFi1?mk<|Mp_gcw5A_SB|L1 z+S_SQ^z-jiqFHly12FC4JlGfPg`u=--$$tb>kLx)+i01G`SdsQP1QfzM4OMf)F?NC z(6lX!pZT+YD#cXekZz?Ba+i2-Ccf8eFB0Sj3w8l!;{Q4}bjlMO51u+olxm&ZR#@dA z?%vFsUS6(AcXj8%A37rPbMr<_oEwWHJBKR&cHf6`gsy&=P{es`hERketoV8yab-64 z^XnCS9vyKrEPq*9m-}sG$<>?mF*|$X#5t{I@<|S}_&OQdRz#4o%`21m3PIDE7xs!v<8|;am$F!10%FDu@FWR)9%C~r2ql1pWb2`8hz&WR} zj6?Kt@w)Z!SW^#Fv!8H>9g2zJcVQ$2pf?Pr2rJ|}dJHkBD=eeuh>qC&@DT zGmp|N|6Zd_gqLwgn*WvJA&n6|a@IkoG%un+0ZMXn1K@8_D{xO~3m0#yo_swGUK2th zd6S*x&_EB2Wnu8m-Tf4R-PNA=*?9sN0~IAgzRGZ;!$eTBgDiSz9(6WT4qFRD$QdHg zMn5#4))%W0z7kJ040pfqxzy{q_}fI*_B$9n{cec7;k!ZoO{FMz$ThcO?7_6!O;-BQ zK2S^~1Y?2%AB~+ml)4XZ5;-QU=8@gmk>h}8DJZ(0DsSK~g{Ys~Y`jw*@mLh>?x3YF zHhcw`CB!U3KJ7+0o^EF9M8Re{9#%mpol zwJVbyY^o>;e>`SN@2{w(nKLi`dYWH6@$$d)N9A-B)id6pa2Hh7*+(1PJ|Zl?c+ zU+}7Kyy-9A-I@_L|JuN~m*vRW*-gFd%y-`oV}0E<(Roy>3A3A!(T`9qX%DtfLk3Rw z#Wxii{dEsz<4gs&$juf;f1P~>Xh~p_jB+NZ!G^;vxE3O{63yO8%1Ndc7ipLS*^jfZS>Nd&60uU7BO)# zW9(3T*#wIm$n$2y$VIQe{*${!L{hMccs&Q9jc>Xz|55rdEjfEO?$R$78)HDQX5CiInUhew#(0Su(?0^ zNceVu6<_0#`w1#hy>Im=So)(Qp2hC7wkI<*XImu4)9GwhTu(d^HZPL^>cAI{)~;}^ z%@yHiM7sov&@Z$I?wbgz4;QK{^G#P@@2Y(Gtf=Hk^t?ahW_T)Ijk`gTXL9TU`r>sr zVUGt~txfGAzM_O4{ZfylRYB>mmG+k_<(b{BB`@T)G zbr9K0&HA-f7gZ`qLDdM*!GtpdS#9A6u@~k@##-17d6`K1V%R76pN()K6HB^rWeSa^ zyDf^mAImb-KBoZ2>G?Hrw5$aNkb+8QI5Tw9}qzOWSYzV_c`q1`PsqLmJ_9PxBcmc4OEQnkNc#>(ND z)z%}5#HCBm!BURZWAz~lQit(1Pk!ZH-zNkVZOf^gA6Mv|l7qUN+$577@J$6Bu=!Gy z6EHHsP5xy@=Bg+36vpW6p6p2Z)HtU%h<0W*K+V7!Sr!(6r<&CHi|d24tmqTZ{Ct}HgMYm+<$_*$Nn26gfxMOst!id=s631) z(1oWb*k$E#C#&!)Kf6l{9yC|^Ww3UXnKJnR#GSyn$E^Z=7Yx`!U}W%981X>suPiaF zhk{8x_Vq9>o$QE!zJsFkKs^EqX7z})iRkT$hSwKkr=LTgu2?XOnO1LPUbSoA;Lkwq zj^q@b1WQ_qHX;uC7gzIOyR zf9@6oWYw=1xmE_A-JmrtWB#>FD|~ESoSf*Urh*mv-pteA!N4yImk>Z$r%#8**cY#@ zR-7#XQ975~;5a9BnT2VY?pMAY!8qmlD`v3VvC|u>&~^vDy64^G;sczlZ=ekAt~{#! zDf&E&?l5a>EY<|guLl;NBlNmz4P8alQZ;u8A>rx@Qa>SLjU3siGL4HClCFH7q0DU; zR-kiJWPSD`n>hu@2>b6SDKJEN*`lO*9W=A|;0XGG4&94H=zi`sHmdZe)LGr0-{HTy zv^3VQfk>dL`927IFl#7WJty&2eu_9Ry1vyTAY%^I*#!hPp2wlxn&P}xG?gD5vd>J| zPNf+p@iUHNEZNfF*NegTOmC9#yfemEt!D14-t#95p-)n{_X}AO)ajdT&aE+qX1T^B zw>ZbBJpIOm$MbKaoi?+#3>4T9kVXwIpT?W?6`Ss!V_{)+L?&ZHV>?$iy)UA&XA<0y z;gvj^%y)%g-CT{UHU6mKd>o%Rf6TR!vB;^>!szU?59>ZyXj_mwgApQ$8J3^+r6M%XZA*pt1b3po$l@rqlIwbiO|xAUtQg%r?$a-z zIGcV84%WFvTe8v5qCe$36;byJ+$Ic`&{cH{F0u9EE_aKNZNiDUoLm)kjz$gXgUunj zKL@pp0DZ?Y)fb{Cp2`Mj#`L8#+t@0zYRkb>7Zd}vfFmA zIyS+el6Qp)0w)#ChAZ#&cP}l1h}JsaUb*vRWs{*@73d$?=)Y+d!J|Tr%5#rp7QGj4 zA_jAj&~c`F_bdEmAXX0N?#VBGt-S35i)DiivY6o)iV_8kP^~`9OCPL7oEhAq241|e zi}6a2K`t<+cp(3p@75kiNE)e|HnjH0W3@u7A`R!C{@O>E4Z<)|K`$_fUJa4O4Ga9Y z+UnCG=P5N^cEbCPz_60^opW_zn|g?_K@@<86K6dC#4)QXjry1`JvJzi*KWQ~zwklL ztUqc0J7J&(`S}KO7D2Bmj>>Jbw`%n|(%)nW>)heBo~Gs2vat3Mvg<$FhIem)gst_3 z>3z^8R#1V-u29e5iEpAXd9)b=QUy1U-6*%4N)9Ax-!W$iLI31%jK?A45b`@ZgaG(n zqAE;F&g7}y} zrnq{<>`53n*x{KbXXhKy=^5aOlzMrONSQcwe%NCEow0z6(?7}bz+=Ow-_JkX&H6PMus8Jim+admDZ>pMDo!Py5ijD& ztO!iV*S;m|7pr>$M^)B)jY;`+ZHkLhJFUdezsv^--sA>oH9#dytX>!J3Po{p@^H9n zrrH4c+TK(ho$wdC->=lRoPCW^0&}{Ec@tx2>wRmcxN|ga$%idT9^A~ELHg8r3;9dB ze@CIchss(iAvu*W*A7Hg|QAga{sW#gx^ENvK^>4*DkT7~HI~Ds6 z>t#7<9je9b5r7JhY}gB?v;1u_kFXLgBMR~<@DXJ)+;tk7o^8!m)|fh>`k7`^=_o#03_%BRD{wKm>;6SJFmJ!W~P}lUhpj-43c!Kulk4UhlZGx>{;~jjoQ+1IW z#8n0dih*z604p;v@6=@3(>&vG(=jJ^{2=dJ)tX0_3bV9qSA~%{Sl)b#eooFc0-0@U zg7SyT&|wmmt$~t3pl~H9M-SI_BJWVcZE`K^GOv0fD6m?->*H^1C!=%DuVkBg=09A* zMHD>!__*9z>NtVK;M-SWB2-bkXoe+EY~J}VVa~=LPE=}aMCPmji)hc#yF*1%h?p$I z%ZmeXfo7_2O>L+na?FY!&6KS!Pn z3>$7=S7CM=lhRf->h%u4d9*5PH!X2T+zE8KYJ{4*VxlRgB#39tn*GvBBRAzs?5 zMc#3EA(447o5S)2$;j3-MMCXP$2*3i>i0a_8U(sb@W7802M?9^{i{4Iwoitl471f{ z+%8^KYUK*rSI}I9G^cyatyJIlw)&qAfnGQ6iM|KCgDg=iz$EpbG)@)9t0&||yFvd? z0B{(O=N%NjiZ^*my)vPiVABx9wCqAVdCa?Z))@wG{Mly63BXL&jh1jdZwDX2JJiUt z&DuIIUYRjta|NmV-nV(eD)TkYk@~sXcrVae$(biE1C~H~j;$wmRXdd~&rIXz@eh_) zUAKpiAQsU`C-P0txR*3i`NwvXvogJ1T9Rvo*SLocujCo$%@om=%o58%;!Hn+xa7jc#R4v?{eu+TSF|N7oRP#r_ zC1(u|H>7Vc+dTN;I_`no6FbwJeri)YrZE7I_My>z%;l%RIZP|6FI+L^O$1EwS6`v{ z=_!_qK5AS<{lf*OWPnT8Dj#^23fG@jvq{VD=PiNV&+Q;TYhE+&A;jav_B~t3+cGb$ zeAP(q%1)}?!B^T8w@QKCL^U5uJmf2~l3}(uEHF#Ay z8?_q)FsByXIEHRYZa#&sAzE2=mTHSS_@$zZQ6qMv6tVQYU95TcU0OoDUYrv(UVnhM z>I*MW(WcD1&Dv7Y6L-__nFXfGAmt77amJ?C?&Qg>6{uoH1|Xh$$4lfc#}Fm8JRIfy zRP(jp@TCMz8Rd)Rp`jxysPk8Mjvg{dowt$l_yR>3(eE6g@uGwZYlUJ-+U zP~0k&utXHR64uYlF?;Hl(g0I4GSI;L;LB8W-3c}8wCtE=?tJo28sYn+&6GjUi(4pK z-`UQZIP)bTbfasw{hqM>QlF3N#?6u0=r%)e{^ly`dYZ2S>MBi|r<(tiv1blG~X<&0k%MU)wW8Ml=Wl+l#&+g~_!k1sVhlbmY zO@yhk(%TEFJ80hBFOZ!#%&2+U`EtPOFTFtdi!)%=qxbyhj>W0}z_())MQbZ3z6jAS&$gSrEct7GXua`Bi8G{SL%kO=Mmx?l< zunO>&$krc!nvcVoyR+D6dC{G2e*F&eb)~-~BzYChZ=iX1y+l}Qg2AkN^%wYP zk~R(UttKrDUT9v+TqyzL0OtOM2}Lxaymkungca|hort9(2JeA}CDhN|XEXK1@AH)I z*w3eG4)N|HJ`|sU339i&d3; z?&Di48eh1F=HL4wIg2w`;$aY1tUE%kb&cd)(VX#mcc8OO1F!d#%(_pU9C9J$G<(n{ zuGGnaT4KU#MdfL#DQe}?T-mtgBa?`#RnYTblg9Dpx>4||oXPFj5?OZJhg7otKypR{ zp#lD}v(9I?je^%ege^U|0hqWqh%(mdVfa(c+5pG`Ex|MNkg@sZ+i7McMRz@!a53HE=g?kKBKG z8UF&G2SJ97%^8Yn-aGahj+2NHW4-n#?jY}pxOWAwkSDh8qWSmmW(%`F8V2xI z1SwR-2NR4B%Hq~1)bF9r=eJV-d+XwTqvOVz$)1MAeKo;r=Ob~;dCWVvgqI^m3!>Mc z;#Oa#`01(2gYNR)ODoNTmo;zcwu4lb1@MOX3ZBki?zWV^*yEJ#S2*%41l}2{| z_8jE{@%)pUb9V6!83vB}h0B5zsO2T34A0QxKz`>_JE-qXJ_2d-e06puaCK7i4Yp!i zn;D}u6hi>^`i9zy3&JBdn_W&ao5?6`UI%O5^|KH2Vf^SdIPnrn22g-Jo0)dQPs;z&m@#+evT83>SqcP?I@$ zQO@ocZl>P11ia~%dStp@J5`vfHVp?ux|x*{dUVJc+*H=nndvp zHTS1e6+;@h;mi+v<}A40ysVhB{BCb;E%n08?LIFdPmXw>#|1CQ@LyNn=^n zd8#hz3`uAEpAXyaW_z#jiK_WFHYBRWCog#x!cvEmqRk-^~bK9}A7iCD}Du!Ol$wDnh0`#@Dj zsgKQFG5SW$a3Eisbr!c#il6)NHC0mcPwq7uB_Usge;seQo_AQKyX_`n$`cz9VGY^z z`)$-)zliLm(I0}#B*zH^2wVVGPsh)i;1QgZXk!jGj%gu3KLTOh_~n5B?Bo7 z9))UA5&;-YW9o|?Lj#vD=a(+KY_mD6)@(r97KoQeQdC=wRn>NAP?rvk7f z#&j?k3V7dk*h_clQ?uNb1S|=pCjsB!~?#e(^pcx zVGUcBv)eM31S|<;Ac3H-r~T5=AG{v$bz^EDYZJJ@84qrRz{8Wq@@fzBR#fiW~ z+Ry#AkJbIFX@4(_d{Zo*^uNcJ?533LhWqR5y*+2K0}F2s0G2lhyST+JZsF0Wz6;Iq zG=o4Gp)q?;+n)Dp7Jce6@0`f39r#*ttv{9oriKJ~_SxHc`5n3WPz9w5RMoEX#Oy}8 z(9)IGy3v8Y^S+9@yBFH+?rW!psH{?2639IXj10A0YCQVGcf!G8bdv#8=^hJ(zuJX{ zT@5qbh30rMO&sxhJA?fl$DZQ-#n}2~8A}3|1WZbRu@mWQe*a+5*Dc#8RZR=KJkYeN zJ*IitU$dvuz$lugc}FSu?ZX3?2P)_9sIuAJ8^B;0O9GYzj7wl-u(`hJ*nhqg@N1J- z(4+!VO{=n7G%rp7!VeUaTCmFmm*!Rh%we%5B!Cu1?LLd$9vtbSh@SUeD8 z)*6pvsPllgy6*l;N1kURm@Q*Tz>+{}35*OjoWHpLpWldt0|H(ZlmboxtWdoM7QpjQ zoR2pGTKI~_gg`vy;z)?BxPyVB)qB+G6|z>)wXHWCii@B7BXzJUw4xDEgY_=EzaqJf2~U1#Y& zBMkAz0Xr3d0a83LViy`47yJ6pj^uf2PkBlfZRe|Jt!oyd^0NL~65zWagMqelPd?pw z>ACYP4$-6nUKOyg3vi|DEL1>@_@bq?kAP7C3usj^ix!5Ty}$MS&cgCFZTX(rTNvxs zc$kG^nVBtt2*bCp<-NBWj(i(kX92GYLKU)le100$$O= z8trL%{bJeN?Rj~wGTz`}OO&>nre(JzFkK}O8SFZ`zwz+DzZnV+0+6D01;_$O0Wqim zSEcY5UKRXWI>|sLAQs?4&|+5s3_ql!g>^Q)ex$r+i#yL%ypA_{fX_0P1ZI>3_#W-Z z@W7el4Tt~r!|?M z#&@rhZ?NH9LCK;)r@MR;CnI5!hD9w2Oj`-CnvIMMTs(W}&_6%t>%X8L6_jqTPykqf zi9OK(usi}@2?v#U0%A?0C)29wOp$}cj@pyy&Lim0f?%m6!68P0$>%q(75tQ9DD>MO#v}zN&quY+-|ghSRXEK zgvZQ4$4BkHf#%&sm8vuHX0nJ>%^?j>8E8E>HoSXi(9#0#5iR0xrV9Cvhb$ zq$|LU(NtY%j-#0v5XFP3_`yr;phz4BcJ(2?#5&T~`ql-C_>bi0&nx4uHUChMwO*`A z%lb1VB)}^~kr8imedmRzUu-+~i-SRbFMx`;bf)REV%wh*sfh%#;kAWV* z9Ayl6nj^^{0jRJDFfsEi4?YHX;KpTnc<>9_*p(#|6Irmk9FAhotfimZP(Ekpr}NyE z*YNl_(FJ?VZW&7gS6>2tUq=&fR(hwq;rY{kU%Oldi-IKsFTf~|02W|{UEEkk(t}$* z0P;Z)01M?P#>T*4gi%2(02Y6}TR^R+062IZk%*rLyAE`4_v47oQPAvg7UlCrd8Hg| z0U1)YOpZt(65-Q!?Y%8;ztw#5p;vob-fRd3hX6LwxMTxDL|0CJM?eh<{H1xKKFOI+714j8M}6Mq~l5*x{B(z${&90kD8r1+oBF0FAf^cap>5Dy!Z+ zuVmKN^`5d78yv3k`3}3Yh&Oa09m`k}m>3E0I&L`Z_p!F^9PT-Js=xa{eP7G&M)m_> z02URzVwW&IfCach1*k#=z$$QsSHLf6A`i(2b(gZhJ`RwYM~XixfF(Zs1iWI0TL3IH z3eYi)EPw`x#{euq9vw#)l&z{PuDEtVQRTXou7de1@(L;z*vO7F^<`l%%UBXHApyQr zj&Fu+@eejO4E7v8H`IH)b-3qvPnfaHsQ@T20vG|UQ1Ad`FakON6Lx?x-V+bNhy#B~ z15|z383VAC1%5D*bU~oTgIyj0GN^!904$nVy3#@cWbgvWD##Ho57LM}cBiwbV&g1N z$-b;@DF$P`-U2O zy*)>|cotdiW`=B}m8d`wyJ$j{0%!;q@aX|8;1u8rsD)qL5)V{943dxJFO*p{1Gn1X zqrN8o3RqOC0yzr~p_6uNn?M0kGJSHu8eM*oErL!z|U51^}h{ z;ufH&Zh#4HJ-7fk_zNK6#v{OtM-i^)7eDw5aD}qhxDSi!)<6W0ObjXjP)LQ57BYu{ zKMExVJn92~xB))Ifn5qNl>r5C@xU&iMI5ms9~HoGs}DQJff#NW@K|Q%NdWCkHL4qS z{UdD>6m3u*@rPT$D*zS>5K9*sb^)vKt7#!$NmI(Cr_3^D0E%=C5*7f6T{1(OCZ!C*gb4t_qXJk!DgYJ=9tlHyxYb9}k$mCC10z&#*TA50r1=L4OXC+iC>{U_ zg-K+D4?F;&6dV)}DkoGbiZme*P&|mI0vKgU^l*@7q`cy`=**P>+MpP*tL+kYwOy)h z675iJt8fcgLBT74l`x@_uA~RIYDc;-@K=rAoiTusI1&|5sCE=W1u??Z2VrmvAApET z=pWIzLIt$QQ$P$Vj{q2MDHmH~cGVC5mYKN{K>LFcDt2j?LRFx`uC`ItEdUh&$8#gD zq^sJIj`&MCK*b>J3IkAN!ys`1hGYac3I!m6N;q4!g2ErX5?Aa}5miXwMOg*J0#4Ds zph#c*g|e6CFL?`vziJZst2~x{YD+-crM?#8FYQm<(hlK<9qm!5YDc&VScK`_s-L8X zv?LFevbTl_P%%;_amQ%30E`5Yi3$X9qi_I}6arLz5RQ28Lq!2b_<>5rVTVva1xYa#wZ(e_~AhFvJyBHTj7j&K1i!lcbg zxcI5@5J%Eg?d+}jgTf#zsmUgQkr6VC2L|z%0*PSYR>J^9gy9jO3KgL0!76S6u22E5 zlt(BhJF^J>_J~n`{GE zUjh^o00E6>q!I{2MH3nD0jR<+AO(dVsQ4j2{li|d4~qp@NW(H{H)6yt?MqMfAXMGp zRY8j|Xk_6LDrv$EBW|JMrv;9Ni#;Ko2mqr%Fai{DM}4CO7l4T$C;%u%DkYUi7=$E1 z6|jo-MS2n@`AQzD8|jN1cFW9c2}s)%3Ta5&14aAOQ?x_)iG~$^@CuNn-Ktbg2X+{V zEAFUo)L-mL=|neXefr`CP4oy51*Ad&R>YBT)h;~Zhe}KOVpl2bV!&sanIQo^P*7{} z6Dodi3xLHgfP`CsEIewM*d+}7^1u%UZWy6a*OhMZk4q=3@v8ZVU8SmB1t|OloZ>HT zR7md!et8hCx5F=y0k37IuLSf!QERG(!LB|6H~}TX1eij_Pw!UKQSFj9{KTks@k>en Ye;zqCW{(?I=>Px#07*qoM6N<$f|EdvivR!s diff --git a/plugins/firehydrant/doc/firehydrant_logo.png b/plugins/firehydrant/doc/firehydrant_logo.png index 3db13826348372b0847b9a891ea855c15c1a57c4..47cd43b0fc8d8a52050ea1fa2329e176a987fddd 100644 GIT binary patch literal 3063 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 diff --git a/plugins/firehydrant/doc/firehydrant_plugin_screenshot.png b/plugins/firehydrant/doc/firehydrant_plugin_screenshot.png index d3e79bb27339f22a0e199f0809dd1d1d53ff5d2d..25b5cbcaa18f0e97cbc370e1ee7436eb3f5dc9f9 100644 GIT binary patch literal 51194 zcma&O1yog0+b+844nev_K}xzC1VkE9y1To(K{}*Mk#3M~knZl3?(R5~?;HO(cZ_@Q z8D|q~uh?tGdgFPXcS5MVtOOb|AubL{S7olPBc!CupR1e*=8TISg#!l;K{z zQo^(?nQRR)tP1nPX+k=8D_0Q?N*UPN-!z7Si zDB=n(m~YVJU*Z`;PpQ6TXlHY{@eLm%s~2}bJmBsn1YFd7&+eI6wlFR4JO!&MN}hIjb>lHJ=XNDFnId(r6OrJp z?^|-c%G^(nN4dsueX8-qgdCkB%MMG_s^lmk8M>vDyt+ZI#F^x`^Wmeb1hTL0DmJ|; z=M-266Ys=_%iq$8v?zc7o66ov+;`AYd0|qo)n+{KxQ+5l&zMUBADgFh{GvBYmB_kY z>rGg6&j{YF`_R!o^$7o$yF8CTi`#h%VI?0v=V6Nnk5}f?d2F(zL{Z28c+IgHYVnNS z`>wH!0|pLn37+hl*{Bs;IEc8@s?0k#*`@!%28}Qw!aw;yLHl5Y2pPHi7-SAvuP5=cxb%`_h?m{B#A+h$?&FwyMUEn= zQb<+_KBW4-ipO6QBL5v?a=70AmV1KdVgmst#0T0$h{OcOQwN&fm%#?Mv<>^lPY4tK zmk^%{YMl^-#TRzN54s%wwZCFHfd$-D7A~8Q=PEfD>U^864PhEWV!NXa5g+7H2eAo~ zy~>XtOpb^ULqYo+eOD+bn%e+{nZmgj4>{;fmdksj&w(GKQE7dqMRMf1mC&YyBSuF? znRnP6UhM@Q3B!ycUcld>X6s-{`&a8wx{wYAsM=r>!!i1OSwRx6n|mN(2Zs-Ud=8+1 zYknO*9n`VNec>B2pWpU*~%?DvIP@$~Cj!RrFH{4@oE-@o$jl&onJ8xk5)-6DBDc8-UeTBs40 z#5+Y#ixPePs!**Qp`e+omhC1|of-LyXl(8R^^WRJ_m1Rln_JCZ#s0>Adc-utUVj*& zy520q%%d8s8n=4EEOUxruVrs{&v4IjFN$rH(1;L)&5qs4G;P9#JCS3?@q@Ef-(siz zv3W;|QOc{70d^v`GLuMCQ}bK%(CQlV^f9*rnVG9e*@@Cofdc+tqEogrulAI7(#Bnf z(z!117CVDZ}g&v5>b$*jsyl4Y9pqQS#xfW^9Q<>c?>jRczMc+(WC zhKY*?0*fsjDyAalMkDl8-V~G}=ag$T9r`qekGPk(o(XTI5)2~^<2HS8mS|@SM+=Ac zruh6_kjVN+OYlniiYp#C6GRI%u;6zXfL;UX^%GyZvuCQ-fHu zQWL%LVa;qe<)~rB(WT|ohTEm`t3E@(mlTeKNsmGsT|)WziWm& zUuK20s%bGyv9i{*7FJod7Fm;Kld_BRU6#j#=VuQ$kFOrto<*Ky_q(?_ckH(jcf2=@ z*T<-Bu&{9Pa2FNTvl>p6?rftpHe#!DHHN68SM{rLK$gM=bw;>V*X53)_1YMMRaKazjw8|Z?t z;_xT%&nc#1%#rV^2#xy2beq;LN^d?Hz`2C@!ZqN4fjjt!-H&zdJPgSpL zZ- zrkd)VA&hfp4w;2l{++I#et&m{Ok}E)Ww#ZsWy_Z>g_<&rdcIZ<8>!xeX=HIR$*#r{ zsT!@~KV`*B;)_%Z)7m&1Q!Pt6_gjbBg>X}~=1pd5P9{ext^Yb@mxqZO`JCBj%%&2i zy`B_q{w?u5H%z-_-}S>=h{|?OEHO7Wdh0z+)=B8g?Pip@i@D!liyiPT>`czqY1=>N zQq@ULv)PfKwq5!QAS|RZr0%QjFysWcQOy0^7Nmak*tQv6E2SKy9BRdXs{Hr<(nOMO zGG&Hmp2}q3I?DQ(AJ?V0sI&UtPT9BCBmPqNrmcXzP#Nk_>M&W^w0=SMX__MsSDwoY zotfBD1mpGLMV`i0i;?;)d%WTMbis+CJpsq|!;aIi4|q|UdRk_U2QKxO-#dR~E0dIx z9@-w5Hg7qUJT{zPMlRnrQ(L<%a(PZ{Z{4aNs3%$nFCR5ydv054>uEo0E-$UQ6I^Sp zjMNK0>0F8qcE(D4j-n+q7btxmx~NU?>UqM35r9$7_$c7Y)Jx)V+IVrzC@Ln`Cr6jL z%V$npLn0us&T02}+p#Um_?NL;8~0iCmW)>`x#?~bV??ktygLHDIiV@TrE0C@ZlU@3 zV8t;aFpA$R^VaJ@v`^wP>Ry0Z(9t{c@xg-c8s$i8N_nD;WW}$AKdm<{3Df>a)v8UPxp9``c30hGtu&i^pp9SiQak%5~Pj=(vlz}9nL8&?O`e8=Lr!q zUUrZZq?zx%e}jkcDXId)^kQn&nq}$unWm?8M27|1{p9*KlWl{b{V&xAasYM7^*<@f zKp<{Z5Qt9z1ab!s`RqU-j!Y29o(=@UlLUd_*`zfn@PQNXdQuV}A()p{-IfnZCxfDsv80a- z(rK9Nq01wz{O^!yd&{)3hVoeL>I|~0`@EV)x5I{PL_}Js!nZ}R5g1O5pPypcs`t%< zd(Pc>m^g3gBrolsL$lwLDb?Lyiar<3JbBE2l|O*}t%Fm2BIy03OZ-TNqB~!3SxYO~ z;w5dG^0{c&{O3)%nubh}3jurcrM09Xp7(xjFv1F+SzXT5-L#xp;sZ`d*`t^qjbRhx zAH&LXkA1v~H(kr`#ubYbt^;kX3fP~IsQc4zT$N?Y6+WDLQegdyQqaaNWHDnnU3#q; z_T#oD+R5~9Y0G7{+K;jXIM&70;%VI{)!X+(?YtR{uf$)cs!Hn~-{$tZ;J+!=n#Z z5MA0yis#SLKRxxw>pJ(JN(Zm{mPxSa#x9Jy?N5jTluN&@HKhppCK#_OE9gk$f5~0;+pZ^kI2j+W^hxIfU;V9`0CKK@)i^7|VICzZPKiJI0W__ra}oFOwLHI0d;1*sZ`cCutWB8B&- z{?BP#+CpWK9IQ7u1!j3@SY!?bC^|)Tjq6+Y7&z}@5`({zhpzI+kbXp{;PjtG9)H{_ z^wT#m#wx+WcY}LJ6~BZdk{^;I^4ABNx@^)V_^0D&>-bOw4&iLMIb|fF>ge}?Z}(zM z$D@QKAq#HQ<*r7G3xtI2Ca`6%T!K=%#q*|#V0#H$d0Uz5WLo_y!kB;d;PaR{zTFS_ zD3K#O?;w)(?`I^#UCPhez#?IiY38;nz%4{6s=S+xUCgERKy%(wWp+i*)1;1Eu$l` zIH+db;}Sk?>>LNw!=TA3@z$Kh_{(9m$hqm~1Rx;G)9#doSin^5YP(U1W8MBb)g&g? zWUe95?@^o3TSVJri&bX+;f9V8GZ|X2okV46Y>?wSWzd16n-77MU{6#*!~U539Utw& zL{{e&m4o(q0GgP?9U7fk%nTRq+naYV<@n0Q`eJ{0=A2wuh?jo#hL01M%OQTwyv`T$ zPr#>fS4b5*NBP%m#BY7W%76{T?<= z9IpA8(K@}JEJRWXQroO8op#^1`j;-4A($amXC;)f2dvxS*f5Mj<}qDT7y)xQwvHO~ zH9zl&BiFmB7CtY+PA=n;Si3Bb1+HA&pX@Z|5q+-U4^2co56g#PgYas=K{(=#?jzZ^ zWd}dgnEL7DOy11#=Tzr&^ZTkJ6@Q-l+|Q~>b1y6$J<;&89?jDOt-|P>WWO2rPvQ0b#Osj=UvW;byp49@7wa_oW?~WcjmCui z?|#QTy_7azjrD)J79#iH*nPtAUJ_b0g6u&2Y4(T(lMv!ef)&}!l)~(x-a!3^@o9!2 zxv^Kq-_;XVEca8*B`e+gkdW->+9z4o%5FvA5E-kq>2SjQ8H%2Arer9aHnxtx3qo5s znalOTzhKBRXEv^{_2f0?9Eq3l=j>d{GA~E_jIzOcAlRp;gA@&uXZDuF zRk27t-6@aah#fy(SGp6udv0c4s9fp8ZxkZ|BtIk1SCcxAJ67?+so}|`H6cue(F)fV z*D`P^G%0ky@@wy9tWfGz`3aMO*Dda?! z^e1(OBv8O7CpUk&D0uG~ZKO~1&hrWhTLbQc2*b)`q|BV)Y1qeYRFtri@=`A}lirXp zzpwc>*uO_+TddjM%0);=x=v278TXcOKsfwL(SB1@OCEQb!t)V!8_2s|D5#pA|NE;- zJkd?Tr!QNmMu=;Rd(hG(8qj|4Gt@?v%7T3ORust30+&d09k?QIHg^PpKmneGd4$Ij?*mOC+d!Bave$E$mE2V_(r?>Yo@YPhRIc8CSjO|^;Nfr z$w1_>enRPYRD?VRib74bK*_Wj>R4wvcDi3zB1xGLwOZw!-3Ca^IJLJFN!*Ay1P|!M znxMaR{plm`EM#8pY>B7(p~n<+s5?n~RM_zolwh!bm~M_#n6;JnqVYlV&P> z$@vERBcAJp(@mZ=*G@RpfyYdQ=;x=pH1*3ChRs$_jj0nDd)k*WMqNu zspAdPj;Oob{8qPv;FJ*Uw53{SHN0+Z}tv zyBwS5c7)Ma>gBZ~=g+8GjNRUK=dCX2a;y)xE5(VCwNTGc@QeC4d|#w^0m*?#eiT-6 znLkK()WDg$@3}pCaynFLodqhza|f2nSZFq47&YBhhZdlC#n9 zag=|Rv#_S)09WRW>*@M%I=B5WtNqIPp#A6Y-~jNt7?MOyh9H4gJITxM|If{?wI46u z%gJ5AsTEXb1Npaa-~KLxJJlnLa{TN3vcnjBKqmf>=X%r|C?O&ua)(8H)$;7Yve-vF zXTkB~^Wn~pi5NI8au+qZyeyleIA?JcEXt-w_OHagUIRC*%cu%m2lL-1a8if1Y`Exg z!#-Z*wWoWK+i(%X`}^wR3;pK?&;h#m{_U$aT*oLaVV=^_>0NqbCT!6E`DoaybXjAT z+B`6T1Vai;azqyAC{lyjbAn$NN9qmp!oi|K9xXOaA{a{lB^9<>up@ zr~82&w$Pn6(jnu{(M)MgM~FwCKCKjMi(|qDt@RSFq}re&c-Lgnyj-k2=uAuv0M}4_ zd_e3BvsjtF!d>ncXyr3#MZ9R3#1=2d zn!jL}EW5$H#7b729RlgG-KeLe@Jwaga#dwN7hloh?)qp~%J6wbH1_VYujv(rbIFt; z3l~`s!+)Bl`H4x1*fCCiGo#JMG3Z`tlzW?!jQig6-ZUX_QbYaJwy-{V3gUzDr;fZ1 z+&0|?PY2@DgW<7-?oEDKbO^3BH&!i9Hw+!GO`jh86P;$x>%6h_`(_=3GmrA+6J0i( zqZc^7ym~8m2{fLn`3cis3ty1x56#uc5ip9 zD6g4LhEtn=NJB5Et6?q^`udeN{$eY_qo`x>+&b5X*T|A*Z!TE|oE`5e>b!BR?$0E1 zV_Vg%)2ypbIa4FfS5kj(sWN67AV=MuS&N z@TR~2GjY%U-X1av%EJ8oli^7E$hPvgZwUzrEcoG3g6yh>J2`e-#OT52axO>B0lF$O zGBT2qNT#=~R!|(NXk~DT1CL=D?v|F8l9H0f#@oF7*GET3zkdBHETqNi-%$-*ePYOQ ztYS!%VXLS3x1uE1X}b>7Vf(8(+4k9Nwlb7ze@n&lM+UEfwzBd}M<9ws1R=X_M*tEw zm2?W5>G*HSlV^o&@zAg^d#*IiMyJPP@8^lpQF25Pa?Dk(WjbOCy~Jy;%i+TtvZ2Ky zi>axpIBL14-Mq|)$43hOzU*F@m2JMWTMvkkg2GtrJE`GJLB`j}D}BWnub*I6I-XIG zk?9#2mKGOpJOqR>9e6xX*Sqxe^tQIPV1*F_JFX2cnQ?uhWu&DKR$8AKTOSlvRNkBU z3RXLnSumrjSLg*MB_)-Xet-y(V@6jH`U(kQ2Kf7nK0iGsGHT2Il+S`%QEn1w4Oljk zl$K`W8P-B~ZFIYv;6WZH$mNeS`@*Uk@S6*0mLVM#a{mIb6#IVkrAdxb^=txVT&*9Kw zpbYy=j_aj%KpAJO9n=aMdKRjeG z3((4=961vR3r*n^6!bpd8ftVpEG#X>HMC;f57i3M(r&NP!Kb38u64WIQ!dkrS?Gga zg$26yHp zfd8ylh!^E0GZI)gxul)Wd#4|p8(pp1?ZU>1>4ytY5mTt8v$HbbhI42zcOA${p^sF0 z#O`*C1#PIVQqWPQeju=Zh>3}=2F5GLqRG>~@h_~wDsl?k^c|cfgUm&}=w+7#VH=x$ z*3rdN6u3anY>q13&-y`Z3=?U78mBqrU1*$PIj zCKD4AA0i~b*=Bdn0AAGBe`;&5?c&xXaKnHRPnl=u1E))ur5@;9vuS8(_>_4^Nl6VW zEMK6+lm0ZE&+}W_z~Hx^U&nm4Id~9Uw(Eb>d7WFZkA7`#*wt%lYSQEGdw$(NI-(Nn zSR^JU4niZUv07?qNxm+8x-TC#U}LM zBVg1F1lAtP6kL=Ax;>S~<#5&u>v_AOmovjR&3}f@9BEYercEA+=~!YAuG(xPadxl1 z-wj~8ZYXRJ36(MIf4rjLEKN40VIOfj<@!f0PJS0pvr|f}Q&JRLJY|XDoXboj zYG=yk1+LQrRLS4s9saPhev{^SbRtv|6%pxAvYyCgyM(0-QY$+Wyk^ELqU4Rv=R2oXDsiibF-Z#0PzsAAQ zrT?U<>Dd?iuF7~stIB8yY(CZgN!<2Y{~Kz#Q1kBHyLQZ`f`VP}w2cRonBA`qz|+3F zy}jhYwEHF|CI)=qVrOh3SB6N?>miba_wFo_PfA=oC_Fs8uI?tBfEAZs#RlB#;=-ZQ zjDdjx__Io-L0=lTJ#gQ=>}=7z2{>rq@bJO)uCP4mWESV6MRxOPV4;Yx(A?bIl9GKP zAt46`2VhjBJP!E{4TM^L@$vD83$?)2Mc>R!Pt(%Ueh&*9NoF+$E)4|*)tu((a@En% zLBi_NB*nG0wRNlg_V9R))b?O< zmD8ab=%fn~VxR~ZI!G+a%E~I&AfuR??t)MFrx{8X5wbm=LGBQh*h6C2Ek2Mx^EFfEI(7G>Q$A%XO9_(FQAj`on>4 zT+(!&_2q(((h{Fnx?H$jI-FNm(c?w1gLGoD{``lJ*uIfhMXH;ibDM0@d-HYGrG0S% z2y-mUE`418ZYj}lBbn6%d(7Mf({;+$V;^dgVH{?LD%3c}V2@f&ipP$l8UzxwS(dqI za%=u%Sq@2~6lj;^Y^h2OmiLaB!9$V+q*a*aF$G9*qgYGdTRk_deVCU-7axCoeCz?P zV>Fl)9gSJ{>WLma908+2{9ZjIq)^j|+{d#4#`X1e@c6kR0S@_~+3-iZ)vM@R$vnti zNzD2nkX>!`Veg!POzyqW8xxF9Ldb2mfg$){b9JD~k=ktguRGKG$#v|PSflfC2A}Jh z4-^a=JG&S&T7~+TFRc3A;l3~kA|ZkAZL%x8P3Z z)5Qc_Hmks`ua1^@L2FNs_aODAwCnWt^~Km|H#(K%=29gNXaYNYe!6Zo9?2Lp`9r{J z^zd*#;^gFX^4rYJ3?L8$4AQ^0O~u6)?S62N_t)CZZkNEX#iK|c2N+wqcz9yNsh%Z2 ze+$aKZodpfua`*$futvbh*PJ{2Oy#G z-HT@TYmquefIXHP98WleY>vf}bFigprFdiY%@e~}wb-V291R@BY#VPr`7c!Z)-uA< zXIeD4HPrjM9qi|uxmW!G23o`5wP%v-^ijlVx7uTIn8UJN%oV{NgE@ z`qgGBw>4dI>BX!pv4k2KhR0f_t>Do2zTE40k$;qh8i)2yWtG}Y{U^g+SqL+oQ}H5{ z(ETLz<2aFSB}o(3vuqN_dHt9MrAy3o@pVecWbx>=%v9qdje~3$WhC^d9bW#Pm5#B0JH>V zq~hm~3|Uu%8Q$+vEKP3na>rH|B`Ch9ufASwnF9Kv#lNYpEwgK3%+D;X#Xh95mZWT; z<7#J7GChQN^T)p;SIai+S+rwDdHs7_tOiSy?_K+klVCC9@cUWCKDg#0LfM!;9TsAFl+f ziiwLyk_mbNlPM`FDJQYsn=H`piEOstmX(&4_VjG7skwT7TzT&6>pOMf!~d4S>wI)` z`VRo`-dLf!nW-rTm2w~Yk-N+NU<|U-U%#N`Kr*fIym!*q|ED$Fm>(Vyfq+h2=YD;p zWkcOL4G0QAw=s!{!3%)BFFS9G<0-QkjebuP{_0U-g869+1VW*ZcZoiwN^l2RPVpTbs|oCI=^{ zHJJFDlQrYvG+*ALf`ZHand8|C{rB|evbe>W=I;dhPeOH(qv#g7R*X9N%+b|#&beIY zM|c9~7;aUESnmYS>=}y7e(n@>zuv4dc*_pxaQimW;n2hu>R3_H9z3Kt)zVdU9uHz; z&>4mkCo?lrtp=h$-KLD8Y0o2_?&Z{yVW1_jW(LycBMV~!{t$PPuS@b$AWi)p5|DWR z*PO0Vd*0|2$_*^hWr2{7X3w_2A%U!#{Gh1nDQRhFY>czC5&UKpl`7-|pzI&^ zPw)^hS@eYZe_E^=6Xz1>@{)EJLn~Qj+>K(*j8lw^Q?3^ImJ~V1W^y1}0{cCZSgiC3 z;hayo9V}!)_obSR@TmB}PSNV@0r3UV5?KAw=4RKXo%iGA%;~?L)a2yA;9vxJ_`Cai zM)ewt-e`)9j0}j6;XpzJJ~O~_*c;p2eD z9l$}pDk!|d#ej!NJB#-EG&$JgToam=y~UG zvf2*RsB+;S?K;IEUxUt0Pfx=NySTeY;WO(2(^R5mXJoWqtg{6u&(hlZcxT54x2mQF z9R&pi74_ou)b}c21l0rz>j=b*bRI`qq-&6$78@MH!joR0;%vFDmX;O-0=yRZu&SC` zZ1V4%95^_*uV25eakc1n27$}ljMr)`7l6avthU48A3#H9XJ=6fSPZ(pt*zbN-MzFX=q(W3X=`s!1hU)1?RjcysxI@7?-$o zAVA37M>{8&8F@==d&Z5w8m5=Xgog$5Wu)dMZq&9j!QFDme;IRhs@)GQDU-FbU*=i~ z8XvO#GdO#aI@DKE29}vPX9IV_MLrdcmBoI}%PGY7)4vIFoi_VUVS(%2=c13%v-mwW z61XEA^x%ot!AItjsfoc%{2byKyYFZvgCl0kH8;B;7t(#_J*!ITk|6jzT&>1Fm)dVHw*n|33C5Nk1 z$a4uuG!(QJ1r(+E+e-488V#G0au;>wAZ_+x7ykPshF1-TaW3dE0-pLuVLb#h-<(?S z&yZ{(km}Sb{kbXtW$9YU>N?@S0^Mnc+}X*gRISCB{L7 zJ>MQ76Yy{m5`qNjGX17<^W?OgXVYneu(Gm3+EUofgHQ?djE$*ZBiCzu2-5ZP@&ceR z(1w$Z?PY=h6%=op0`rv%Se{{jJWzfZ8Os2GrPpq0s;a`)q9Gwk2fz-PJf0I$ppBs2 z2I8Fl58}fIG~Tn!`4P{#k8urqR@#Zs+KGPO*CPRS0RUe%qAU!#kgK{A4tssKY`;oz zxhb+xGQ2c~f5-N**xMX?N&4RLqfqj~c^V!^@?1fLT*Mb3oq)VlZB?_`Ph+%@z=w84 zo2$qby?Z~+Wy9UE7@iycJ1D+x{_jo-Z>4YKHv(%j8ccPf?S=BB2lvlE+qJroUQNY$ zyWp>WQ~5U8VtM)n(iZr5#MF<`c?9GckdFE)stF{~06;2r;-=Xn;65pw52vhglQl*`lYHh-WQ~+W zbpQtf?FYcEOZIpGMw1-0bY-RP_U|Td8vf>|+$(p|J7TM+?(7?>e;_n}z4t{?ZIt7Z z`q1XNa=mGKYGMc|67{yjt?z3Chp;BUIcailq7X4GBduFXWgMv*dOQ3>Ns6@gss}>} zlL&?QL~`U_IBoyZYfK&iK28118r;qN^ zVIBN$-v$(m0h%o>9S(pRpvpmj^XPWM_r`1S>k6Pc+abJrmqX@tqYLo+)6)|>J3AU0 z8s-r*GqcD~8z5Y~7+D!P(-jsG(f-B;Y&OBDy>^um8nN!j4ifkjD2Q^Y#_mknN{&<_ zpfS^}-rjh4cz)#l{ry{m$*f)vE+8fN`9Y8Cfnk6FG!rVVAUOkJ>P$qa&fUdjm8->W zv%lRRq1EFS3*HZ`q(efAjEua!y@eGHaXV74wIboNAwM`DHmcGcAse^%2K)oSD1Ub3o4OB|z0!6p|+fZINjRa>)(nDuwBXaxbinX~>l4)zkMQdD+hfhg zhd>!4fTDmOyx3ki3+m|ynm%ju;%wRhIGN+TfBPEpT9Yjvu{4>~e*l&Ll{8)RPOfNx zmy!4+1etb3+AL}8c?={WG7Svn=GT=TigLx?`@ixW2$jVIRF2B2qp11nhHaNmdU7$a z8+cUAjno{RD*-*C`fe&YRP}+;s@1N4<`ZQpa0G)UTM43xB?@OZK@z|b>USf)=8%B6 zTwBYOss3B;X>AQi_6csG5*$tCqcRp=qUwS@pFQGrL!2dTt@by_$i(3j@HYQQ-wGar zOjmLd3uMtW!47iH=8BdEcc6+mcX$FR;tQ{a!BrhlIVvq;cL6a&kszYgM%81zjXntL zCVbKJZqy^{J4>TI*Oq~;%;I3N7y>b}GYHK;J(t236%DQLiI$L%kcEZi^74`^vJ4~z z0A9)kQNtk`O)eGQ&rdUD+ISJ+K%N4)J5GR$z#bSJ8M~R1fx+9jzQqdt9^gNAc6O^T z;g?c8T&2l{xws(zT((kOTiYAJ8KAMb8mk2L`~&h6P^|{LyWy?mq@~k9Q6gU)x}u^Ypj@4IVh94vgAJ>zug~gna{|CxdqtxI4+)1009>y#-f(fb z0PX~sOETC8B0)+@3Zfq^gpq6j%&YIFw57y$IqI23CFR*Q8&77?yH2HGDO zA`cJGMxaWI$1NZbI`5wWo%e@Fd}~6`6z(yiT`Q0}AfAEo(m6zLq@BuOt_#>X#4{sscL;5f^Qg56j!D)})$Y*0F0)nPAMzMe$zHt3 zZ5qr?h9Pz3+G5$|yWXy^{-HUwnXZm2@QTb3pGXY}!qvLs*=fxI@hPkv-i&h>p9Aco z-Xd1g%!4E|id%3<)?HIuTQnv~S2v4vU|YXs816P%h7DN)$g#)Dwle8ZL2+A4gAB*j2(8GF7^d~?h%R~bl=opMt^8x`nxPs-w_ar^nRI;;z-2z*vPgx zw^{H0MZ6Q!JK9t3cpc>q13{PM86qjP){jPo0JZNX#x7ny1fdQOXR}7WVbUc-|FQvP7j$Fswn+(@ePSny{oZuZ$qIKM_4OR0PgC) zfofh^k{&LemRO!`PAzYzS6X?Qtk@|hUc%}1HPDzZLS9jVIu4@Q0`d-TGsXc3pUg(!v7J(Mk|A zGrjI4g@t{I=_l}xqDcAofXA(rMsH9^0PMr0%s)%9?`x||X%ao`=DNYD-F05>Rztgq|g(>=ck z^8z04=Wmz+o(Eb~$EzwJQ4#Vu>>j)Di!Ea+cFE75y9t&DqmkHNY?M) z2H;$0Fb2@<4uRYb`px9^a04on^qLK?uNr|m`BG@~0(1>%gPJWKuJeCPfpQ0w2^;!- z@GuhSRYk5wy#Xl)oW03>=EKd&^u)wIsF@Y3R(%8caD81-O^pZK78J{XC>n<=q)Sle zfA>QklpsKOk- zN3_lrXIPcm0!o~ZcTj~`rN+PH0aV#^UWJwqa5X_Ryon1XX=XJ*xHdG^=DbfcrYZo3 z81OCMJOBN1smK)mxl`*L!&;0s{X~^}%3@QAwZ1c>Fj3?iXhmS|Vvk0`I64y!SI%4B{k!kLbDMk@4~28Aw*3!ADnM@u_EPMeGud7GQcm}>II`|m*4 zhlTbxo|%8o;j&Rj4(-3RV!u6f9M9#5)3JBZk62oCR(3B zZ)59ETpsIlvJqwYw2 zDVPLORVUYroN7Fj0w?#DXD1hL5fn3lsN)PmZ_5ORQ25sNwly#+P!0z@%3#*-_D8?~ zwOdx*4p?`0_d0bmvpparfKOZlQFstkMTCW+INoBx3TKbKe*OAySC>Q-DFYqd!1g(i zY&n1rA|N1~v5S5E>If_wc)=Oq-KP#JlP4!Opu_{}>}bGMKyCuLCqO^Y4Gl8f+ndC0 zo={ikqN)nyniq!C9YLubySlRuvR()LSCd)K`>O~t!KXxRuVkRk7Z=mB6$_F3zz3sI zKzuefHXQe-WkAUjI01+*e<~|ez$icv$l|ZpDE0Mi2Px3&b~7=CQUWObudgNp(&MS* zGMWJJbP0umfRZVgNq(;fuBH}{=K)ms9VP z#K50R;I|bvH)h@w7j^v$9|&7mP{#+rkAty%iQcLaxK8(q#q>z^e8Wy3mF>dNdJRaN zF-cZhT43Q)zXo^Lfw~Xxq69hSs0kZkg!o;(4R2iEZ^Z&8@W;=8b(p}x4ZMvQA@cVO z3_~L$)|QqyBzfr90Z{u|Ff;)4+X112h)8gB7mgHt<+|@W2o&l3{QR_F4BfwCdx7FH z8zCBpOG{a8IJgC<>M<|`j-jFli{>bUIXbd)aCF4fHq)Zyt^XV| zap2K(AQ#|fPy=wO%)rq_)sPBME;z=I-7*IhG23-8rY-=-aauJ5~PRLZS$#_LVzl=n{h#bpw{MbfOl8tU7F~dl`G{vmO_($_V)j_Y%*~@pEbI!{f zJGs23PDNQDB6_0Oc(07({>oNyAM%|FllaBI?6x`TEN9oa^K`5s{L9+4H%DyQb(Obg zrNw$Zr3FQ=sjSU2jJCmkCHYl5f-;J0W|u%WG+7JgG3x=s2h|lM^81v#biIoDK-#Q(YB-5Zfy3FTh}PJ>OY1#0)20UB!|)GxQYSDyRi=;0pIjqR=n9f zAK~?i+enDq1+B?q2KXi%m$jOFel!yuR*D?w%zV2&KTMR9k|r;L3HzUQV+hU^@DLmK zQ@1OoDVg)kcB_#0p68~I#QT?3C5h4sMWq%{$ExQLu-tE#4%ZhfdUJ4Qt|=Q&=D&FA zr^%(0zat$ti0gZ_F3_~6LWJ%Sn3O|(xz-%#fJ{?;%1QI;Ru-w5VZgDbbabwgILDf` zJ*-jrtYS@65It266V~_HwZ{b38LST+vVYc>kozhBeibFU#T4n*L%3{;Ic6KILK`}J zgn{XaZjscTm55jqdKnw;gY)m>&#RQ;+Z15U7X^i96^0kM3wXgg|IhYi5A6OwHoqC6 zj`E4Hf%rMTGERZz3RcL0gkt~Pv-*K$6vc0h<-h0Tq%N%drX>;K{^B7zZ`5bM-0Bf-r1>1> za`Dxo!b_u=|J}%o*!;I|HWL4bH~;OuyWjmi@A)(TNNBC<;}qYTuYtg`Nr7AX{&8jJ z%2$_{UbR2!+uq97JWo3cp4|21 zpJX9p4UjnG*^5@$jaF(h7EX9Lrh?<)h-o-{rM9dm%k5vzFz7Jkm0%2h_pPh-pjK#N zx~eUR?P6Cnu$yj`=UkVuwCQBh{KdzdJ4(i(R`=qFbmqsn??eNl8ny7@Ta$Fy9Fkyl zuWNf=q|XSlrq7;~)i8OEuQV-_qJtY3@RomIV4p!%V+YqO%-fn#L!IY2VKIdo+~2#> z9CCpx9aizlZnzVdV$jwqAvrM5aiYsxG<%_Oybd+Ua2g#06)MW_WZMokY0VHmY{2lw z(Wmz8ijZbGt@Bfx^%SIYrFr_z`*w1B^qzHkFCS5{bjne;=lVAwCk&ECrLa(iE$d5v zmarzeqBST}ljpTn+dgho5Q}{0eDpP=#a(e_{=B3M{2v8^#|{PHe+_HHzkG9fj<(?B z2CKisX76W1o9X-Szj-vLMFy>|RzPjQ`9lZr2_3^*@T@XHrJ_U7ZJ>1Ig)p3X5OL8k z@gev+<8B;hvp&mcb@bpLbbPnEaM6POIhX|jbuM%+=_J9`N=O)doixOTe4`yvT#i_@ z7Y2d={UOU~RRcHXv|(zfC0Y!In99fdp46{>kicgMV$pr9@QQ`w%?6YPA_Vfyt-vn0 z35jCI>RGg)VV=ncs)T6F?E4xle8U10It&CA($qs`0l!-1JrXB_;K7mQglPc};ev3j zvOwKxxZY!K&LtbLPubQ>x9LJt;b7vE2b@f;MJZ&-`Yod|2|!>$2UF4Oq=oij`J6`7 zQA$vlO!>ERz8?(gU(UvLLNtsFlvGJkkdTm+ z6cj;05D}0Lr4bI@A*muIEgX;%rA3f#5otxbQ@Xon9pCq#neU&OYrqQ-&U2pFYpuQ4 z+V_30HMaOCbs2ul;l%PGNpEDeEf`A#Q(Du`lvUWM3(c8@Gfop!O;wdEqtsVtqfY6V z_$)qDGLWOVT==>;S~5{u2Vw)+;~Cr;G4RmEYk_{q&% z#^k|ivG|OMWD0RfL-z~Kef#7Bjx>f7O=+JytJD16>J}4Kia4k!DSFkJhQ&tRqxnT` z`n?4jH-v0{l)0yJv0Tg9#*!cfCtfg3v{Ib6I!X@5k$B#m=Bk{Kvmi^ z#zAhT#$3p4IjJYB5v9H=qFS>ydfQJ<3rmZ1e^V>m|OT~H7PaNrrwhpSIQx1374BHym?NWCT z-^$HYGnevFF(ywH4#zD&_P;|dvQobXIUk8lx+rWm`7fRK(2%Unp$H`l zT_A&ujE8=GsB&m>H_%WQS>%k*axQCS*sXEnLBXkizbrwJm0|w9g1Z-R3Aa8Eaoc$8 zM31kK)WZtXKwj`Ud#Pf_*Utsd`9I_GAR;Dyq`d}lvo32|JIA|$6T`HAeI4IOcXKIF zM3w|()zjG*etbOm_RwkMQ{7N?PV%@IQ?5J5OpkN_A8}oA9X!$pDiQ%Q*Ti)8Y02Fg z2G)58(1kgveh|ciq(dXUx(4ujIh`yNHVt4`U}4v zKCI!yLgq(at4ESI;`++r%;NRvwXJXxTgWoHq9hj!dlZG5e!U1uc`u=Gl`n#-x6Gx) zf3s;K@#ae?v){w-S--{i*57*uV*)2e3Dr3ayZPSUrdf&G&T*7F=&qHWPuDYW5GBE= z%+hjKF{%|^8}LvT)Wl(~;lb6Yq#l@Ed62lUgY2Jy8>0Ig9i8=zF@;yz-rq0f4KTz; zJ>6{D=qRvkTR;$xKe-{nRR{uqpv$fe@(4Q8U>e-`b*SSSK8ArM zB_>0zcVy)!8UH=L$~28bH~Isw?#~xjFtU{npp%!V75aouQ(O2TzHf9tjg7iK&7Q9t zYJAMQFCnXWZS^#U&<`QE{ZTed+hE(vEC1+EfW4te%H(Xun2dUr?N+F>R7 zS)zXZsHZ`U6n{HKRcOBh-G_?ka)$&xHHJN>nM45PbWcIOKAOz4pZI9M5TDK^t#}$2 zKiBaOS+0ZeZxnJ{jlpBu^D(xC@kHIDlakFQ3fJzWnO(3K zf$Q;+_tM@Qi~a~kWvyuKRAs)TUDR{(1nM17O1i^V^eqgul>VrP`I9RGB7ZAw&UDb0 zRrsfmJgg|J_C}VJ`6o!77)tv}HrY^z%*3?|yQu3NI;{BEHq+cfPB#V=T)5tkv;l)E3b z-ZXphW0~7tSij|}Yku;wBy5mlWtc`<$S2-9FU~SG%F^uJ#d-a)-`o;c7pN$oG(S6E z=36Mzff-jLzv`S6EB$b9%V^V7^yYWI#?hL>oX`SK{_jr&Ue^(lMJa?@ja6^nN|b?M zHEAB{PqGn4!%}^@xMp%|Fg{T)wVjb1@m08B9iy4`KCC2;3J7utIvrK z4DZT+eN=~IA6zXr8AkMKxQOM@k|-d;+=_~VZlhgvYxBMj4+*{Qoa34OceQjua6k5- zi#E=I=o?kdmP8Px5kyHSicGPFuV4AixzFW3*B;C9MAyAOe6X7w)3wBvz`2=PkUaCu zQ??F|p#!_$49{iu4ph7sa(+0_NuGqbPt^3}mnYLwKk_gR5m^4*@PVRxSl3UIb8-o< zB#Hv{n&e1w<>A+#vWq!CqEJ3Y=w`EN2^z*pO)qf5s0T@-E8a_lpYN1m`uwEA)ZciNi%K~;J39z-r!c6W8$q#jy zTn40C(#@^=G8f|Hn7bIR;qKYuu2yPFsl4?B?hgv<2SFK92to9N8|&elVxN1)Apl$* zDJDbe6cejpFZGg@=z4~ji}5U(+aHbw2;(;|Xixq)-d4VR;>bfuAM1Yp^5ZTu$}(RR zUstU36b_;6?uWQ^IkJ(jIp_XpDo%WTS)}`Eu<<$J-gZDN9F9xBKCe8|eQGV zQ}1dcF+c4zncH=vz_nB1kTnouY>?!wcIYSepka8rkjVhSYZ?c+txoi;iRuqb_VFJG zRs$$RwP%(Mrhh)eL6ExoK%8XFj1w$o2mlfSodI)cjuYX%7s|Ll6kICs5)W#;s-Ys% zrEC*CJ|=oO!%dQSXtBQJlgESeabbsjv0qW70}0O$Dx>R1FEm`r@Gh~jqT;jg9eU|N zch`x-VXt-W#!l6Iofs?*BvDxx%$zF%T{UF8(A{A;^z%7vFnJK%Rk2OLv0^B(xrc*b zu0g2X_u#VCRYu)4T*43T9J-p<$ZTdDd~39^^C-Xmx?gT5OlU*8ldml@T$FRp!A%=w zQ$XO>_;leTjfq+kw}Zc=$CSmSRBz(<7nR!l110z;KQWmG=OkI;+Ec-)heBq6O#UN2 zx@thA8A+mEN#(ucc-!--f2|dUT%~?Z5UG1s5^%0|(9vamB!%{Vdm#jeqt{07fAlH< z6gVpFQXqE2j#mATgKs~`^%EsrZwPe_{l1deRWH(6O0<{v_X|qNR|g*b_%U(wdUX89 z4w7VZ-B18l4VN_N?>wdpo1`UoTi|RyPuKZz`gLR|?53)Ue_4Dy)M?_ZsX|ay+;OYs zpcf{@RE$+fT|Mw)UeC!ohPjV|t}|mY;res@6cbD&DA7I)8M{`QH;mzliM1Pr&(u3xkih#FNneX zj*a*xMGLFtJ@@x0m~4|u!NDGF9q&P*SP;?ct*c?gxp;I4F62E!Qp){mugSMQUkN}c zn5A-A*fSX@kZJ!u5l^4En`D#-cCpZAN}}0P_V=Tz>o3hXI@0g$_W}Sq?UDe*4Hj4f z!0R3mbebaC?30^<m-_N! z^0r5s^`6ENsqnig_17c_^KExeMSCSmn#;@%#mQ_Qrdek>{;5%%7^g-qjU(@q{`M?WMur@l;4@BoXpj%ra) zUF`=h9Yif(5?5;k^=pvRv8dfGgPxXF+vL;f;c2geE}^S6#iII-!mZA>FJ%?c=NAkb zO|WkJLe5of5u5*@s;lksKvWEcUvZhKSFbB9PT$|W7hv#E^D;hRT?Y2voSeJMGy}K2 zIa>539QL+9FKF>K%-N?rCKAGi0*`DJpb}JT--VWeX+^eI5o6ba1@iUV6WY;zq2{w8 zsgSz6P&ec!5tliOP5c*Y;2&5xL$5FPuK31D6J zA)Dgk6Se1~lw zF6puvj!^#HhKdmhTF0Fy;TuK?YX+~MNUSDO_2Lp(rZU~axFpxe^ln$#j8#G|!!>O`T>lUH)e6Eo(&##=vXi2jPY4 zw^tMTu=d)!=Gzt5_8VCe^a2y~zyL$xE+_xVQA9m#Xm_x96r<+RHXN z3~D+s>t%);UxP9A;ed&-iLn3uhkySE>Ne)@|JdUH`Qd1sY;XMN41R|xEZ=`U_VAY& zQL69$)I=E6`~MZF3qnl}x+Pq&5oRiiXtK9MuT}q7h5GKw_vwv<&{kXOB2#I3YmEcj-X2JfpVB;j9Sg-x$9CIro1>as>>V z96tWXH^P4$^Z)Ia|6ecp&yoM*`2Vk$`g~TZ?ju^RJmuV`R|tGYazR0X&)>#kMOdDzDfnBi#b z08@An1&pf^{wpghfHVL+X~Y65y{f7TQ2rGahk#6Fab`)MlwN;I+Nj_eb=Ih)phAOC zFfw{i)M43+++_M~NI3Fma1cXof=%Z!fB?Z(YAlbQrG0l5oFp zrl%ReVg>{d4K;OS7N{Elpy_pJXp@~T)kqr8w@24!He5x+E?>MT=?W@zb#q&j<%Vp} zU$)$www1etOT#8C{4FB`*EB4d0y7$CnR8?pOM!d=ySTU4sF7cTgMhnj2XqIp0+*Mq z3HNGL0;QdTCrbeq2+(S#-7=$7*r^4c5eF9+7Y`2)FuJ(8t*xvuTyrwAAiO0HZY@SX zW`KqZ0NW7WViT+G`Aymz+OTuoBeLC7x(=*$Kr1qdwh`bm;Q*$??ey5qzyWqY%t0JW;CyIrOO zACDeUEKFh}+B!P0X^({r4i08!W&(5)h_<`CyVk;qx!4)0U%qUNReN~EM98LAd7oTD z9_i_!larOxe*+U`yvhv+AtWX3>F&0-vwL|UC@9FSTgE3Xogyd_+^7Xu5JyKxfZlp~ zN@!&O>M}YSR3M^S9PRBZe9pYx-7CVvI%Bvr5hOA)5{*W~o+*a93)eSR4Vu-^NMivGdz ziHU7Le;>FuQ!o93GXfe#?YVIicXq_MmPopW2M zmut*_Um*^}zN@VZuqmJn*Sc+)0)?T(svn4|QEju}p#b0jAE%)a1|o-#k5Bv@z@=KW z74F{$BrGrk$GwkU0Ds}Khz;;qhHqKl&=f~2LV16Y1i3P>9yG_d_MFg)CqC=y<82#S?rVt`Y6tfq#8 zhyqy(*tCzH;$t39$UHHT4hPURHFKY0At)y&;Lnn5&-3b*8CqHn{r(LkWNR(0K|rGe zB@ztI2Z9o=%%_~3@k*EQ2bmZ^JO}^)yYuhX0bDkKdM93Nao}#m-q3-nfwne4ru>(! z%*~$yx(b5XhbR&Yb4yFkYuA8!q6*-B4Gnx&S->@8n2N~$;h}8nwCE{ddYeLM4JsV( zgM+K8+5s5G-QC?<7(-M9R5Ca|m??M+kUjYRDpPz1L+L_`6Cl5S{dxp%hydTW8GxM4 zb~GV)z#lrRdH3!DVhAb-rlzI{!Wd4FatD4YwHY0%a{C$(fGa}*Xf6z;iW)YN0p$fD z3<5tOguZ?IR+vCrzVcu9Ba_Se1EK4Qwl*g_yP35$X>CkQOd#rPrF2}s+I<$-|L`$4 z2M|{BYihg!>3WlKz)F_=*n-NZz`o1yUdoYbIW#C;%B9 zCPXX*8wUp-dx3s+c1FfF&`0oaaqH9o4-7=a!itLb%&))|NG}WLW@ZLjD+iG7Y;Eb3 zSW+Vs7>)(6egJ!3PEI0lkd|HVG%yHj@7f=$si_%d3^z5I037o9-vlf01yJOOh}pJ=z@O#Bi!1I*PHhy-Uz_} z3xOc)3Y>uAw`lYzV243zg4dwN6D$yDUf@#Tm%#5Q)X2!mQG^)H%S*0-f=C5iu1_-5 z|DwYBNK0z=f0$oapTZRYz;r?jfTj@N+B-Vh+S=})3JVJd1qU~p-FflihIIw5&$8;+ z$OvimDVSnX5`7s?NYk6Jupmj27G-f06B96(iExJo5JGDH{{6O@G(Z3OSKE@|Z{Nb4 z>Iicyt962ssjI8#paRKxri&K`;jPFmtgUrREY$&}swE?pvNk(wVs0KE6{V)7l@Jqy zhbSm1<>uvGQvsN0enG+7(o%>~d~K}{C;`9~4OX?!ZKbEDTL@l4e3i3~_V>U4_<=2$ zcNr9wj%Y=>o{w9%s4A`daIB*`vxY0tarx-c& z=whg1;NTP!37O|R(NABIn+shys+~6|8YVp;-=F5jODm5Yphl;E>7Ps+_UL&2s}w1$ z17!|y2AC7AY;1z`M&(?X=lFaNLi)5F-{IK12a!LS;qCn44-4gqQ>LnYpO>jY(l;)= z9)*ab-xU<@QY>f2eY@u#2wC+_oSq|m*b?&Zr7HvIjUrY`+4f z8fbJ)Zh5cP;K~!`$PA*MHFEMY?D5<%KVreu3AHJ_h!9+=n>$$C_MttzLrGKLF_L;tt zvNmhjj^P7tD_y-%qZ5sFZgmMV#Au$eGjus z;a7Ktcg9c;pK#Fv4J7-u7mfBG0QBu{coT}A0=Mr`>&+PUr;g@zK11&RGFHM>fP4ir zzLe~e-gMmN%F$x8$9+jDdN1t^_KAlRSWtjs6F!O3$HSc1g?KMH6+88SeUM zoP3YH)Vyfx{K&Y67(EVxI^m{U^l;A?ojD)gSjHh2MV)nDWm6|hl%#^!P z!_ggQ>5Pnul6;%h3lj>9HAclzYAAfXh|q4xy$M%+ILPi)oqJ7ufE)HDW#prQ?(KC^ z+_3X>Zv)I$UrC8lelARsSxhdYw8UpXgLO6D<;`mc*C{6 z+UG;|*dTl^35H`tBp3+y&rZj3xe8ao=YSYNdNI@}4?eEUck8Qjk;jU_T@omwEYc#6{ z?MTPFMP>&Q?Q>H}t@3cr)`!Lo@J4*X z^K|q-qTegW87Rf+C`2XP5f=!7lda59{RrKm*OWYL3KU%UOi$qC$9!7;v%R0nVa>AM zlH|+bCEtgf^2kpemp9#{FPEEeLs^igH-kacI?b#aov=T5qXnc1J`O_OIm ztj{<+@CEV!}d zx4Vwh?;VLvgu(JL3ZHq$s z8gdWp6F0HG>}!IvrP!Sp{nCp8-VCTCdQCJ6NGtRH)v6LC~(}I zJiv(HW3w*7r%LGP)9P@nc*{u9M@QOQy!CG!AI zjBD7E4RoC(+|={tUz|Jbq^u7Mhv6Lhyiol0>)_uvGk?ynHtz6cpwSPbl7H-!;@f)k zd)A`XF4DNf6*(Ya2#a5cs{c&#gE?KSllkXu(nKyK*WPan`gO^ne45KqNz8<~U;Nm( zl)=qfcqZ6uOt^wuNQyJ{;#I`=xK6Thl`W3E@V|hGijpA|R8@Zu-*3q#Z}_F`D!=?D znto(Z`RAYI4wsvt0C1y( zl7W8T`DpCQPnCI=Bk92h8|odzumvZwrf@bz<%9`&<9oRgnEB*=*9lIixU2YkqAaIP zW8J~sY;KZkVbfmT@0jY1e0{F@y_LYAXK^8xo#E6cl%5`m*RZo6$pP9J6WUnI{0q#l zvDr_Jmr62wT-bFEB`Xf%{7-}y-_P!Ubj{+_fP3W4zhw-3IXyR4`QM0(`SySRJbT)5 zmP3MB=$!V}p_f_T5^W?0Orlh~q;LFOYc}4mt)kxrSgb2yu|DtggeX?X9__=s6tyXV zb+1cf34Ai^&TvOO*aGSa=^Mw*-l$Y}_}KdX@UB%gVq;49J#Jrl1uoWSC;4a`JAJLs z;f(X`?wafB^W?MypKosN=)mKBUh)6{Nu$mzC0rOaZW&}3poz{UN{mVkvl`v>>fXOB3s4;w>NuT7juuGi$s$P$DNsAAOOHsfri%?+A zJ&)lhRRKDj&LBGZsmdycCGI{T66%QjiZ3W{3o%;&AjjKOP5yFJBh7O*5zlXwhvfC> zs&8`TH%#PK(ln(7%2TV#Bw0UC?pe29fyZ+-DaNa$P*P1ino%af;SJzjqU!*CXSms?}+E%!xt;I867#-E$=DB|(Y_i&!raEspVM zE(t6Do-+G3PJaG;ay$hA0=txNNOCN*vj9UjMG_z`7`XLknNr7K2RD*1ttP0*EXM1( zVfkNBvKctNs#d3jE%}Bl&&Bhf(8f2jlInDI`~;tZtmUIsCO_sURDM>Rm4~sZ{dn$K zM&~i1UaE{K)_$rG5A{q-_2@6&x4>IB3a<)L>uGV=8cLzgez;p&dg^J)hx_xo41CS_ za1N<6BI%}F5{SBti%p8$&az7&WsGC>9k;*b2dwmu`0veyCJlJxBX%p2B}hsV8)pfD zR%=1zeAsPa}ckMKxVW@P6akz8KSaZoIso|LV1UX#_ zzHbe+^2C3Qa{7S|kpY&t2AjnB6!vxnOIJ0;z_qZBP{*eR4`>sII$oaLQHx04|MHMm zjdF29i`YRs%*L~Rc2_AzH)2#bssHPH>d*$Gulv?CKuOQcj8xXhDM_d|BB>A`{8fec zzB}$~&%k#rt$Q3pTQX-VSlz{p@iqgd5%OFoAXYx3>zsRr_y(6=c`G=DAS!PkQ-!$$ z5~#-Y=hp+MyG}eu+gS#;=><;UEYY*#v=RA53{GpP{Z==)1N-)~DYfVxc`Z>p6CQ%G@gLeN{*g#mlc*W1+J<_kGJ(!n>Z~Fb-2fP#0Hcr z=hW+)?j#@X8N+)>R&2Dj~Ulw7l(S`}gOeLOGV!vI$XZtc7xeTeBW3Zw=J7*fRt|T4>k# z$#h`fbbB|@*UIep32)$Sg0s%1I>qMdHI6`C(FuLk5m<=IU*2lK3g6ZE8>3c3eT{)G zC`X@IRbtiaB)1ks0}dB$5Z}mqBh(h-aJINiFOJcq8W#r4;!~4o?;8#N-3wupzzGDi zgoAH-=o*nJ_g&O3y>fZ!Gk{L-w8|4`nvW5(6Uv$v=saT(YN%}o6`OlkcXPeRhVH7o z&e6No#LfBzNRo~X?3|zI-PM~*tFk}OUm?W)yS$anJdlG6E=1&w@9zQl^2uk^7EYoM#8qwUI0X~DE zss#@?dlkkHTPikX5ceTQgd#$dAMtIPU>ZLzDvvy!a4+i*@uY9&W!wa_44Mu|u}TG{ zEaq;4&tK7*FmliR|16++v_)7y^xK-f|L7%tA$jlbf$yX7?tWDv&>8z{CJgm)n|n`} z*w5omEG&BzXfV}L^z*xfi@%{0u6k}5U9k)(H6RMapNuEid=P5OT6Dro{0J}+jn204s+W$ z7%<|uW!DE&5oYkX)eSFt?vbKjVN|!l6O6!^QNuT&NNx>Dusdk(GKDLU#p@apr zZlLKqHr4`HE}xOKYH&_XoitK<%4y|oig^eR#yR?l_dscfui~7OA^I68dmwvhZQWA9 zUWdFJRI*?OJIxQA=?ZF^X_kL7DQY+A+nlJKUE!@=ozkWR(;yek>8PHT%Av%> z1}xc=KW{vV^jPlK7{qOmZ(pzQ?voa&j%0pS_4cD;a}_^}xA9U=d<3z1Kt}vf>^-rP zLTZJ;+N_*^A z*P6rGO-YdT*x^dG>2m0yx-->~ZP?7&L@7lV#58<-Qz~sSkHd24{Y5>7Tya`|&^CD{ z%PCu>n7jQ>Fh9`ArKqKSSSJBuxT|gBEP^b&=(ID6so?yr;eNl@1f7ZZ;^ONa2#DT1 zf$v4GxF6aO*)>g%acnr1^57O~d13>a)WO!u$b3A^(>oeyP_YLc>LQ1N&^bf{X z20z;#lI>w~^Rw28v2KO&iv5=d;Nq>qn(}e#4!zSC`r$=O9YIXpB%f{c_4(oboSh82DTlKv0*I-UQi0Pz!-++r_1bmtjlUM~`V%r-({-!&Od~A1wKbc+?N*Z6wN3|aXKKxi? z+3v7aJW%+6&Wr#l6Q5}iEirE{;q7w>Hs*INB9Jsxx+oUpp3RRdQ>cZFv-9+TRiBp3 zHDEuw?#_jDE1RGD({xx!nH>)^M@7(8*5dGV^&c?&E4HyB!bQw%V@>u~&cj z8k*_AzNi@J5iS%UdkiJ6t*_Q))m3GBDmQ|d32CAV3TRk;zANs2kT3JODB&UdeCOOn zpZJ3g2P_V|k_-amp-eie*j7Gye2oWQwo&FXw3PfCCx7=AjM8&*_ooXmdz`1#wlIsu zx5e>e9@3_x-K}igAdtoS=pk}RN^xY_0hU!9^gAFd=Pw=b%C`t2030|!7HZn<=H>Gi zGnY`+OE1V?td;h?fx5kFkPtRx4kOIeGGzU zhDo*6)@MT_H<@hT)OhT=`)(Z2U6iG&Rvec)m9`PauIg8<)s$hb8#AN$mkZe1T%4D0 zyejsX%I7kZufH4?vNqs%W)#4}bF;RU>BqM!^b_3XQ$Dwj6#7?`2B*v)T?V#Z=-tg> zwZ!fIITN9<$>}Q{hT}TNl6J?Zwmtr+IT4#y2=DZW{wD5N;&Q-GZpj%!an&}%E3wP! z_`h|~^k+(aa@(liJ`tFDpOeSxU8WuP@>Ovi-=DEA^r!i`QqD)>F8dV{F5PU^+3THR z-k*xTr^L?}JiN2Z>$d6>&M|f=VBdVFy83K8_DHP}le(pI_?*2+e~*UVRM~vm7PgNPRjRCc@MgLY}Eh5*&7+xlK;(S-W~Oy%3D_Nn=Z<4 zr8ny+9pT4*vG^Z%CWUy(Rg#E?q;x*q(H~i zq=&Xl6U(jm5^X?X{8QUOOOx*jQFH*WJB8|xm$-JvCSQSF%Jb^^rJINJCl z8o68164Kctvx=|yRCDyCTX^Z~jdg>1hE<VLWE}b)YwBnAFCdo?VH@|3@GP**HxI$8wzjaADpuf%CXLc_Tlg{68U6n{K%nQUC z3lltF#c)eDHlmRR@}_kPGM(FZig-#8wd)=1%tyzq~R~GNJwQ5Fr8Q*$59Ba~dpzGQgDo7k$%(w|XKjz3RIV&b) zDM3jpZ#zoR_HNR^Ve124qSdViHnr7=tYsPUP^(GZutL#--rJI~%&&H7e!Lj0gQ&V@ zKm=k_w93*E=%G)p4LTbE;YGR>kck?Rz?xK|RH4?p?`+_290Z|J^zt8ZJy6a)Tz9@2 z$GZQPY0?MZMbs?U)YHz}O5Gw0W-P9gt049PadR=dg2?YWm$jPMLb~h7(XrHCaR4B9 z$42M7{eikQieNbiqVJ|`A<|FmfmKO!6q*sL3D+|Y~(?jp)XCF`pnn2jj{N5i+TT+m0lT4+f zm}Rd{eP_GZ7?mViGaEC!MbG9xH>d4t865KHRok-N4G#0R`&J{>-8CQ6LWVlrGA@*O zwWz_SwD-H;{-Vh3XOcTTWO3hUf;y&Wye@PlUNT3oz(w4l(D-iXBGA{tCBycmZo^dYbH!3@5x6wpy%V-Gba!y*kEjN%}N{JkK6fw%}GC=7~ zy{L1zuJPPSD{I@?%-%;hM1>;Y_|&>VfiWZcxRjzlU|fUJ~7fqLLf0wz|)gO<+t3;Uocgr(XljH zD5rS+yOxr-3%j^b>T1wiX>_aPw}ory*1*o+kf(WU6}Qg>v0hGs!q*6c{fRDdx?d|u z4MOrzA|&vy;2>2w9K-x7w@2hnbG5XG?uie{9+VG20GqhzJ++GKo$UH#=BzJx(Y z9vb65*Gz=XzRD4JSV?mMHx<<`F7O$Prl_d6ag2MVhm0072^3FPLBGSAmOTmh9cW@f zND&*>BIvv2E;{2b`u9MTcE6JPamB!-w+ZKFy{K|DZ3)B_n-ftEfpVFxd*2m@xl<{m zYYwixz4qRsmW|M_tJzqVbEnsMH}_jOQ63fg6@ER#LS>*wNV~oWQU}4%a4q@30%${! zh~m3brHoGFKTC(~V3ff=WJ^2s52wrbKb?Z-Ff}}oCMp$pnCa{QB8x;P+eyVgnis!V z(-^o{3M?0CX!f3C-|tlRasWBjxss2kbEsPGmBi$yDJg^8KHo_gm)0~~RX73~X_mE2 zQcf!vk#(``OFTj@8z1D|>^vSTb+FuLXY(gO3I!C0i{^>22+`t^T^)an{=$kE>>EVM zRK~^jfxYXACP5GwxNG*6>S5{6pyZ;+($%}X%f9G~pR+k;#Stc?&)6#e7j>zvVw zd7`9Chf_vf0Y5DfB&1#~lKk&RK&n(G;hVQP-8^fy^LCv~n=+1zB0%`90y4Qb_Bw_>kC5A+R9^q+(udCzM@)!kAe)%-ULkJs@rwF;BnvU15aE$8E$t&vHO-T z>ZCnjmav%ou}oZDP3{wfHh@m}DeTbUqct@kmQUC7B{fY%ZsA9rZIv~23bSo!J-d50 zoF8G$W^ldEFoM{-iI$%*v5A+V+{CZKFvy)qYq8R6KN+V6( zc}Ypysg)}ZdMn&yfUf)SSQLu#X^kB<74hPOU%yrQkN$3@TKT3x_AvYU0`n5#L#((k zBmM(O(z)@;kzY?#DA)N1JX@!UQ@tjJjkJs|SJ|p+DrBr(T4h@MvU_7)+}7pyDbd=I zxfX}aB|%@JveoCTpElI@G1diQenByWwm ziTa6myDXOb7{6wu-_AfKRgJAgtpGnl3cxnUFxt-vOJ#~Z2-UEeR ze+68vUK~g1q}A5FZX~R7$&|MKq%SheWJd21EQyhCk=QFxF>f93s&oNyo`0KJm^A2r ze!>nH<`XCqTYyK0O+>je_NE@7nqMK5z5cxx=kZRD*lIx|S{H9h-2W(R%ypb)q(w>s zOm?;jbKn^sfW8&p=Kod=Yp^49nG!$fZIXYoFm^(ZY15W&U!ZU_HQ+K?9yRKES}~{p zZv5~*Wvo|+2F}%ywl_m(Q?AzmMWI4dHA!5Q2*RAf@vJpC^+qx;SLKB9TfPsgGe|9P zZn`yq%`OP?=%S_*X@e%|g8KU(MbG+4J(Fd6bxSX1zBs_0zOgoHczH#M=w*kkIqZO= z&Jrh;j|WGz*STn-V84Qr%<-7f+o^C>Q{|}swXT$fwx&HUrVIfBy*19KK{rgT;;pSB zNbk~kA71T}{P~sckYc>+MJ)R8FI3CAac)x_FGie1=C{Yqmq(c^zRCrDWlU6`f->Fu zxg4m#dWZ}?6=3viJ?2a+5XY4%7VMrS^S}o{;zE_}m~k{u*zi^oSC;2R!7;NDC%)?} zR1=GuvLO69G?tIQb1tOq{MCE?P_|Z$|Kn$T6HpTJ6Q^0p;s*`!iDUcg8Udih03~d= zcCV-fhy;mq*gd?#@0KjR@w0x`LY*(2U?V%uuORu<^oH|J@i^35vfr?19Ny0@6&Qof zuB%j2P`S2zz|wDWTx*mLOYWR2Ot~H+6&1OZ#|#1&nNQdYPh}-ticp?XOpS%)RA2ft z=mm+hs$&L!|5!FZ5mw9kNt~G?(pnYQG{Eicu_R)}6Ypv}$9%*^Ly4aKd%31wY$$#u zPwB_--mSa41!BzpMsz7%%^T7Sw0lf-42^uL>*UgHd+Io^xaCeH3GanUi|5N}lQyoe zwHo_0IS^c-Eb<|5;HQr~v2q#1LS$1Z8Vh;JR?{SJN$m*Dc$l*sdl-z=5Tu_twR!o_ z+u3pUu320lxoNnMi!%RQm`zBdST&ne*S4^(s*3frvO=&xVAvD9dYu>tpjNG z(EIDxtFSOOm5mw+Y1BQ$7ew1Xel%5Ar`Fo95#KuSWDE~Bf;~rQitxLq#}m|m2{w=G zoH61}dp^Xg6dhE=CZMqjYK`y+5kx8_+=wo0I?bm=?HL{Z@1SZxXM)@${24I(2=eka zLjLj$l;Br<(7&=H!xe+Q+> zUpX|@Z}Fwyoq661Acp5wf8sC!O)EiS69oTXfzCg9f5zHpBQj{U0ib6kwy(AuAZIB8 z5|^)D5zYtIZmqij=>$QnU%Y@f1nj$9wf(v<$Xx&ono-EI2ju@iIz1vf`mT+S!HqEs z$+8X5LI)k(SFc|m)SUtb_wp_K=Kz<4JriSf4Gm!-p=$EJG`v%~XBa~)JL^D!ymJS1 z%1BR3Ly-RfoB^#(OiT>AhPCcHoX;-?n1T`(bnOwg8F2*=X<3FI+D){rKRs zI5ANO;9*eK3B`H` zftW3HJOE0=V2xKLJaE!;Sjf=O5YQX|@?8NfZ9p^?A0NMA1zM`ni8-1pDYXME8}u9# z0aluaj)-99;>yX(^MbYrZqVim#vMS_0HC$Dw5)L7$u21o%DoiTQe0RFjdtn)$KTu2 z15d{A8?Z~@q*}G!C!ip?I#zuIeN9t1kE zhEIPqG#F3Ry#(0*0ATUq2t*ubjc)cn-ybF{}xw*J>z#yEL2jC!CE~3B&^&AaM+a{1?)CMp= zuqcjpmP(zLG*TZaWIm4L*1E#R<_rxh0E65PJ%vD<|I3#zg*2dB+}hp_4~v45veIpf z?O=1G_UuGOQE>?r!NImLN|B(^Y;A1~b21EDJf|rT>4bh=kyr5*g+Ft4A}LQx84i1qk#<|A|gV7P|?tILZdPex&-%m z@#2MAmNJOA8l*O(+85>VQr@WcjSu9M~72PT&?_;{-FJGu) zkKrz%tHj>U4j!@$J_CaUBUw0z89D+-86;KaZ|0_F>*RSghS@cjXQNJ0X7yjYHlP~OBKoz=?t44q*!zSr~b05b$)pEEpFP zbF$IQ1n72FR#uGZfzLqz0EP{;QSgLb2r%fzU!DS7olz8J2s+)l?~98Ck6%rf0TVz) z`XnPS@1d~byG{=ilUeBHGuM$A_wL5GJLPW&6&)J?2-$bL~ z|IyfYhg04E|DQ6lG9oKwG)TrVatmc-G^`{^W?4l@vPWdJtlJ$)c9MitLP9wTSs_X0 zK_NP1Z@a^4+12*5Mso91}qNt27&J1r3)y*a1PWK zvM^4ky$vHokT`8`ZQUXu(2f0t!Je4@AMT~kIPQk}nV5V*Yq+qD&8}gLYpJeGV(7`t z+PWX!1n;)Bvx5{$0YoZtaxPd~t7W$tBrDN1vpRV}Y6(|Y`;wHHxH{8jfSI0*op<^( z-+%q;U44UtQ&e9-23`Y_5)fAfF*2xMPn|x^Nm~dBArPTs2x$xLyTRW$(K1kqL7P5| zhXlqVu#nJD`_kI_cS6ECsE%Q7VFzAWd3nu4ROtjJuuh%2`u5re_&gL^OrL@#9V<`> zp{USh3^%0HVO<3|fR-U*icp3YC}18huQO*lF&IP@J*xQPUqYs;I=@?;2wn!iIS5|$ zeJMDOVaZ<(2t>gHz+hq44vqZ>Ktqk|NF^{6#Kgqz;bB9i!rJr)oCc-|DC6T)Vs=4c~V%7#fj*cq0G7jY7aF)VXujmQRU`@w=m@uNo4OG!W@aDqMf9>pq zZwEa$L%jOQlhYA`ff(7LAqXgYvhW83x}Ed9eEBzSoGOusk3%?0cx(RxQgVu5^X0vF z?D+9zC^UFZ^ll~)iNu9Ih{H{i58unfFdO*hQ&fX2jf^s$K4rO%n29RI5VZ0e0zQ)* zVJo1cL{#%Cbs4e-u)LdmUORU_#~ol^OsxcCDZU`z#j#}{psI{OLje}EBk!!}o;~*G z!EB-!u=Enzu9cOlRE=Dm&!Fz-cDE}k)}Yfs6_~QEje&L$;Tpstsa7Kyy!Fsffx+V8 zKkArK*~q9Q<;#GuuGRQM94EJDid(zN)AFx&50(1juc@Ay^H;2*YS$4_aHBnE*!II&^mVZBwL4zV;d3R@ZRl&Z-C55UsL z>n1>b$e6^!U?|2xSbFvKpW(sbsXKprIg0Tjm>y7hPh^wZ{Sl@pTLwI;l z?+aAk825zKAXW4aTkh)R%ZyA+5X#_Ha5nYzRiXm)Fnqr`o_Hv&!d&2$&o)oPRwBxV z=n!$z9rugtkoIUQCFiH77s7Xgs9(~mLclkTR=vC2&kH2Wa1&H|Q!g;Tpn%Bn&fytJZ)!d+pnXL zl$Vx9#4H{53pXyMxXy zre%sNS-w17`H$swaUT^+C#QX($s;Zqc22IY*+w~&LGH(-rzRmp!#g76^x5f}Q9(DP zZ}0TSoz)cGIkx|4s({D0Vs6__cQKj^spXbct>)v|ZPU}!*vC*w^v5b#eg}>96c^Dz z)I7Y={H0i$W$VQeSAuFxjR{5Kts{=fo0i?x}_ir`F33;Jb! z4w^%D4GskZvmXTlb&nnecl}!!T-+yH-+|h5l`qyaMh+LA)>{s@sx{kLfvl@!eK5tZ zAklewt#2%Igv)xOJJMrab=2*IZRM2#$;p(QYpN<|zSi)txj0|A0PVTIgy|aiu~@(JvwtyWxT8Ggvaux zuRUJP(RHs}2Y*_fDLExi@}hhyPH0+Ynr`~(pE7#L8C|g|*vU-w{%bGDI|C-?(}x3y zs}G&m8yR_24XdJ`Hf4YLoou<1T_FNm-~eg22(^X=dGVLQ1cJ2SQB8H@hR~DSDqM{@ zN=>w_=GoT{ryGWZggi)2Mha6$f0Tu^g_9Fe1q}@iS=de2s3=IDI3sZ%%Cw9`) z(kdz|XFq!Mx3+bmDF+8fQ&SUy!gB2cMXz2}o%yC(ch1d#g=B7Fflg35z&$zKpR zAkNU^k9^R?@+LK_*n7ov9L#&NE6Y6{1L|C@O3aaOcr?H zHhkv}3#Kojkyg>BOPx+TP|rR>J>Y&HVj-}$jhEM7H?Mb8&M4h*_^Z-JUrR?`8xy!1qB42K6<2`U(wUSm)+tBhmQrs zrosp`Lcx$eB}K)N(b4xcHL3wi##UCTBr<`3BLUNgZw6&k+z*rw`)w~mCmdR6(0+hM z$-jf0UL_c>d~2r!9&weOD^Cz}iE%+deZ&hU11m_{OMAqXM`M!+H~`w)vmQPA(%$}l z@Hgygo%$kjrwkX@0`|~XuRJmOPSp&53{lbJbRBKiq)PGRBS_Fe54}=PAT0P}GI5@) z;fo9#Ump<%Cgy1;_;)V{$7?F7) zum>P|<>ch#zP?#1jEJSvy2%$02_Pk|GQJ^+tOPDkC=yQO)nu z3^%FGQte$739-2cA(zmy#6-RaW?Qs1gn|MB0_qkkt=Ec*irhY)Gj+!yQH;C~<5y51 z9W|)3qq(&e_94APQ^}SQClhLWN8568bK6F7VULoS8)`z=UNk^piO72gBhmsjNH{5YU zgYIMs-)Vdbax0rx_+EIlNb-O?guDD$UYd(FIbUcBE(W(9*7!E=QBZ*bp~6WcBh6?` z#OuCcn-UKv2J{ix&>YMkliD9O#I*HNb+{ceGQX10<>iR!0TJvgoil3?J zz0Ay=2SlGmMX`oyT)1>805i)#nz}&IA$+CohsnL8XU`@#vk*O(+0iz(O;*KA$$3eGri|}i8l*g9DGkR{pL79fMDgL#0VbHn5WiU-X$&N(_6aXs= zkp-x<;dxCxwu~EAYbz=)K8qW zQkP>45jNQ~8s?mW0=Anb z=H@HVVuZo^dP>`4u4n#Tc|}EUbF;L({O7*D%EH3f?qL0F(OtWUiMqVQEv>B~^`hz} zq!7d`GhxbSI2;9V(+nm8HA(mH8=pE=mlF+xPhG3?dnewUkd>30=&Kau=BCeO6g-h( z!^g%JjfrXkL5vHcH#afQ?Mx%ob#(&)*%+z>NBWezg_D4Y*>vqByzS}PuDt_mHU8@t z`9pX(Jzy!NYkBX9B_6VC#wE!Y5CToj#r5Ja+wKef`daA{qiFtyK^)cj*4M zwSjU%Kfh206BfcaY|_ZYBK3aO#WFKK4pY5AE}=|0KLMdokgx1So` z!JIwU;+aznDUcniI#n*-+R~Dqoh|x}hZBK98FRhbQ`n>KiU4Ko%7X`2fp#Im48MnT zR3EXJo*sN_#nmxx?DLBkkvEHyU2JSNa2)o%&&|zEwBG`D04x*_PtCbPZRaIJ@hHe z`HcD>szK(FzL)9-Do&Qfm1bpUcl~kNdqhR!np|C6^6`Aj%KV^y!E)x)=nQ06WuL_$ zP`_UPkOs0EW22*t%2!M93O_&6As^j6a$;h&khAFQ z(KnW>ZK`hh6?V%U)DUW`|Ivvs)@Ng`{%D;#rPE#z1zws%rgvhTAV^t zXmP5GhFRANZ(dRT!L@GBn(B3{GIyxle8RwNerqEG=HW38_Vv5+gT%RHIr=}bb>>xG zAg}QAvH#ou{S8<}~f0b3Tfg<;6x5ZeA_$ zp=hK(D=SM1`SY)5eo`ph476701}mOkTmjz*CjCvQS;0dCV?-Ork26Qt9Jb-3{)H=E zee)9>d${0`k%S_&f?{U^p2YOBDtA^KWP^Ig*aRI0~&6wotv&r z@Xwsd8;q!2J-K{6=+V{HvDLkSy*;xwa)A$XRHxi$z76l&KLOwoNl4%#L&)39<}=*8 zR0je+I~C7w7khoUfJ9a;pW`_A=5$k#*^Uy@G80?z?H_Y=y&?f)y}3&Ps%xVzwfk3-4cx3v=zKv}~>3NU%HeBg=J_ik19rlhr=QniE-WYF*W@P+!*f#y#?f%5I3P3 z0XgVY6`ui6SvBwGVuC)gi>oUem3QKEOUo$ohVk*w2xJam-v7Hh-em0qS|qLd%)C4t z^UVZQuD<*sqyYwT83H&GYLG`yOzZ?G0P_>z?_Xk0;aOOTc!ZGU)0Zzz&CQ~Jf<{o? zP!Ho-77{?6tlZqOu`y=CBs83^cC@v?*4k=hX_=p$?Qrhg)WYxch%{i_SRTz}w?9*{|;AMvSi6$snSv zjNM!I>Q#K5t(mDQFEob&SH4zPSJ&60gt9qB3>n-Txqm}96nOw|K$?(?ot@CEmDvIc zB~TI#4h|x&(+c9>vuBU9LfMNKZ67{pqsXME*9f!u&e~rxMLD|W${z>7%*ZI*Ncr(& z4&UzW+qa0RK{$r&)4P6jYz!=Ci_6QTq{ugHbK)8@@EpRPz7P-?-CwKc(piQwkxc?gPlfS<2ik(2lvP60vD zn;ynL+Iu_(CMI*hKOa4M1lq3A@$om82XQf06`s`QO27I1Tep~~dUwnCrig6u13IY^ zkZ0N3qYI|a7WqKc6rPwxiCeHlbbh`I@a+AzA+$z8!K&S{AHxdwi zQ}58hVR&TZaejUv?i_4vAwmc}z@&tIgKQ#wev@D^c#vA!+Ytpo|BQ?%4_gLB+?m-~ za68@o=O3U}=di78ZH=+VuTV)~UbT00xVpNcq@x_TQUbG>f+k-+76Bnq5Qsme&7oF_RTCr2$dr|HlZ0kJv}78*rT9G0)!?CRvX6l z-rhi54&lbokS&U5KzpcML0Vexq4xaYL%2pdP7XFZ5YAQ-iCQ&)vG_n7XZ~xy#I~eH zlanWvQKH0?jH}zlkOA_zqen-9CId=GK9W)VfLi7a)51~ke+6Ygl-267G_X3zhb9k& zmY_iTM*@d_3x~5zCENXf>%Qvhyv#yeB9L36lfr4(?!kfHk3wJW=7G) z3R?v_IBXrPZi%^+W*th0s9%w%uQw0FirqF_0~!GHHl%a-FbOq>IdMy9+hL>h*RzM) zR8&+TRz$)zIhfGwwS@vq8x;v)huEJ;oq;g7e);l&_!QN)6DL~w`UI(kw=07mHhb^f zKtK?M9Ze#;0g!?i9}>+fYhrCGJt6H+Qu^iNMesQglrpM@My_--ta+h ztRSj?WTy@4>;kMhVE)6NC-R?naPV4Px(cI+=HSY8*aD8FJfdBiN5(B8aR2e=1+ArNpz!2JgB*WpDc66@jS)cMKDx0hFt z9_#DtqppGiU=y+}1`$cgUnr6ixoD(g)Px|CJDGe`jH|cI%K*er+TM&ILh3wIuP-Gg zcB9cA^mRC)TPo(?((tj+M-3uihNaspC1p?KMfwBO*|t6#*%0ms*@l~&p`jtnKmI(2 zSYUdZlYtg!0A7e)X=H57!puDArU(!Y;WTj?*aubwtq*SnnG@<^8t%r%`pk^P78F32 z?-viRW1!$5=tS`(aH^2u`PYTND@OuEtrCCXArX=L0RwA&1D;XZjqsItd^eaeG0MUM zz^&6+q2Yjp#5ig#IQAJ*J42R5->%~RVCR6%;8Sqg{)vmbZY%TGU#AVxh_YS*w}*?R zrwe=RhV_Ay+C(^V?AYJ2v8b&D&lkUW;}3ZLP0XhkJISoao5*f>1$Ena=i^IJy8*C6 zGu&J+TnIM46&ZODYQrdMAuEIKthHf?L^O;nv{U`7c(!gmV`^%S;suN%6&`Z!W}zqb z^x){MV$;BiP$-o0*Yp0tZkq_^WuA>F*uH;{;|Kan^%EyhH9>W;zPg&0V2#iW3iTNo z?!!&n7mWH^T27dnGFTiB5dv|dy^|9&;YMU6R&}qK*tHIgmyxU=M$pH{6NFUnc!Jag zM@O|Tq=_WaY7m^^vi8`tIc_3nBJ?MHeS0zr`s#783Q;4Gke;0|h14DLXH(ad;Y`48RL2 zH6#tqRxYlzonR-U)9d*9m4VDEy~%%RX>k$RH#X#NY;BN!K2v_1Fi}eS%N;%(@Wt8n zzTVEj{joi4E2<;Q72`=MDQ#_Y-Y;J{6mQKd%Mf7*-O3)!E=)V5F7z?v8Z+Gqn|-$_ z?usKJ&)HnUj;WuytiC}@qw4xK6!D8t+vjw*8;C06Kkt-gpxB7q01=M`t6gaCFD@%P zo7=9TubpFjEUOiORkHr3y?*g_SCuR-UoJEj-_IYJ@@2gsvODhYzoC85 z&zhTu2kdmh?(5?(nt^$vPp7N9`>C-wfIvoJ9G5LOcVSySd&d6c!^NVI_m1b79@P|> zmu~ENVMy^%lt`#QZ2{C>2j0xpRXR_v5R|+*k$2-cKEe%{XBizx)-N?r0iqfcgLaIX z=gvSU98v?cRdbES5w8I#7eqA6&8_{NRWJ4jU)n5s-KuM90IQNa5q?&mE7VALD>Ww` zO^S&j#EHbP(v^~~HZ?SeaWR#cWww}FT1s)>#`O>8y|2)uV8d<~6pX%o8$`PLdV0yJ zsm+az;G%rERsFj@ZKbOEch*g`rEPvI=qsXjP_)?$t=&K1byIWWZ5cJn@87@W-ND^B zR9zj~Le&gaN^TnUK!65d#vhF@=zRjrk06eYTxICx<_2BSsyAXamw_c$RcDaM)Z=ma zvdl`yFV9ri{~z7&&lSG2zL%I7dt?`S6lg++P`WY7nL$;Ry%fzDVAjiQaU}B2_?DB% zD5J;K+2W&O!lQ2AMthJM^w`zZ2u};s=jR$4?l^o*{?W}S4D$o0aQ=(}vIYrmpfY#a z|4vC^?cFOWsdw}!ho>tQxjuRnkq?;t@(@QpefrOlT|i7E>NP4A<>i6Qf+ZJw6m|5v z^@4nKh>44drDdHkH^;NQs+9aJe=C!gt?dxJP^9)C1aF~SttrAMC^-L;_*3V*rG^e zYg$FGAR>swl$0+Qi>v}>Pa(lNe3+(rI_8QXKYvJkPTS~HlQA^yp>U2gN|#~u=T9m? z85z0SQMpFf0Y_si%cu8zetv!v9bEt(Nq@!Cl5efQ3s))1{59CF67|5uZoWUqdkBqh zsP4v^&<=HXX|Al87>Bs{Rh{PYuf+a?1oNz3Q z`}TWlB2KoNBb&P_l!TGCKDceei$j1C@a3!{yIRL)wKHYJ#734UDkEIYEG)JommA}T zIUSijlGO?<LJAff*ICqJL@i+ou| ze7wA0TU#|4Vvl9Py%KfAx7;tn7#IM({Uq6;L zh5X4VM_x&ZpF09Afw7U({4C=02yIcn1Hor(EJy4ST z+_@8$Aj{|iJ~}8!wc*1idiuHfd1*<>qgh5bH8svIpj*mjX$k7-Ha0d@@rx)g<4~fJ z=hZ6(v3xiS5L5GyjCj08X@z%_n$!qlA!O6p`pMKqIkF4D?>4)lJ0vm?hENV41c27o z)(#G$600G6`^A32%_IGEbT^c?zJ{`n`25Ps3Urgz2B5iTXlU0{qhUqXL-}o^(DlXI_LLSE zl>Qvw|GU;Uq6STNc_4k=mKY;$c}p3LFYextQqW zPoxaPt5Dy4gK<9DCBBy`r2|(ee86lpw*z^*@y2d1~x~r$&@$Ez7w)2xgWBYknflNX@8WHYK5o6{SBt` z{X>;~k~D%ri#nDbOs}Kec#rc~x12KBvHW10x$5Q+$KUxqM=CRka~v+|eP-UaW+qNf zdJE!rc~13xlkIk!h(FW*I@I80!KIk0iG5EQY%VY`4}H&?hpt!o-7g2rhIuBQOlVZj zzLx&mYuQ~34jo#{ ztW=z19csG#V7odytq)CT!NEzPQe}KPWS9= z(^;kSWNq@*g0l73I;P9fSEEPW6vsWwCi~Am+g*Nc>(jibtEy5@6FN$+73p-*o_gWp zq#2{P&&$_8sS%BTyy1nN&d)g3$ev|nootgHUWKGc8{UNa-Q7X$93wjBQVE|1U5fqM z4}O?lF_>-nA-DYcRKJH(*r~U~hS!%yw6x}LX)%Sl7I#@lB|OWpkmBiie&CkbSAGuL zX&nk{#C3KA1P%6b!quZ5Rner^J(R^o9}R~3=NeXJ=29B9Xzpvj7?wH|zWU2^d0;?9 zn3f_~*27x$+*D$wj9o)_Ph8h8dWUx(u#@uy3|hoZfxlT=a+_=*5#f>zc~~RPU%t%o zc|C(;OS?RYIHuhv&3CI%!l}?Sjb}r47r68r_XwQ6R9(v>wbg7ViTHAV#z3szj#5&G z7`if9Sdc-Lxsf-LhypbSfh)lTqaOiX7`C!9EQc+|-4qWk4spgF85$e&LqcEj(6w^a zWY-p;c@DO=*I4_X0Mdj7_D(4_m5`{n1TpxYIKIyfQVMc&l|4obVczhgrltmF za*xzcl~pdEWZK*xg`l@KG)B}QMF3cwon6?l&)s9+|G5952CqKzFw}IJnW9 zB}_Y@s8Ned9&L+vv@?%oeX9uY12I3qP4u)Gi{EW=#M&Wc7YXey^OBa5qVQBMyr=fi zlPh5eZ))mi%b_3a`SZWor$}t73(L#W($cP-)=W?OfwkZ}1c5vqRcN3Cz{nry4xDPA3 zU=3LF`Zo$~JFk$WCDMY=Oo)$X5C$@Xcq}P7c{?8;)Ef?Xw5v3eYVV}@9`4SHVZ=Su z)zww5e3wq&Rcya2*m-NIvH)vt`^Ra2cX!#5d+bJM&iJ6a>&FlKpr9bdA8>Q<;KOKs zh_|!BU)E8z{ko2^1>mP*($WU{`r(oY4#CN7$NLDs0Ps^(TtK?%b_og-)W#FHikB}x z+rJ1{AV=^=(%Tl&$;lbg8QuZV8@6$!Ms+O~-8(HUm!Y=tbqa|qa^JHO@hAfU5J4P) zr-LpLHQHlw;?J%iEl2Z-aJ8d6H6W|-KY;R#xHxqLKvJ@?rUsdf&ezvzq^7#MMYQsh zs;YGNa0+WmB?L`3{)K1*^}U$wRKQbHGxW$VCHJ8tUS7q-+*iUwy+DT1sJI88w`Y$_ zr_Dff!fZ%9i*v{}A305mbvznQwKO%~sw38bn}^%>q;xYbf^>gk?dR!AS^DAJA*RPe3$9{DO?2GY1LIDm!?>x$+%S%fL$-rEW z!~=a-aE?NHZ`xg0Zghh$8!(At)Sw~-p$QNY;PPla|L&7wa}RMA77EQ>sLK{~>Ys076;MRyD1v7^Mi?J`hTrq^NPxm-Kv%DFXNQ8}3Ttv7 z@Wm{nz1o!Z>4B?dJ;s_z{o1`O?h$Dptcd_fbVOnwW@qCDQ4pL|k&dfFaXbM#iG!mM zwEl3-DZ{66yJgQ2H=#y>Rv95Nk&U2F!@blz-SEcy0YsMwJe4+;v=8YsAX7pRZ&-?nf`mF-BwQOy%X(b$ zOv?P`U9P|D9~)an$;B=>^(dEw;v_JyRR!r^MS4 zYtoxt+1PQR(6rlyst=JaU`SctH}0ui|6D0q3N>=7JL#dn<>M3drktwv9hwl4)OF<=$538)nLPB z>O=z$;R_EI&h=x4JkfvU=i>tqjEH8j^mDD2c#X0UBlsokBjxvl_*%ef7!6E;*6KWs)-|C-Mcq2IXUdTh1k58N;*@{ zBruOE^^oh=fdj&{<2c#4Y&I%XX>M+4U}0sg;=Z3bKQ;Apd|ZH^A2~&cB+}V5KQ|q$ z(~z4QJT>WDA!IIIp$Sa`fJILCtnWdVx#G{|8#uhM(;!8YPGH-vH1nRv3YEy!x#CLX zZ()4n*RBy3P67~>&%qHoQHmfuWf=Rs*KeX6ixX~R8lQas{?yzY+KG3C0+g<`l~p`c zyZvIQ%{}BG6P4?0_-Npi{_CsrC=dO0;J_l1(yNY+H`rUzqI156HBvh+F_E#Tplvj8 zb3?HtLxzYB43xpSR)=c64kq=zja!8FEoyzR({~^c}{&xTm}4 zqRfjfAe0fbzSi9>AR*Q|>ha9^ zvrDm4(YI60gV+qCv%cBZe0RX#X_kiz2L|?96W@-`_^u8NWK374QVP3%LGNTP;$(DW zBn-EDKigT%69i(G4*65mpn|#GyY^Q1w-NVZ8U;(0>6)Wjci)^zKgnyWuGUCoG2OAl zKDe?fbwnP?CDV1_~ zUDMAzYVZr|gVVp*oV?Qxs7uf%FNNnzIFhynBQcE8)%*_=F;X8}H2pNg+vy=}HIKJsmq{LJb2veq(lu6K{*wwE!<3j;*keYO9; z;kuaDf5Q9!De^}m^8d&PI{W|RE|%L2{!wn87N}`SeW?=@$r>T#y8X$?=$K{kanEJh z5N9_DZe~x{8=IzFM7&al;q}l$@RQy4`W5KQAotv%-Yb=|+e;~YcPMJ60rNdx0QA?j zV>Ta0gCHI>wvGY+3z1r;VOlzzq_L-pd8|CinyCZW>;{fT`b+{6*Xw@;BZL+bUev+olqTv zU3kM6)5xVOTzmFNtzxo%T%G*EgUH?wdKIzg-xNx^;K{r-+S_>7?5HF0Z zNj2_EPB+|a?-7bgZu_4If3lbvIaTleQ1;@kOR|Z8$+p$?xD*2j{^_A~rxPEmvZ374 zRW=bz?DTQ_v#wtzxwHS);9z8P+x3yj;W6ZyC@UzSD21L`JcQcq3ykhO^b>H?=L%8g zPyzNSjHGQU3auHuf`Z5uCQW``CCv#YEI%1y325F={YU*Y@xYs%R!1osmDdLf-Eiq% zxRxD1wCsaUm7aW2@bNwJMIqP3Q#SKSAGaqXha5PNQjJ%SZjFS9c`-ns(BJo7p zbkl!(!zEYS3-&($=Qjwg9(TtZ#Hd%eaQ?i~c~>_d_X`e=9)!dH^_viyF`Vs$j=G$e zk`SU^IiQ>Ztx^Kdf4#@n?#g-h3vM14TwMs77p}ND+xYNoy9+T*0^z8ZzGl9Lb;$n$ D4;9W$ literal 57062 zcmb@tWl)ga_XUciw35;wNOyNgcY}0ycc%i%wq{y*Gt z81cz-&U5zOYpuNuRge=$M!-dYfPg@jln_yZfOvBWesbQzf+O$Wu@0HW^= zekCWA8YHoDJYDGNjp5&J3PyWj2sqX^Z$3bhhtObFcXoY!^O5C3Z|jKwLUx3Mj>6~Z z;pK&znzt<;5hDJ(J+u74ut)fF%a*cePWdh?>5LN z*i{Z4^FqV4~iNBz$li8sDM_|Q

gH2c{@qG!3Q5{r*IOL2 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

7^9j0^lG5-`K>j@po`bNMRLvBJ+a3 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 Date: Wed, 11 Aug 2021 19:38:48 +0200 Subject: [PATCH 27/46] fix master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx | 5 ++--- yarn.lock | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 43ca5bd52e..f862fa66fe 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -100,9 +100,8 @@ export const RepoUrlPicker = ({ return await scaffolderApi.getIntegrationsList({ allowedHosts }); }); - const { host, owner, repo, organization, workspace, project } = splitFormData( - formData, - ); + const { host, owner, repo, organization, workspace, project } = + splitFormData(formData); const updateHost = useCallback( (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { onChange( diff --git a/yarn.lock b/yarn.lock index 52f3875438..1b5e65cdda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18659,6 +18659,11 @@ luxon@2.0.2, luxon@^2.0.2: resolved "https://registry.npmjs.org/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== +luxon@^1.27.0: + version "1.28.0" + resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" + integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" From 29f7cfffb7a3cb545797296aab94160a875af96b Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Sun, 1 Aug 2021 20:38:17 -0600 Subject: [PATCH 28/46] Add resolveCatalogMemberClaims method Signed-off-by: Tim Hansen --- .changeset/beige-colts-pump.md | 5 + docs/auth/identity-resolver.md | 41 +++++++ .../lib/catalog/CatalogIdentityClient.test.ts | 101 +++++++++++++++++- .../src/lib/catalog/CatalogIdentityClient.ts | 86 ++++++++++++++- 4 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 .changeset/beige-colts-pump.md diff --git a/.changeset/beige-colts-pump.md b/.changeset/beige-colts-pump.md new file mode 100644 index 0000000000..a6c5fe1e1c --- /dev/null +++ b/.changeset/beige-colts-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `resolveCatalogMemberClaims` utility to query the catalog for additional authentication claims within sign-in resolvers. diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 814ff63729..38d7567172 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -111,6 +111,47 @@ export default async function createPlugin({ ... ``` +## Resolving membership through the catalog + +If you want to provide additional claims through Sign-In resolvers but still +have the software catalog handle group (and transitive group) membership, you +can do this using the `CatalogIdentityClient` provided as context to Sign-In +resolvers: + +```ts +export default async function createPlugin({ + ... +}: PluginEnvironment): Promise { + return await createRouter({ + ... + providerFactories: { + google: createGoogleProvider({ + signIn: { + resolver: async ({ profile: { email } }, ctx) => { + const [sub] = email?.split('@') ?? ''; + // Fetch from an external system that returns entity claims like: + // 'user:default/breanna.davison' + const ent = await externalSystemClient.getUsernames(email); + + // Resolve group membership from the Backstage catalog + const claims = await ctx.catalogIdentityClient.resolveCatalogMemberClaims( + sub, + ent, + ctx.logger, + ); + const token = await ctx.tokenIssuer.issueToken(claims); + return { sub, token }; + }, + }, + }), + ... +``` + +The `resolveCatalogMemberClaims` method will retrieve the `sub` and `ent` +entities from the catalog, if possible, and check for +[memberOf](../features/software-catalog/well-known-relations.md#memberof-and-hasmember) +relations to add additional entity claims. + ## AuthHandler Similar to a custom sign-in resolver, you can also write a custom auth handler diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 329db316ab..6f05bef5f8 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -15,7 +15,11 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { UserEntity } from '@backstage/catalog-model'; +import { + RELATION_MEMBER_OF, + UserEntity, + UserEntityV1alpha1, +} from '@backstage/catalog-model'; import { TokenIssuer } from '../../identity'; import { CatalogIdentityClient } from './CatalogIdentityClient'; @@ -37,12 +41,12 @@ describe('CatalogIdentityClient', () => { afterEach(() => jest.resetAllMocks()); - it('passes through the correct search params', async () => { + it('findUser passes through the correct search params', async () => { catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] }); tokenIssuer.issueToken.mockResolvedValue('my-token'); const client = new CatalogIdentityClient({ - catalogApi: catalogApi, - tokenIssuer: tokenIssuer, + catalogApi, + tokenIssuer, }); await client.findUser({ annotations: { key: 'value' } }); @@ -62,4 +66,93 @@ describe('CatalogIdentityClient', () => { }, }); }); + + it('resolveCatalogMemberClaims resolves membership', async () => { + const mockUsers: Array = [ + { + apiVersion: 'backstage.io/v1beta1', + kind: 'User', + metadata: { + name: 'inigom', + }, + spec: { + memberOf: ['team-a'], + }, + relations: [ + { + type: RELATION_MEMBER_OF, + target: { + kind: 'Group', + namespace: 'default', + name: 'team-a', + }, + }, + ], + }, + { + apiVersion: 'backstage.io/v1beta1', + kind: 'User', + metadata: { + name: 'mpatinkin', + namespace: 'reality', + }, + spec: { + memberOf: ['screen-actors-guild'], + }, + relations: [ + { + type: RELATION_MEMBER_OF, + target: { + kind: 'Group', + namespace: 'reality', + name: 'screen-actors-guild', + }, + }, + ], + }, + ]; + catalogApi.getEntities.mockResolvedValueOnce({ items: mockUsers }); + + const client = new CatalogIdentityClient({ + catalogApi, + tokenIssuer, + }); + + const claims = await client.resolveCatalogMemberClaims('inigom', [ + 'User:default/imontoya', + 'User:reality/mpatinkin', + ]); + + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: [ + { + kind: 'user', + 'metadata.namespace': 'default', + 'metadata.name': 'inigom', + }, + { + kind: 'user', + 'metadata.namespace': 'default', + 'metadata.name': 'imontoya', + }, + { + kind: 'user', + 'metadata.namespace': 'reality', + 'metadata.name': 'mpatinkin', + }, + ], + }); + + expect(claims).toMatchObject({ + claims: { + sub: 'inigom', + ent: [ + 'user:default/imontoya', + 'user:reality/mpatinkin', + 'group:default/team-a', + 'group:reality/screen-actors-guild', + ], + }, + }); + }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 07a7190503..c1f54f74c8 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -14,10 +14,17 @@ * limitations under the License. */ +import { Logger } from 'winston'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; -import { UserEntity } from '@backstage/catalog-model'; -import { TokenIssuer } from '../../identity'; +import { + EntityName, + parseEntityRef, + RELATION_MEMBER_OF, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; +import { TokenIssuer, TokenParams } from '../../identity'; type UserQuery = { annotations: Record; @@ -64,4 +71,79 @@ export class CatalogIdentityClient { return items[0] as UserEntity; } + + /** + * Resolve additional entity claims from the catalog, using the passed-in subject and entity + * claims. Designed to be used within a `signInResolver` where additional entity claims might be + * provided, but group membership and transient group membership lean on imported catalog + * relations. + * + * Returns a claim structure that can be passed directly to `issueToken`, with the same sub and a + * superset of `ent` claims. + */ + async resolveCatalogMemberClaims( + sub: string, + ent: string[], + logger?: Logger, + ): Promise { + const subRef: EntityName = parseEntityRef(sub, { + defaultKind: 'user', + defaultNamespace: 'default', + }); + + let entityRefs: Array = []; + if (ent) { + entityRefs = ent + .map((ref: string) => { + try { + const parsedRef = parseEntityRef(ref.toLocaleLowerCase('en-US')); + return parsedRef; + } catch { + logger?.debug( + `Failed to parse entityRef from '${sub}' ent claim: ${ref}, ignoring`, + ); + return null; + } + }) + .filter((ref): ref is EntityName => ref !== null); + } + + const filter = [subRef].concat(entityRefs).map(ref => ({ + kind: ref.kind, + 'metadata.namespace': ref.namespace, + 'metadata.name': ref.name, + })); + const entities = await this.catalogApi + .getEntities({ filter }) + .then(r => r.items); + + if (entityRefs.length !== entities.length) { + const foundEntityNames = entities.map(stringifyEntityRef); + const missingEntityNames = entityRefs + .map(stringifyEntityRef) + .filter(s => !foundEntityNames.includes(s)); + logger?.debug( + `Entities not found for '${sub}' claims: ${missingEntityNames.join()}`, + ); + } + + const memberOf = entities.flatMap( + e => + e!.relations + ?.filter(r => r.type === RELATION_MEMBER_OF) + .map(r => r.target) ?? [], + ); + + const newEnt = [...new Set(entityRefs.concat(memberOf))].map( + stringifyEntityRef, + ); + + logger?.debug(`Found claims for ${sub} in the catalog: ${newEnt.join()}`); + return { + claims: { + sub, + ent: newEnt, + }, + }; + } } From 762520d1baee561c588e6eee488da3d90d86b959 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 3 Aug 2021 08:12:04 -0600 Subject: [PATCH 29/46] Structure args, return inflated ent Signed-off-by: Tim Hansen --- docs/auth/identity-resolver.md | 12 ++++---- .../lib/catalog/CatalogIdentityClient.test.ts | 25 +++++++---------- .../src/lib/catalog/CatalogIdentityClient.ts | 28 +++++++++---------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 38d7567172..0dc1c7434e 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -130,16 +130,18 @@ export default async function createPlugin({ resolver: async ({ profile: { email } }, ctx) => { const [sub] = email?.split('@') ?? ''; // Fetch from an external system that returns entity claims like: - // 'user:default/breanna.davison' + // ['user:default/breanna.davison', ...] const ent = await externalSystemClient.getUsernames(email); // Resolve group membership from the Backstage catalog - const claims = await ctx.catalogIdentityClient.resolveCatalogMemberClaims( + const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMemberClaims({ sub, ent, - ctx.logger, - ); - const token = await ctx.tokenIssuer.issueToken(claims); + logger: ctx.logger, + }); + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: id, fullEnt }, + }); return { sub, token }; }, }, diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 6f05bef5f8..f5c3219e10 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -118,10 +118,10 @@ describe('CatalogIdentityClient', () => { tokenIssuer, }); - const claims = await client.resolveCatalogMemberClaims('inigom', [ - 'User:default/imontoya', - 'User:reality/mpatinkin', - ]); + const claims = await client.resolveCatalogMemberClaims({ + sub: 'inigom', + ent: ['User:default/imontoya', 'User:reality/mpatinkin'], + }); expect(catalogApi.getEntities).toHaveBeenCalledWith({ filter: [ @@ -143,16 +143,11 @@ describe('CatalogIdentityClient', () => { ], }); - expect(claims).toMatchObject({ - claims: { - sub: 'inigom', - ent: [ - 'user:default/imontoya', - 'user:reality/mpatinkin', - 'group:default/team-a', - 'group:reality/screen-actors-guild', - ], - }, - }); + expect(claims).toMatchObject([ + 'user:default/imontoya', + 'user:reality/mpatinkin', + 'group:default/team-a', + 'group:reality/screen-actors-guild', + ]); }); }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index c1f54f74c8..7b29ed859b 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -24,12 +24,18 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { TokenIssuer, TokenParams } from '../../identity'; +import { TokenIssuer } from '../../identity'; type UserQuery = { annotations: Record; }; +type MemberClaimQuery = { + sub: string; + ent: string[]; + logger?: Logger; +}; + /** * A catalog client tailored for reading out identity data from the catalog. */ @@ -78,14 +84,13 @@ export class CatalogIdentityClient { * provided, but group membership and transient group membership lean on imported catalog * relations. * - * Returns a claim structure that can be passed directly to `issueToken`, with the same sub and a - * superset of `ent` claims. + * Returns a superset of the `ent` argument that can be passed directly to `issueToken` as `ent`. */ - async resolveCatalogMemberClaims( - sub: string, - ent: string[], - logger?: Logger, - ): Promise { + async resolveCatalogMemberClaims({ + sub, + ent, + logger, + }: MemberClaimQuery): Promise { const subRef: EntityName = parseEntityRef(sub, { defaultKind: 'user', defaultNamespace: 'default', @@ -139,11 +144,6 @@ export class CatalogIdentityClient { ); logger?.debug(`Found claims for ${sub} in the catalog: ${newEnt.join()}`); - return { - claims: { - sub, - ent: newEnt, - }, - }; + return newEnt; } } From 4a019712b443a63d4a217e085aa9df530368b3d4 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 3 Aug 2021 08:25:40 -0600 Subject: [PATCH 30/46] Tweak method name Signed-off-by: Tim Hansen --- .changeset/beige-colts-pump.md | 2 +- docs/auth/identity-resolver.md | 6 +++--- .../src/lib/catalog/CatalogIdentityClient.test.ts | 4 ++-- .../auth-backend/src/lib/catalog/CatalogIdentityClient.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/beige-colts-pump.md b/.changeset/beige-colts-pump.md index a6c5fe1e1c..c85a260b31 100644 --- a/.changeset/beige-colts-pump.md +++ b/.changeset/beige-colts-pump.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': patch --- -Added `resolveCatalogMemberClaims` utility to query the catalog for additional authentication claims within sign-in resolvers. +Added `resolveCatalogMembership` utility to query the catalog for additional authentication claims within sign-in resolvers. diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 0dc1c7434e..482d4d0314 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -134,7 +134,7 @@ export default async function createPlugin({ const ent = await externalSystemClient.getUsernames(email); // Resolve group membership from the Backstage catalog - const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMemberClaims({ + const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ sub, ent, logger: ctx.logger, @@ -149,8 +149,8 @@ export default async function createPlugin({ ... ``` -The `resolveCatalogMemberClaims` method will retrieve the `sub` and `ent` -entities from the catalog, if possible, and check for +The `resolveCatalogMembership` method will retrieve the `sub` and `ent` entities +from the catalog, if possible, and check for [memberOf](../features/software-catalog/well-known-relations.md#memberof-and-hasmember) relations to add additional entity claims. diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index f5c3219e10..c55ae17919 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -67,7 +67,7 @@ describe('CatalogIdentityClient', () => { }); }); - it('resolveCatalogMemberClaims resolves membership', async () => { + it('resolveCatalogMembership resolves membership', async () => { const mockUsers: Array = [ { apiVersion: 'backstage.io/v1beta1', @@ -118,7 +118,7 @@ describe('CatalogIdentityClient', () => { tokenIssuer, }); - const claims = await client.resolveCatalogMemberClaims({ + const claims = await client.resolveCatalogMembership({ sub: 'inigom', ent: ['User:default/imontoya', 'User:reality/mpatinkin'], }); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 7b29ed859b..179843839c 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -86,7 +86,7 @@ export class CatalogIdentityClient { * * Returns a superset of the `ent` argument that can be passed directly to `issueToken` as `ent`. */ - async resolveCatalogMemberClaims({ + async resolveCatalogMembership({ sub, ent, logger, From 4f11e67cc47ebb5830f21a9aa8335c7672b73eb2 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 3 Aug 2021 08:45:07 -0600 Subject: [PATCH 31/46] Simplify signature Signed-off-by: Tim Hansen --- docs/auth/identity-resolver.md | 7 +- .../lib/catalog/CatalogIdentityClient.test.ts | 4 +- .../src/lib/catalog/CatalogIdentityClient.ts | 65 ++++++++----------- 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 482d4d0314..e772832c1e 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -135,8 +135,7 @@ export default async function createPlugin({ // Resolve group membership from the Backstage catalog const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ - sub, - ent, + entityRefs: [sub].concat(ent), logger: ctx.logger, }); const token = await ctx.tokenIssuer.issueToken({ @@ -149,8 +148,8 @@ export default async function createPlugin({ ... ``` -The `resolveCatalogMembership` method will retrieve the `sub` and `ent` entities -from the catalog, if possible, and check for +The `resolveCatalogMembership` method will retrieve the referenced entities from +the catalog, if possible, and check for [memberOf](../features/software-catalog/well-known-relations.md#memberof-and-hasmember) relations to add additional entity claims. diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index c55ae17919..6ffcfd6f46 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -119,8 +119,7 @@ describe('CatalogIdentityClient', () => { }); const claims = await client.resolveCatalogMembership({ - sub: 'inigom', - ent: ['User:default/imontoya', 'User:reality/mpatinkin'], + entityRefs: ['inigom', 'User:default/imontoya', 'User:reality/mpatinkin'], }); expect(catalogApi.getEntities).toHaveBeenCalledWith({ @@ -144,6 +143,7 @@ describe('CatalogIdentityClient', () => { }); expect(claims).toMatchObject([ + 'user:default/inigom', 'user:default/imontoya', 'user:reality/mpatinkin', 'group:default/team-a', diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 179843839c..6e8494f805 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -31,8 +31,7 @@ type UserQuery = { }; type MemberClaimQuery = { - sub: string; - ent: string[]; + entityRefs: string[]; logger?: Logger; }; @@ -79,41 +78,33 @@ export class CatalogIdentityClient { } /** - * Resolve additional entity claims from the catalog, using the passed-in subject and entity - * claims. Designed to be used within a `signInResolver` where additional entity claims might be - * provided, but group membership and transient group membership lean on imported catalog - * relations. + * Resolve additional entity claims from the catalog, using the passed-in entity names. Designed + * to be used within a `signInResolver` where additional entity claims might be provided, but + * group membership and transient group membership lean on imported catalog relations. * - * Returns a superset of the `ent` argument that can be passed directly to `issueToken` as `ent`. + * Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`. */ async resolveCatalogMembership({ - sub, - ent, + entityRefs, logger, }: MemberClaimQuery): Promise { - const subRef: EntityName = parseEntityRef(sub, { - defaultKind: 'user', - defaultNamespace: 'default', - }); + let resolvedEntityRefs: Array = []; + resolvedEntityRefs = entityRefs + .map((ref: string) => { + try { + const parsedRef = parseEntityRef(ref.toLocaleLowerCase('en-US'), { + defaultKind: 'user', + defaultNamespace: 'default', + }); + return parsedRef; + } catch { + logger?.debug(`Failed to parse entityRef from ${ref}, ignoring`); + return null; + } + }) + .filter((ref): ref is EntityName => ref !== null); - let entityRefs: Array = []; - if (ent) { - entityRefs = ent - .map((ref: string) => { - try { - const parsedRef = parseEntityRef(ref.toLocaleLowerCase('en-US')); - return parsedRef; - } catch { - logger?.debug( - `Failed to parse entityRef from '${sub}' ent claim: ${ref}, ignoring`, - ); - return null; - } - }) - .filter((ref): ref is EntityName => ref !== null); - } - - const filter = [subRef].concat(entityRefs).map(ref => ({ + const filter = resolvedEntityRefs.map(ref => ({ kind: ref.kind, 'metadata.namespace': ref.namespace, 'metadata.name': ref.name, @@ -124,12 +115,10 @@ export class CatalogIdentityClient { if (entityRefs.length !== entities.length) { const foundEntityNames = entities.map(stringifyEntityRef); - const missingEntityNames = entityRefs + const missingEntityNames = resolvedEntityRefs .map(stringifyEntityRef) .filter(s => !foundEntityNames.includes(s)); - logger?.debug( - `Entities not found for '${sub}' claims: ${missingEntityNames.join()}`, - ); + logger?.debug(`Entities not found for refs ${missingEntityNames.join()}`); } const memberOf = entities.flatMap( @@ -139,11 +128,11 @@ export class CatalogIdentityClient { .map(r => r.target) ?? [], ); - const newEnt = [...new Set(entityRefs.concat(memberOf))].map( + const newEntityRefs = [...new Set(resolvedEntityRefs.concat(memberOf))].map( stringifyEntityRef, ); - logger?.debug(`Found claims for ${sub} in the catalog: ${newEnt.join()}`); - return newEnt; + logger?.debug(`Found catalog membership: ${newEntityRefs.join()}`); + return newEntityRefs; } } From d051bc2fdaa40e2e531fb11c60e5667a63cb7e38 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 11 Aug 2021 12:16:59 -0600 Subject: [PATCH 32/46] Minor cleanup Signed-off-by: Tim Hansen --- docs/auth/identity-resolver.md | 2 +- .../src/lib/catalog/CatalogIdentityClient.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index e772832c1e..7ec6ec7117 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -139,7 +139,7 @@ export default async function createPlugin({ logger: ctx.logger, }); const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id, fullEnt }, + claims: { sub, ent: fullEnt }, }); return { sub, token }; }, diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 6e8494f805..02fe9818cb 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -88,8 +88,7 @@ export class CatalogIdentityClient { entityRefs, logger, }: MemberClaimQuery): Promise { - let resolvedEntityRefs: Array = []; - resolvedEntityRefs = entityRefs + const resolvedEntityRefs = entityRefs .map((ref: string) => { try { const parsedRef = parseEntityRef(ref.toLocaleLowerCase('en-US'), { @@ -98,7 +97,7 @@ export class CatalogIdentityClient { }); return parsedRef; } catch { - logger?.debug(`Failed to parse entityRef from ${ref}, ignoring`); + logger?.warn(`Failed to parse entityRef from ${ref}, ignoring`); return null; } }) @@ -128,9 +127,9 @@ export class CatalogIdentityClient { .map(r => r.target) ?? [], ); - const newEntityRefs = [...new Set(resolvedEntityRefs.concat(memberOf))].map( - stringifyEntityRef, - ); + const newEntityRefs = [ + ...new Set(resolvedEntityRefs.concat(memberOf).map(stringifyEntityRef)), + ]; logger?.debug(`Found catalog membership: ${newEntityRefs.join()}`); return newEntityRefs; From 5a4d10c04ee71e5ae95e6b63e7c36c9e8dc8e7b2 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Tue, 3 Aug 2021 15:30:56 +0200 Subject: [PATCH 33/46] Add details page for builds Signed-off-by: Niklas Granander --- plugins/xcmetrics/src/api/XcmetricsClient.ts | 160 +++++++-------- .../src/api/__mocks__/XcmetricsClient.ts | 114 ++++++++++- plugins/xcmetrics/src/api/types.ts | 68 +++++++ .../AccordionComponent.test.tsx | 51 +++++ .../AccordionComponent/AccordionComponent.tsx | 67 +++++++ .../components/AccordionComponent/index.ts | 16 ++ .../BuildDetailsComponent.test.tsx | 101 ++++++++++ .../BuildDetailsComponent.tsx | 184 ++++++++++++++++++ .../components/BuildDetailsComponent/index.ts | 16 ++ .../BuildListComponent.test.tsx | 22 +++ .../BuildListComponent/BuildListComponent.tsx | 19 +- .../src/components/BuildTableColumns.tsx | 20 +- .../components/BuildsPage/BuildsPage.test.tsx | 43 ++++ .../src/components/BuildsPage/BuildsPage.tsx | 48 +++++ .../src/components/BuildsPage/index.ts | 16 ++ .../StatusIconComponent.test.tsx | 38 ++++ .../StatusIconComponent.tsx | 35 ++++ .../components/StatusIconComponent/index.ts | 16 ++ .../XcmetricsLayout/XcmetricsLayout.test.tsx | 3 + .../XcmetricsLayout/XcmetricsLayout.tsx | 4 +- plugins/xcmetrics/src/utils/format.ts | 8 +- 21 files changed, 942 insertions(+), 107 deletions(-) create mode 100644 plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx create mode 100644 plugins/xcmetrics/src/components/AccordionComponent/index.ts create mode 100644 plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx create mode 100644 plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts create mode 100644 plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx create mode 100644 plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx create mode 100644 plugins/xcmetrics/src/components/BuildsPage/index.ts create mode 100644 plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx create mode 100644 plugins/xcmetrics/src/components/StatusIconComponent/index.ts diff --git a/plugins/xcmetrics/src/api/XcmetricsClient.ts b/plugins/xcmetrics/src/api/XcmetricsClient.ts index 81b3e78a31..7aee5453bd 100644 --- a/plugins/xcmetrics/src/api/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/XcmetricsClient.ts @@ -20,9 +20,13 @@ import { DateTime } from 'luxon'; import { Build, BuildCount, + BuildError, BuildFilters, + BuildHost, + BuildMetadata, BuildStatusResult, BuildTime, + BuildWarning, PaginationResult, XcmetricsApi, } from './types'; @@ -39,24 +43,12 @@ export class XcmetricsClient implements XcmetricsApi { } async getBuild(id: string): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build/${id}`); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - + const response = await this.get(`/build/${id}`); return ((await response.json()) as Record<'build', Build>).build; } async getBuilds(limit: number = 10): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build?per=${limit}`); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - + const response = await this.get(`/build?per=${limit}`); return ((await response.json()) as PaginationResult).items; } @@ -65,80 +57,88 @@ export class XcmetricsClient implements XcmetricsApi { page?: number, perPage?: number, ): Promise> { + const response = await this.post('/build/filter', { + from: DateTime.fromISO(filters.from) + .startOf('day') + .toISO({ suppressMilliseconds: true }), + to: DateTime.fromISO(filters.to) + .endOf('day') + .startOf('second') + .toISO({ suppressMilliseconds: true }), + status: filters.buildStatus, + projectName: filters.project, + page, + per: perPage, + }); + + return (await response.json()) as PaginationResult; + } + + async getBuildCounts(days: number): Promise { + const response = await this.get(`/statistics/build/count?days=${days}`); + return (await response.json()) as BuildCount[]; + } + + async getBuildErrors(buildId: string): Promise { + const response = await this.get(`/build/error/${buildId}`); + return (await response.json()) as BuildError[]; + } + + async getBuildHost(buildId: string): Promise { + const response = await this.get(`/build/host/${buildId}`); + return (await response.json()) as BuildHost; + } + + async getBuildMetadata(buildId: string): Promise { + const response = await this.get(`/build/metadata/${buildId}`); + return ((await response.json()) as Record<'metadata', BuildMetadata>) + .metadata; + } + + async getBuildTimes(days: number): Promise { + const response = await this.get(`/statistics/build/time?days=${days}`); + return (await response.json()) as BuildTime[]; + } + + async getBuildStatuses(limit: number): Promise { + const response = await this.get(`/statistics/build/status?per=${limit}`); + return ((await response.json()) as PaginationResult) + .items; + } + + async getBuildWarnings(buildId: string): Promise { + const response = await this.get(`/build/warning/${buildId}`); + return (await response.json()) as BuildWarning[]; + } + + async getProjects(): Promise { + const response = await this.get('/build/project'); + return (await response.json()) as string[]; + } + + private async get(path: string): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build/filter`, { + const response = await fetch(`${baseUrl}${path}`); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response; + } + + private async post(path: string, body: Object): Promise { + const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; + const response = await fetch(`${baseUrl}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - from: DateTime.fromISO(filters.from) - .startOf('day') - .toISO({ suppressMilliseconds: true }), - to: DateTime.fromISO(filters.to) - .endOf('day') - .startOf('second') - .toISO({ suppressMilliseconds: true }), - status: filters.buildStatus, - projectName: filters.project, - page, - per: perPage, - }), + body: JSON.stringify(body), }); if (!response.ok) { throw await ResponseError.fromResponse(response); } - return (await response.json()) as PaginationResult; - } - - async getBuildCounts(days: number): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch( - `${baseUrl}/statistics/build/count?days=${days}`, - ); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return (await response.json()) as BuildCount[]; - } - - async getBuildTimes(days: number): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch( - `${baseUrl}/statistics/build/time?days=${days}`, - ); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return (await response.json()) as BuildTime[]; - } - - async getBuildStatuses(limit: number): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch( - `${baseUrl}/statistics/build/status?per=${limit}`, - ); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return ((await response.json()) as PaginationResult) - .items; - } - - async getProjects(): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build/project`); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return (await response.json()) as string[]; + return response; } } diff --git a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts index 430fbb1006..5258215175 100644 --- a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts @@ -14,9 +14,20 @@ * limitations under the License. */ -import { Build, BuildFilters, XcmetricsApi } from '../types'; +import { + Build, + BuildCount, + BuildError, + BuildFilters, + BuildHost, + BuildMetadata, + BuildStatusResult, + BuildTime, + BuildWarning, + XcmetricsApi, +} from '../types'; -export const mockBuild = { +export const mockBuild: Build = { userid: 'userid1', warningCount: 1, duration: 1, @@ -34,22 +45,99 @@ export const mockBuild = { id: 'buildId', buildStatus: 'succeeded', compilationDuration: 1, - schema: 'Schema', + schema: 'SchemaName', compiledCount: 1, endTimestamp: '2021-01-01T00:00:01Z', userid256: 'userId256', machineName: 'Example_Machine', wasSuspended: true, -} as Build; +}; -export const mockBuildCount = { day: '2021-07-10', builds: 10, errors: 1 }; -export const mockBuildTime = { +export const mockBuildCount: BuildCount = { + day: '2021-07-10', + builds: 10, + errors: 1, +}; + +export const mockBuildError: BuildError = { + detail: `\/Users\/\/myproject\/Sources\/MyClass.m:241:97:// / error: instance method 'fetch' not found ; did you mean 'fetchIt'?\r + myclass:[self.myService fetch]\r ^~~~~~~~~~~~~~\r fetch\r + 1 error generated.\r"`, + characterRangeEnd: 13815, + id: '3E6EF185-6AC1-4E95-87E8-E305F41916E9', + endingColumn: 97, + parentIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_8860', + day: '2020-11-02T00:00:00Z', + type: 'clangError', + title: 'Instance method "fetch" not found ; did you mean "fetchIt"?', + endingLine: 241, + severity: 2, + startingLine: 241, + parentType: 'step', + buildIdentifier: 'buildId', + startingColumn: 97, + characterRangeStart: 0, + documentURL: 'file:///Users//myproject/Sources/MyClass.m', +}; + +export const mockBuildHost: BuildHost = { + id: '9DD5508D-4AD9-4C1C-AB7C-45BC2183EC51', + swapFreeMb: 1615.25, + hostOsFamily: 'Darwin', + isVirtual: false, + uptimeSeconds: 1602055187, + hostModel: 'MacBookPro14,2', + hostOsVersion: '10.15.7', + day: '2020-10-26T00:00:00Z', + cpuCount: 4, + swapTotalMb: 7168, + hostOs: 'Mac OS X', + hostArchitecture: 'x86_64', + memoryTotalMb: 16384, + timezone: 'CET', + cpuModel: 'Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz', + buildIdentifier: 'buildId', + memoryFreeMb: 24.5234375, + cpuSpeedGhz: 3.5, +}; + +export const mockBuildMetadata: BuildMetadata = { + anotherKey: '42', + thirdKey: 'Third value', + aKey: 'value1', +}; + +export const mockBuildTime: BuildTime = { day: '2021-07-10', durationP50: 1.1, durationP95: 2.1, totalDuration: 3.1, }; -export const mockBuildStatus = { id: 'build_id', status: 'succeeded' }; +export const mockBuildStatus: BuildStatusResult = { + id: 'build_id', + buildStatus: 'succeeded', +}; + +export const mockBuildWarning: BuildWarning = { + detail: null, + characterRangeEnd: 9817, + documentURL: 'file:///Users//myproject/Sources/MyViewController.m', + endingColumn: 22, + id: '5F2011AC-F87F-4EDC-BBC6-2BBA3D789EB3', + parentIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1845', + day: '2020-11-02T00:00:00Z', + type: 'deprecatedWarning', + title: + "'dimsBackgroundDuringPresentation' is deprecated: first deprecated in iOS 12.0", + endingLine: 235, + severity: 1, + startingLine: 235, + parentType: 'step', + clangFlag: '[-Wdeprecated-declarations]', + startingColumn: 22, + buildIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1', + characterRangeStart: 0, +}; export const XcmetricsClient: XcmetricsApi = { getBuild: (id: string) => { @@ -78,15 +166,27 @@ export const XcmetricsClient: XcmetricsApi = { }, }); }, + getBuildErrors: (buildId: string) => { + return Promise.resolve([{ ...mockBuildError, buildIdentifier: buildId }]); + }, getBuildCounts: () => { return Promise.resolve([mockBuildCount, mockBuildCount]); }, + getBuildHost: (buildId: string) => { + return Promise.resolve({ ...mockBuildHost, buildIdentifier: buildId }); + }, + getBuildMetadata: (_buildId: string) => { + return Promise.resolve(mockBuildMetadata); + }, getBuildStatuses: (limit: number) => { return Promise.resolve([mockBuild].slice(0, limit)); }, getBuildTimes: (days: number) => { return Promise.resolve([mockBuildTime, mockBuildTime].slice(0, days)); }, + getBuildWarnings: (buildId: string) => { + return Promise.resolve([{ ...mockBuildWarning, buildIdentifier: buildId }]); + }, getProjects: () => { return Promise.resolve([mockBuild.projectName]); }, diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index c018a67056..8f6ae9f0ae 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -52,6 +52,50 @@ export type BuildCount = { builds: number; }; +export type BuildError = { + detail: string; + characterRangeEnd: number; + id: string; + endingColumn: number; + parentIdentifier: string; + day: string; + type: string; + title: string; + endingLine: number; + severity: number; + startingLine: number; + parentType: string; + buildIdentifier: string; + startingColumn: number; + characterRangeStart: number; + documentURL: string; +}; + +export type BuildHost = { + id: string; + swapFreeMb: number; + hostOsFamily: string; + isVirtual: boolean; + uptimeSeconds: number; + hostModel: string; + hostOsVersion: string; + day: string; + cpuCount: number; + swapTotalMb: number; + hostOs: string; + hostArchitecture: string; + memoryTotalMb: number; + timezone: string; + cpuModel: string; + buildIdentifier: string; + memoryFreeMb: number; + cpuSpeedGhz: number; +}; + +export type BuildMetadata = { + [key: string]: string; +}; + export type BuildTime = { day: string; durationP50: number; @@ -59,6 +103,26 @@ export type BuildTime = { totalDuration: number; }; +export type BuildWarning = { + detail: string | null; + characterRangeEnd: number; + documentURL: string; + endingColumn: number; + id: string; + parentIdentifier: string; + day: string; + type: string; + title: string; + endingLine: number; + severity: number; + startingLine: number; + parentType: string; + clangFlag: string; + startingColumn: number; + buildIdentifier: string; + characterRangeStart: number; +}; + export type PaginationResult = { items: T[]; metadata: { @@ -83,9 +147,13 @@ export interface XcmetricsApi { page?: number, perPage?: number, ): Promise>; + getBuildErrors(buildId: string): Promise; getBuildCounts(days: number): Promise; + getBuildHost(buildId: string): Promise; + getBuildMetadata(buildId: string): Promise; getBuildTimes(days: number): Promise; getBuildStatuses(limit: number): Promise; + getBuildWarnings(buildId: string): Promise; getProjects(): Promise; } diff --git a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx new file mode 100644 index 0000000000..baa8c0e5f2 --- /dev/null +++ b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { AccordionComponent } from './AccordionComponent'; +import userEvent from '@testing-library/user-event'; + +describe('AccordionComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + , + ); + + expect(rendered.getByText('heading')).toBeInTheDocument(); + expect(rendered.getByText('secondaryHeading')).toBeInTheDocument(); + }); + + it('should show content when clicked', async () => { + const rendered = await renderInTestApp( + + Content + , + ); + + expect(rendered.getByText('Content')).not.toBeVisible(); + + userEvent.click(rendered.getByRole('button')); + expect(await rendered.findByText('Content')).toBeVisible(); + }); +}); diff --git a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx new file mode 100644 index 0000000000..d8c4e4f81d --- /dev/null +++ b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Accordion, + AccordionSummary, + Typography, + AccordionDetails, + makeStyles, + createStyles, +} from '@material-ui/core'; +import React, { PropsWithChildren } from 'react'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + heading: { + flexBasis: '33.33%', + flexShrink: 0, + }, + secondaryHeading: { + color: theme.palette.text.secondary, + }, + }), +); + +interface AccordionProps { + id: string; + heading: string; + secondaryHeading?: string | number; + disabled?: boolean; +} + +export const AccordionComponent = ( + props: PropsWithChildren, +) => { + const classes = useStyles(); + + return ( + + } + aria-controls={`${props.id}-content`} + id={`${props.id}-header`} + > + {props.heading} + + {props.secondaryHeading} + + + {props.children} + + ); +}; diff --git a/plugins/xcmetrics/src/components/AccordionComponent/index.ts b/plugins/xcmetrics/src/components/AccordionComponent/index.ts new file mode 100644 index 0000000000..1e71dc81ca --- /dev/null +++ b/plugins/xcmetrics/src/components/AccordionComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './AccordionComponent'; diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx new file mode 100644 index 0000000000..99c5ec20e5 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { BuildDetailsComponent, withRequest } from './BuildDetailsComponent'; +import { xcmetricsApiRef } from '../../api'; + +jest.mock('../../api/XcmetricsClient'); +const client = require('../../api/XcmetricsClient'); + +jest.mock('../AccordionComponent', () => ({ + AccordionComponent: ({ heading }: { heading: string }) => ( +

accordion-{heading}
+ ), +})); + +describe('BuildDetailsComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText('accordion-Host')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Errors')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Warnings')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Metadata')).toBeInTheDocument(); + + expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); + expect( + rendered.getByText(client.mockBuild.projectName), + ).toBeInTheDocument(); + expect(rendered.getByText(client.mockBuild.schema)).toBeInTheDocument(); + }); +}); + +describe('BuildDetailsComponent with request', () => { + const BuildDetails = withRequest(BuildDetailsComponent); + + it('should fetch the build and render', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); + }); + + it('should show an error when API not responding', async () => { + const errorMessage = 'MockErrorMessage'; + client.XcmetricsClient.getBuild = jest + .fn() + .mockRejectedValue({ message: errorMessage }); + + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText(errorMessage)).toBeInTheDocument(); + }); + + it('should show a message when no build is returned from the API', async () => { + client.XcmetricsClient.getBuild = jest.fn().mockReturnValue(undefined); + + const rendered = await renderInTestApp( + + + , + ); + + expect( + rendered.getByText(`Could not load build ${client.mockBuild.id}`), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx new file mode 100644 index 0000000000..99b4c83210 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -0,0 +1,184 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createStyles, Divider, Grid, makeStyles } from '@material-ui/core'; +import React from 'react'; +import { Build, xcmetricsApiRef } from '../../api'; +import { + OverflowTooltip, + Progress, + StructuredMetadataTable, +} from '@backstage/core-components'; +import { Alert } from '@material-ui/lab'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-plugin-api'; +import { formatDuration, formatStatus, formatTime } from '../../utils'; +import { StatusIconComponent as StatusIcon } from '../StatusIconComponent'; +import { BackstageTheme } from '@backstage/theme'; +import { AccordionComponent } from '../AccordionComponent'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + divider: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + }), +); + +interface BuildDetailsProps { + build: Build; +} + +export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { + const classes = useStyles(); + const client = useApi(xcmetricsApiRef); + const hostResult = useAsync(async () => client.getBuildHost(build.id), []); + const errorsResult = useAsync( + async () => client.getBuildErrors(build.id), + [], + ); + const warningsResult = useAsync( + async () => client.getBuildWarnings(build.id), + [], + ); + const metadataResult = useAsync( + async () => client.getBuildMetadata(build.id), + [], + ); + + const buildDetails = { + id: build.id, + project: build.projectName, + schema: build.schema, + category: build.category, + userId: build.userid, + 'started at': formatTime(build.startTimestamp), + 'ended at': formatTime(build.endTimestamp), + duration: formatDuration(build.duration), + status: ( + <> + + {formatStatus(build.buildStatus)} + + ), + CI: build.isCi, + }; + + return ( + + + + + + + + {hostResult.loading && } + {!hostResult.loading && hostResult.value && ( + + )} + + + +
+ {errorsResult.loading && } + {!errorsResult.loading && + errorsResult.value && + errorsResult.value.map((error, idx) => ( +
+ + {idx !== errorsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + +
+ {warningsResult.loading && } + {!warningsResult.loading && + warningsResult.value && + warningsResult.value.map((warning, idx) => ( +
+ + {idx !== warningsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + + {metadataResult.loading && } + {!metadataResult.loading && metadataResult.value && ( + + )} + +
+
+ {/* TODO: Sequnce chart */} +
+ ); +}; + +export const withRequest = (Component: typeof BuildDetailsComponent) => ({ + buildId, +}: { + buildId: string; +}) => { + const client = useApi(xcmetricsApiRef); + const { value: build, loading, error } = useAsync( + async () => client.getBuild(buildId), + [], + ); + + if (loading) { + return ; + } + + if (error) { + return {error.message}; + } + + if (!build) { + return Could not load build {buildId}; + } + + return ; +}; diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts new file mode 100644 index 0000000000..574b29f32e --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './BuildDetailsComponent'; diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx index 3818d51a16..7d7d1ff585 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx @@ -18,6 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { BuildListComponent } from './BuildListComponent'; import { xcmetricsApiRef } from '../../api'; +import userEvent from '@testing-library/user-event'; jest.mock('../../api/XcmetricsClient'); const client = require('../../api/XcmetricsClient'); @@ -26,6 +27,10 @@ jest.mock('../BuildListFilterComponent', () => ({ BuildListFilterComponent: () => 'BuildListFilterComponent', })); +jest.mock('../BuildDetailsComponent', () => ({ + BuildDetailsComponent: () => 'BuildDetailsComponent', +})); + describe('BuildListComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( @@ -42,6 +47,23 @@ describe('BuildListComponent', () => { ).toBeInTheDocument(); }); + it('should show build details', async () => { + const rendered = await renderInTestApp( + + + , + ); + + userEvent.click( + (await rendered.findAllByLabelText('Detail panel visiblity toggle'))[0], + ); + expect( + await rendered.findByText('BuildDetailsComponent'), + ).toBeInTheDocument(); + }); + it('should show errors', async () => { const message = 'error'; client.XcmetricsClient.getFilteredBuilds = jest diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx index c9ab028359..6cc44eee54 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx @@ -17,12 +17,24 @@ import React, { useRef, useState } from 'react'; import { Table } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { BuildFilters, xcmetricsApiRef } from '../../api'; -import { Grid } from '@material-ui/core'; +import { createStyles, Grid, makeStyles } from '@material-ui/core'; import { BuildListFilterComponent as Filters } from '../BuildListFilterComponent'; import { DateTime } from 'luxon'; import { buildPageColumns } from '../BuildTableColumns'; +import { BuildDetailsComponent } from '../BuildDetailsComponent'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + detailPanel: { + padding: theme.spacing(2), + backgroundColor: theme.palette.background.paper, + }, + }), +); export const BuildListComponent = () => { + const classes = useStyles(); const client = useApi(xcmetricsApiRef); const tableRef = useRef(); @@ -68,6 +80,11 @@ export const BuildListComponent = () => { .catch(reason => reject(reason)); }); }} + detailPanel={rowData => ( +
+ +
+ )} /> ); diff --git a/plugins/xcmetrics/src/components/BuildTableColumns.tsx b/plugins/xcmetrics/src/components/BuildTableColumns.tsx index 95c82ef372..a8bfd0166e 100644 --- a/plugins/xcmetrics/src/components/BuildTableColumns.tsx +++ b/plugins/xcmetrics/src/components/BuildTableColumns.tsx @@ -15,26 +15,16 @@ */ import { Chip } from '@material-ui/core'; -import React, { ReactChild } from 'react'; -import { - StatusOK, - StatusError, - StatusWarning, - TableColumn, -} from '@backstage/core-components'; -import { Build, BuildStatus } from '../api'; +import React from 'react'; +import { TableColumn } from '@backstage/core-components'; +import { Build } from '../api'; import { formatTime, formatDuration } from '../utils'; - -const STATUS_ICONS: { [key in BuildStatus]: ReactChild } = { - succeeded: , - failed: , - stopped: , -}; +import { StatusIconComponent as StatusIcon } from './StatusIconComponent'; const baseColumns: TableColumn[] = [ { field: 'buildStatus', - render: data => STATUS_ICONS[data.buildStatus], + render: data => , }, { title: 'Project', diff --git a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx new file mode 100644 index 0000000000..601f3ea6a2 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BuildsPage } from './BuildsPage'; + +jest.mock('../BuildDetailsComponent', () => ({ + withRequest: (component: any) => component, + BuildDetailsComponent: () => 'BuildDetailsComponent', +})); + +jest.mock('../BuildListComponent', () => ({ + BuildListComponent: () => 'BuildListComponent', +})); + +describe('BuildPageComponent', () => { + it('should render BuildDetailsWithDataComponent if build id is provided in path', async () => { + const rendered = await renderInTestApp(, { + routeEntries: [`/buildId`], + }); + + expect(rendered.getByText('BuildDetailsComponent')).toBeInTheDocument(); + }); + + it('should render BuildListComponent if no build id is provided in path', async () => { + const rendered = await renderInTestApp(); + + expect(rendered.getByText('BuildListComponent')).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx new file mode 100644 index 0000000000..6d7c8cf20b --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useRouteRefParams } from '@backstage/core-plugin-api'; +import { buildsRouteRef } from '../../routes'; +import { BuildListComponent } from '../BuildListComponent'; +import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent'; +import { CopyTextButton, InfoCard } from '@backstage/core-components'; + +export const BuildsPage = () => { + const { '*': buildId } = useRouteRefParams(buildsRouteRef) ?? { '*': '' }; + + if (buildId) { + const BuildDetails = withRequest(BuildDetailsComponent); + + return ( + + {buildId}{' '} + + + } + > + + + ); + } + + return ; +}; diff --git a/plugins/xcmetrics/src/components/BuildsPage/index.ts b/plugins/xcmetrics/src/components/BuildsPage/index.ts new file mode 100644 index 0000000000..d04503499c --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildsPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './BuildsPage'; diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx new file mode 100644 index 0000000000..8833d828f0 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { StatusIconComponent } from './StatusIconComponent'; + +describe('StatusIconComponent', () => { + it('should render', async () => { + let rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status ok')).toBeInTheDocument(); + + rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status error')).toBeInTheDocument(); + + rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status warning')).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx new file mode 100644 index 0000000000..f680003ff4 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + StatusError, + StatusOK, + StatusWarning, +} from '@backstage/core-components'; +import { BuildStatus } from '../../api'; + +const STATUS_ICONS: { [key in BuildStatus]: JSX.Element } = { + succeeded: , + failed: , + stopped: , +}; + +interface StatusIconProps { + buildStatus: BuildStatus; +} + +export const StatusIconComponent = ({ buildStatus }: StatusIconProps) => + STATUS_ICONS[buildStatus]; diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/index.ts b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts new file mode 100644 index 0000000000..e680e6177c --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './StatusIconComponent'; diff --git a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx index 2ce634dc11..b0d81757a9 100644 --- a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx +++ b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx @@ -18,6 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { XcmetricsLayout } from './XcmetricsLayout'; import { xcmetricsApiRef } from '../../api'; +import userEvent from '@testing-library/user-event'; jest.mock('../../api/XcmetricsClient'); const client = require('../../api/XcmetricsClient'); @@ -44,5 +45,7 @@ describe('XcmetricsLayout', () => { expect(rendered.getByText('Builds')).toBeInTheDocument(); expect(rendered.getByText('OverviewComponent')).toBeInTheDocument(); + userEvent.click(rendered.getByText('Builds')); + expect(await rendered.findByText('BuildListComponent')).toBeInTheDocument(); }); }); diff --git a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx index 48faaaa3a6..33e9d64c6b 100644 --- a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx +++ b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx @@ -24,7 +24,7 @@ import { import { OverviewComponent } from '../OverviewComponent'; import { buildsRouteRef, rootRouteRef } from '../../routes'; import { RouteRef, SubRouteRef } from '@backstage/core-plugin-api'; -import { BuildListComponent } from '../BuildListComponent'; +import { BuildsPage } from '../BuildsPage'; export interface TabConfig { routeRef: RouteRef | SubRouteRef; @@ -41,7 +41,7 @@ const TABS: TabConfig[] = [ { routeRef: buildsRouteRef, title: 'Builds', - component: , + component: , }, ]; diff --git a/plugins/xcmetrics/src/utils/format.ts b/plugins/xcmetrics/src/utils/format.ts index 8c6dadb768..67a0ed4c43 100644 --- a/plugins/xcmetrics/src/utils/format.ts +++ b/plugins/xcmetrics/src/utils/format.ts @@ -18,8 +18,12 @@ import { BuildStatus } from '../api'; export const formatDuration = (seconds: number) => { const duration = Duration.fromObject({ - seconds: Math.round(seconds), - }).shiftTo('hours', 'minutes', 'seconds'); + seconds: seconds, + }).shiftTo('hours', 'minutes', 'seconds', 'milliseconds'); + + if (duration.hours + duration.minutes + duration.seconds === 0) { + return `${Math.round(duration.milliseconds)} ms`; + } const h = duration.hours ? `${duration.hours} h` : ''; const m = duration.minutes ? `${duration.minutes} m` : ''; From 2daf2635a4392b8b7f7f455e3131bf2f03194fc9 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Tue, 3 Aug 2021 15:31:50 +0200 Subject: [PATCH 34/46] Make sure datepicker label is above input Signed-off-by: Niklas Granander --- .../src/components/DatePickerComponent/DatePickerComponent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx index 6877340a5b..c6cc2eded5 100644 --- a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx +++ b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx @@ -62,6 +62,7 @@ export const DatePickerComponent = ({ }: InputProps & DatePickerProps) => ( <> {label} +
Date: Wed, 4 Aug 2021 18:11:06 +0200 Subject: [PATCH 35/46] Add timeline and better handling of warning/error message Signed-off-by: Niklas Granander --- plugins/xcmetrics/package.json | 3 +- plugins/xcmetrics/src/api/XcmetricsClient.ts | 5 +- .../src/api/__mocks__/XcmetricsClient.ts | 52 ++++- plugins/xcmetrics/src/api/types.ts | 36 +++- .../AccordionComponent/AccordionComponent.tsx | 6 +- .../BuildDetailsComponent.test.tsx | 7 +- .../BuildDetailsComponent.tsx | 193 ++++++++++-------- .../BuildListComponent.test.tsx | 1 + .../BuildListComponent/BuildListComponent.tsx | 15 +- .../BuildTimelineComponent.test.tsx | 43 ++++ .../BuildTimelineComponent.tsx | 119 +++++++++++ .../BuildTimelineComponent/index.ts | 16 ++ .../src/components/BuildsPage/BuildsPage.tsx | 17 +- .../PreformattedTextComponent.tsx | 128 ++++++++++++ .../PreformattedTextComponent/index.ts | 16 ++ .../StatusCellComponent.tsx | 19 +- plugins/xcmetrics/src/utils/format.ts | 6 + 17 files changed, 551 insertions(+), 131 deletions(-) create mode 100644 plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx create mode 100644 plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts create mode 100644 plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx create mode 100644 plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 8ebcf9a85e..50eaaf5b94 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -30,7 +30,8 @@ "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "recharts": "^1.8.5" }, "devDependencies": { "@backstage/cli": "^0.7.7", diff --git a/plugins/xcmetrics/src/api/XcmetricsClient.ts b/plugins/xcmetrics/src/api/XcmetricsClient.ts index 7aee5453bd..37baa949d4 100644 --- a/plugins/xcmetrics/src/api/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/XcmetricsClient.ts @@ -24,6 +24,7 @@ import { BuildFilters, BuildHost, BuildMetadata, + BuildResponse, BuildStatusResult, BuildTime, BuildWarning, @@ -42,9 +43,9 @@ export class XcmetricsClient implements XcmetricsApi { this.discoveryApi = options.discoveryApi; } - async getBuild(id: string): Promise { + async getBuild(id: string): Promise { const response = await this.get(`/build/${id}`); - return ((await response.json()) as Record<'build', Build>).build; + return (await response.json()) as BuildResponse; } async getBuilds(limit: number = 10): Promise { diff --git a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts index 5258215175..765be721c6 100644 --- a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts @@ -24,9 +24,13 @@ import { BuildStatusResult, BuildTime, BuildWarning, + Target, XcmetricsApi, + Xcode, } from '../types'; +const MOCK_BUILD_ID = 'buildId'; + export const mockBuild: Build = { userid: 'userid1', warningCount: 1, @@ -42,7 +46,7 @@ export const mockBuild: Build = { projectName: 'ProjectName', compilationEndTimestampMicroseconds: 1, errorCount: 1, - id: 'buildId', + id: MOCK_BUILD_ID, buildStatus: 'succeeded', compilationDuration: 1, schema: 'SchemaName', @@ -74,7 +78,7 @@ export const mockBuildError: BuildError = { severity: 2, startingLine: 241, parentType: 'step', - buildIdentifier: 'buildId', + buildIdentifier: MOCK_BUILD_ID, startingColumn: 97, characterRangeStart: 0, documentURL: 'file:///Users//myproject/Sources/MyClass.m', @@ -96,7 +100,7 @@ export const mockBuildHost: BuildHost = { memoryTotalMb: 16384, timezone: 'CET', cpuModel: 'Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz', - buildIdentifier: 'buildId', + buildIdentifier: MOCK_BUILD_ID, memoryFreeMb: 24.5234375, cpuSpeedGhz: 3.5, }; @@ -114,7 +118,7 @@ export const mockBuildTime: BuildTime = { totalDuration: 3.1, }; export const mockBuildStatus: BuildStatusResult = { - id: 'build_id', + id: MOCK_BUILD_ID, buildStatus: 'succeeded', }; @@ -135,13 +139,47 @@ export const mockBuildWarning: BuildWarning = { parentType: 'step', clangFlag: '[-Wdeprecated-declarations]', startingColumn: 22, - buildIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1', + buildIdentifier: MOCK_BUILD_ID, characterRangeStart: 0, }; +export const mockTarget: Target = { + id: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1992', + category: 'noop', + startTimestamp: '2020-11-02T10:59:09Z', + compilationEndTimestampMicroseconds: 1604314749.2909288, + endTimestampMicroseconds: 1604314982.298002, + endTimestamp: '2020-11-02T11:03:02Z', + fetchedFromCache: false, + errorCount: 0, + day: '2020-11-02T00:00:00Z', + warningCount: 0, + compilationEndTimestamp: '2020-11-02T10:59:09Z', + compilationDuration: 0, + compiledCount: 0, + duration: 0.000233007, + buildIdentifier: MOCK_BUILD_ID, + name: 'Model', + startTimestampMicroseconds: 1604314749.2909288, +}; + +export const mockXcode: Xcode = { + buildNumber: '12A7209', + id: '6354C87F-0ADC-4354-929C-02EBE545E099', + buildIdentifier: MOCK_BUILD_ID, + day: '2020-11-02T00:00:00Z', + version: '1200', +}; + +export const mockBuildResponse = { + build: mockBuild, + targets: [mockTarget], + xcode: mockXcode, +}; + export const XcmetricsClient: XcmetricsApi = { - getBuild: (id: string) => { - return Promise.resolve({ ...mockBuild, id }); + getBuild: (_id: string) => { + return Promise.resolve(mockBuildResponse); }, getBuilds: () => { return Promise.resolve([ diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index 8f6ae9f0ae..44f27872ca 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -132,6 +132,40 @@ export type PaginationResult = { }; }; +export type Target = { + id: string; + category: string; + startTimestamp: string; + compilationEndTimestampMicroseconds: number; + endTimestampMicroseconds: number; + endTimestamp: string; + fetchedFromCache: boolean; + errorCount: number; + day: string; + warningCount: number; + compilationEndTimestamp: string; + compilationDuration: number; + compiledCount: number; + duration: number; + buildIdentifier: string; + name: string; + startTimestampMicroseconds: number; +}; + +export type Xcode = { + buildNumber: string; + id: string; + buildIdentifier: string; + day: string; + version: string; +}; + +export type BuildResponse = { + build: Build; + targets: Target[]; + xcode: Xcode; +}; + export type BuildFilters = { from: string; // ISO Date (e.g. "2021-01-01") to: string; // ISO Date (e.g. "2021-01-02") @@ -140,7 +174,7 @@ export type BuildFilters = { }; export interface XcmetricsApi { - getBuild(id: string): Promise; + getBuild(id: string): Promise; getBuilds(limit?: number): Promise; getFilteredBuilds( filters: BuildFilters, diff --git a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx index d8c4e4f81d..9afe736a2a 100644 --- a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx +++ b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx @@ -42,6 +42,7 @@ interface AccordionProps { heading: string; secondaryHeading?: string | number; disabled?: boolean; + unmountOnExit?: boolean; } export const AccordionComponent = ( @@ -50,7 +51,10 @@ export const AccordionComponent = ( const classes = useStyles(); return ( - + } aria-controls={`${props.id}-content`} diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx index 99c5ec20e5..15b8720a1d 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx @@ -28,13 +28,17 @@ jest.mock('../AccordionComponent', () => ({ ), })); +jest.mock('../BuildTimelineComponent', () => ({ + BuildTimelineComponent: () => 'BuildTimelineComponent', +})); + describe('BuildDetailsComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + , ); @@ -42,6 +46,7 @@ describe('BuildDetailsComponent', () => { expect(rendered.getByText('accordion-Errors')).toBeInTheDocument(); expect(rendered.getByText('accordion-Warnings')).toBeInTheDocument(); expect(rendered.getByText('accordion-Metadata')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Timeline')).toBeInTheDocument(); expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); expect( diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx index 99b4c83210..6696a2083d 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -15,12 +15,8 @@ */ import { createStyles, Divider, Grid, makeStyles } from '@material-ui/core'; import React from 'react'; -import { Build, xcmetricsApiRef } from '../../api'; -import { - OverflowTooltip, - Progress, - StructuredMetadataTable, -} from '@backstage/core-components'; +import { BuildResponse, xcmetricsApiRef } from '../../api'; +import { Progress, StructuredMetadataTable } from '@backstage/core-components'; import { Alert } from '@material-ui/lab'; import { useAsync } from 'react-use'; import { useApi } from '@backstage/core-plugin-api'; @@ -28,6 +24,8 @@ import { formatDuration, formatStatus, formatTime } from '../../utils'; import { StatusIconComponent as StatusIcon } from '../StatusIconComponent'; import { BackstageTheme } from '@backstage/theme'; import { AccordionComponent } from '../AccordionComponent'; +import { BuildTimelineComponent } from '../BuildTimelineComponent'; +import { PreformattedTextComponent } from '../PreformattedTextComponent'; const useStyles = makeStyles((theme: BackstageTheme) => createStyles({ @@ -39,10 +37,14 @@ const useStyles = makeStyles((theme: BackstageTheme) => ); interface BuildDetailsProps { - build: Build; + buildData: BuildResponse; + showId?: boolean; } -export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { +export const BuildDetailsComponent = ({ + buildData: { build, targets, xcode }, + showId, +}: BuildDetailsProps) => { const classes = useStyles(); const client = useApi(xcmetricsApiRef); const hostResult = useAsync(async () => client.getBuildHost(build.id), []); @@ -60,7 +62,6 @@ export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { ); const buildDetails = { - id: build.id, project: build.projectName, schema: build.schema, category: build.category, @@ -74,96 +75,110 @@ export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { {formatStatus(build.buildStatus)} ), + xcode: `${xcode.version} (${xcode.buildNumber})`, CI: build.isCi, }; return ( - - - - - - - - {hostResult.loading && } - {!hostResult.loading && hostResult.value && ( - - )} - - - -
- {errorsResult.loading && } - {!errorsResult.loading && - errorsResult.value && - errorsResult.value.map((error, idx) => ( -
- - {idx !== errorsResult.value.length - 1 && ( - - )} -
- ))} -
-
- - -
- {warningsResult.loading && } - {!warningsResult.loading && - warningsResult.value && - warningsResult.value.map((warning, idx) => ( -
- - {idx !== warningsResult.value.length - 1 && ( - - )} -
- ))} -
-
- - - {metadataResult.loading && } - {!metadataResult.loading && metadataResult.value && ( - - )} - -
+ + + + + + + {hostResult.loading && } + {!hostResult.loading && hostResult.value && ( + + )} + + + +
+ {errorsResult.loading && } + {!errorsResult.loading && + errorsResult.value?.map((error, idx) => ( +
+ + {idx !== errorsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + +
+ {warningsResult.loading && } + {!warningsResult.loading && + warningsResult.value?.map((warning, idx) => ( +
+ + {idx !== warningsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + + {metadataResult.loading && } + {!metadataResult.loading && metadataResult.value && ( + + )} + + + + +
- {/* TODO: Sequnce chart */}
); }; +type WithRequestProps = Omit & { + buildId: string; +}; + export const withRequest = (Component: typeof BuildDetailsComponent) => ({ buildId, -}: { - buildId: string; -}) => { + ...props +}: WithRequestProps) => { const client = useApi(xcmetricsApiRef); - const { value: build, loading, error } = useAsync( + const { value: buildResponse, loading, error } = useAsync( async () => client.getBuild(buildId), [], ); @@ -176,9 +191,9 @@ export const withRequest = (Component: typeof BuildDetailsComponent) => ({ return {error.message}; } - if (!build) { + if (!buildResponse) { return Could not load build {buildId}; } - return ; + return ; }; diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx index 7d7d1ff585..761c08a711 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx @@ -28,6 +28,7 @@ jest.mock('../BuildListFilterComponent', () => ({ })); jest.mock('../BuildDetailsComponent', () => ({ + withRequest: (component: any) => component, BuildDetailsComponent: () => 'BuildDetailsComponent', })); diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx index 6cc44eee54..42f6bf4414 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx @@ -21,7 +21,7 @@ import { createStyles, Grid, makeStyles } from '@material-ui/core'; import { BuildListFilterComponent as Filters } from '../BuildListFilterComponent'; import { DateTime } from 'luxon'; import { buildPageColumns } from '../BuildTableColumns'; -import { BuildDetailsComponent } from '../BuildDetailsComponent'; +import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent'; import { BackstageTheme } from '@backstage/theme'; const useStyles = makeStyles((theme: BackstageTheme) => @@ -80,11 +80,14 @@ export const BuildListComponent = () => { .catch(reason => reject(reason)); }); }} - detailPanel={rowData => ( -
- -
- )} + detailPanel={rowData => { + const BuildDetails = withRequest(BuildDetailsComponent); + return ( +
+ +
+ ); + }} />
); diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx new file mode 100644 index 0000000000..3adb1ede58 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BuildTimelineComponent } from './BuildTimelineComponent'; + +jest.mock('../../api/XcmetricsClient'); +const client = require('../../api/XcmetricsClient'); + +describe('BuildTimelineComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + , + ); + expect( + await rendered.findByText(client.mockTarget.name), + ).toBeInTheDocument(); + }); + + it('should render a message if no targets are provided', async () => { + const rendered = await renderInTestApp( + , + ); + expect(rendered.getByText('No Targets')).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx new file mode 100644 index 0000000000..0d0f249a95 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createStyles, makeStyles, useTheme } from '@material-ui/core'; +import React from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { BackstageTheme } from '@backstage/theme'; +import { Target } from '../../api'; +import { formatSecondsInterval, formatPercentage } from '../../utils'; + +const EMPTY_HEIGHT = 100; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + toolTip: { + backgroundColor: theme.palette.background.paper, + opacity: 0.8, + padding: 8, + }, + }), +); + +const TargetToolTip = ({ active, payload, label }: any) => { + const classes = useStyles(); + + if (active && payload && payload.length === 2) { + const buildTime = payload[0].value[1] - payload[0].value[0]; + const compileTime = payload[1].value[1] - payload[1].value[0]; + return ( +
+ {`${label}: ${formatSecondsInterval(payload[0].value)}`} +
+ {buildTime > 0 && + `Compile time: ${formatPercentage(compileTime / buildTime)}`} +
+ ); + } + + return null; +}; + +const getTimelineData = (targets: Target[]) => { + const min = targets[0].startTimestampMicroseconds; + + return targets + .filter(target => target.fetchedFromCache === false) + .map(target => ({ + name: target.name, + buildTime: [ + target.startTimestampMicroseconds - min, + target.endTimestampMicroseconds - min, + ], + compileTime: [ + target.startTimestampMicroseconds - min, + target.compilationEndTimestampMicroseconds - min, + ], + })); +}; + +export interface BuildTimelineProps { + targets: Target[]; + height?: number; + width?: number; +} + +export const BuildTimelineComponent = ({ + targets, + height, + width, +}: BuildTimelineProps) => { + const theme = useTheme(); + if (!targets.length) return

No Targets

; + + const data = getTimelineData(targets); + + return ( + + + + + + } /> + + + + + + ); +}; diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts new file mode 100644 index 0000000000..2a6a612acf --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './BuildTimelineComponent'; diff --git a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx index 6d7c8cf20b..ca3f023ff5 100644 --- a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx +++ b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx @@ -18,7 +18,7 @@ import { useRouteRefParams } from '@backstage/core-plugin-api'; import { buildsRouteRef } from '../../routes'; import { BuildListComponent } from '../BuildListComponent'; import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent'; -import { CopyTextButton, InfoCard } from '@backstage/core-components'; +import { InfoCard } from '@backstage/core-components'; export const BuildsPage = () => { const { '*': buildId } = useRouteRefParams(buildsRouteRef) ?? { '*': '' }; @@ -27,19 +27,8 @@ export const BuildsPage = () => { const BuildDetails = withRequest(BuildDetailsComponent); return ( - - {buildId}{' '} - - - } - > - + + ); } diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx new file mode 100644 index 0000000000..a5ad997ee8 --- /dev/null +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Button, + createStyles, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + makeStyles, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import React, { useState } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { cn } from '../../utils'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + pre: { + whiteSpace: 'pre-line', + wordBreak: 'break-all', + }, + expandable: { + cursor: 'pointer', + }, + fullPre: { + whiteSpace: 'pre-wrap', + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }), +); + +interface PreformattedTextProps { + text: string; + maxChars: number; +} + +interface ExpandableProps extends PreformattedTextProps { + expandable: boolean; + title: string; +} + +interface NonExpandableProps extends PreformattedTextProps { + expandable?: never; + title?: string; +} + +export const PreformattedTextComponent = ({ + text, + maxChars, + expandable, + title, +}: NonExpandableProps | ExpandableProps) => { + const [open, setOpen] = useState(false); + const classes = useStyles(); + + const handleKeyUp = (event: React.KeyboardEvent) => { + if (expandable && event.key === 'Enter') { + setOpen(true); + } + }; + + return ( + <> +
expandable && setOpen(true)} + onKeyUp={handleKeyUp} + tabIndex={expandable ? 0 : undefined} + > +
+          {text.slice(0, maxChars - 3).trim()}
+          {text.length > maxChars && '...'}
+        
+
+ + {expandable && ( + setOpen(false)} + aria-labelledby="dialog-title" + aria-describedby="dialog-description" + maxWidth="xl" + fullWidth + > + + {title} + setOpen(false)} + > + + + + +
{text}
+
+ + + +
+ )} + + ); +}; diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts new file mode 100644 index 0000000000..e923eeb224 --- /dev/null +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './PreformattedTextComponent'; diff --git a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx index 1ecc324683..201cb4febf 100644 --- a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx +++ b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx @@ -28,15 +28,16 @@ interface TooltipContentProps { const TooltipContent = ({ buildId }: TooltipContentProps) => { const client = useApi(xcmetricsApiRef); - const { - value: build, - loading, - error, - } = useAsync(async () => client.getBuild(buildId), []); + const { value, loading, error } = useAsync( + async () => client.getBuild(buildId), + [], + ); if (error) { return
{error.message}
; - } else if (loading || !build) { + } + + if (loading || !value?.build) { return ; } @@ -45,15 +46,15 @@ const TooltipContent = ({ buildId }: TooltipContentProps) => {
- + - + - +
Started{new Date(build.startTimestamp).toLocaleString()}{new Date(value.build.startTimestamp).toLocaleString()}
Duration{formatDuration(build.duration)}{formatDuration(value.build.duration)}
Status{formatStatus(build.buildStatus)}{formatStatus(value.build.buildStatus)}
diff --git a/plugins/xcmetrics/src/utils/format.ts b/plugins/xcmetrics/src/utils/format.ts index 67a0ed4c43..e21e95a7b2 100644 --- a/plugins/xcmetrics/src/utils/format.ts +++ b/plugins/xcmetrics/src/utils/format.ts @@ -32,6 +32,12 @@ export const formatDuration = (seconds: number) => { return `${h} ${m} ${s}`; }; +export const formatSecondsInterval = ([start, end]: [number, number]) => { + return `${Math.round(start * 100) / 100} s - ${ + Math.round(end * 100) / 100 + } s`; +}; + export const formatTime = (timestamp: string) => { return DateTime.fromISO(timestamp).toLocaleString( DateTime.DATETIME_SHORT_WITH_SECONDS, From 57bf1233de2a1530ca2a948357243689743864de Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Wed, 11 Aug 2021 20:35:40 +0200 Subject: [PATCH 36/46] Fix prettier error Signed-off-by: Niklas Granander --- .../BuildDetailsComponent.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx index 6696a2083d..0c636b1dcd 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -173,27 +173,27 @@ type WithRequestProps = Omit & { buildId: string; }; -export const withRequest = (Component: typeof BuildDetailsComponent) => ({ - buildId, - ...props -}: WithRequestProps) => { - const client = useApi(xcmetricsApiRef); - const { value: buildResponse, loading, error } = useAsync( - async () => client.getBuild(buildId), - [], - ); +export const withRequest = + (Component: typeof BuildDetailsComponent) => + ({ buildId, ...props }: WithRequestProps) => { + const client = useApi(xcmetricsApiRef); + const { + value: buildResponse, + loading, + error, + } = useAsync(async () => client.getBuild(buildId), []); - if (loading) { - return ; - } + if (loading) { + return ; + } - if (error) { - return {error.message}; - } + if (error) { + return {error.message}; + } - if (!buildResponse) { - return Could not load build {buildId}; - } + if (!buildResponse) { + return Could not load build {buildId}; + } - return ; -}; + return ; + }; From eed15251ed5747f53493dfb918d54ef2082d561e Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Wed, 11 Aug 2021 20:54:59 +0200 Subject: [PATCH 37/46] Add changelog Signed-off-by: Niklas Granander --- .changeset/new-ligers-drum.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-ligers-drum.md diff --git a/.changeset/new-ligers-drum.md b/.changeset/new-ligers-drum.md new file mode 100644 index 0000000000..0edee17b4d --- /dev/null +++ b/.changeset/new-ligers-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-xcmetrics': patch +--- + +Enable browsing detailed build information such as host configuration, errors, warnings, metadata and a timeline for all targets From 5dceb1de1f0166a364d2cfdc94f1bec09a54d312 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 02:40:06 +0000 Subject: [PATCH 38/46] chore(deps): bump path-parse from 1.0.6 to 1.0.7 Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/jbgutierrez/path-parse/releases) - [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7) --- updated-dependencies: - dependency-name: path-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 52f3875438..ef99b66eb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18659,6 +18659,11 @@ luxon@2.0.2, luxon@^2.0.2: resolved "https://registry.npmjs.org/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== +luxon@^1.27.0: + version "1.28.0" + resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" + integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -21102,9 +21107,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-root-regex@^0.1.0: version "0.1.2" From 8f5a872b340b90784eb94e436900adca815d2115 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 04:11:39 +0000 Subject: [PATCH 39/46] chore(deps): bump @types/dockerode from 3.2.1 to 3.2.7 Bumps [@types/dockerode](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/dockerode) from 3.2.1 to 3.2.7. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/dockerode) --- updated-dependencies: - dependency-name: "@types/dockerode" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7b714f2e3b..5344710d4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6529,12 +6529,21 @@ resolved "https://registry.npmjs.org/@types/diff/-/diff-5.0.0.tgz#eb71e94feae62548282c4889308a3dfb57e36020" integrity sha512-jrm2K65CokCCX4NmowtA+MfXyuprZC13jbRuwprs6/04z/EcFg/MCwYdsHn+zgV4CQBiATiI7AEq7y1sZCtWKA== -"@types/dockerode@^3.2.1": - version "3.2.1" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.1.tgz#f713a8f6f1017c227845ab33239383da721207b9" - integrity sha512-AeZpdQMNqM8dtrEaaP81CbjdRVKNmFIMzgz5IlKIeS5uWEmjlEmENP444AGTEEF71r5TFuY9E4SkzZAO8lOF1A== +"@types/docker-modem@*": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.1.tgz#2a29c914dd64c4c9213510d2ce75fa85232934df" + integrity sha512-ZUXPF0WNnvs7AxoQRijt+DW2jsXnWCk8ac28tTYzTpBNnOEIAw83A+pYkCXjTFdJHMTc+wUmwNr71Zy2TRjlWg== dependencies: "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.2.1": + version "3.2.7" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.7.tgz#0bc1f583b0e1edc4ca0c6179ef251382df35dcb3" + integrity sha512-Y8hMRQTwsOjz4qm6yilZKKjB/Y7+2EOiY3RPN1Xtu63wEUEDVv+3Ou+sgiisPE9+pVe3bmwhnF+E1Iwj/o4J6w== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" "@types/dompurify@^2.2.2": version "2.2.3" @@ -6995,10 +7004,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": - version "14.17.3" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz#6d327abaa4be34a74e421ed6409a0ae2f47f4c3d" - integrity sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32", "@types/node@^14.14.33": + version "14.17.8" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" + integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== "@types/node@10.17.13": version "10.17.13" @@ -7015,11 +7024,6 @@ resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== -"@types/node@^14.0.10": - version "14.17.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" - integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -7368,6 +7372,14 @@ dependencies: "@types/node" "*" +"@types/ssh2@*": + version "0.5.47" + resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.47.tgz#67a8b35a0527b2bb668f6dea4c84be6ff1abdc19" + integrity sha512-ZhqJg8BRV7OsCi0KVqPr27lUMMmLEeHYw1VXUNGGDlQEDq9HTsKx+wYvi8E6oNC6gRZ7PV99ZMZmMr5vztcYYA== + dependencies: + "@types/node" "*" + "@types/ssh2-streams" "*" + "@types/ssh2@^0.5.45": version "0.5.46" resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.46.tgz#e12341a242aea0e98ac2dec89e039bf421fd3584" From ccfd99c9b146d9c76f438283beae217010f96fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 12 Aug 2021 09:47:16 +0200 Subject: [PATCH 40/46] update dockerfile instructions to build only app workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- contrib/docker/frontend-with-nginx/Dockerfile.hostbuild | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild b/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild index 1e0134017f..f0fa4a034b 100644 --- a/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild +++ b/contrib/docker/frontend-with-nginx/Dockerfile.hostbuild @@ -8,7 +8,6 @@ # simply copies in the build output into the image. If you want to also perform # the build itself inside docker, use Dockerfile.build in this folder instead. - # USAGE: # # - Copy this file and the "docker" folder from this directory to your project @@ -23,7 +22,7 @@ # # yarn install # yarn tsc -# yarn build --config --config ... +# yarn workspace app build --config --config ... # docker build -t backstage-frontend -f Dockerfile.hostbuild . @@ -38,4 +37,3 @@ COPY docker/default.conf.template /etc/nginx/templates/default.conf.template COPY docker/inject-config.sh /docker-entrypoint.d/40-inject-config.sh ENV PORT 80 - From 8bedb75aeaed366ee711e0c8956ab4db155a2bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 12 Aug 2021 10:05:04 +0200 Subject: [PATCH 41/46] add missed luxon bump changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/big-seas-explain.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .changeset/big-seas-explain.md diff --git a/.changeset/big-seas-explain.md b/.changeset/big-seas-explain.md new file mode 100644 index 0000000000..e74af190cb --- /dev/null +++ b/.changeset/big-seas-explain.md @@ -0,0 +1,21 @@ +--- +'@backstage/integration': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-xcmetrics': patch +--- + +Update Luxon dependency to 2.x From c734d6995aa3f2af18122f50507cb34903b13f72 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 12 Aug 2021 08:21:56 +0000 Subject: [PATCH 42/46] Version Packages --- .changeset/beige-colts-pump.md | 5 -- .changeset/big-seas-explain.md | 21 ----- .changeset/chilled-schools-raise.md | 6 -- .changeset/clean-jokes-think.md | 18 ----- .changeset/eighty-glasses-mate.md | 5 -- .changeset/four-buttons-design.md | 5 -- .changeset/green-ducks-tan.md | 5 -- .changeset/happy-lies-wink.md | 22 ------ .changeset/late-frogs-yell.md | 5 -- .changeset/little-tools-melt.md | 5 -- .changeset/modern-ghosts-dress.md | 25 ------ .changeset/neat-meals-arrive.md | 12 --- .changeset/ninety-worms-kick.md | 5 -- .changeset/perfect-seals-burn.md | 6 -- .changeset/pink-pugs-relate.md | 5 -- .changeset/plenty-carpets-jog.md | 18 ----- .changeset/quick-fans-smash.md | 5 -- .changeset/rare-pugs-exercise.md | 5 -- .changeset/search-early-horses-sip.md | 6 -- .changeset/serious-kiwis-wonder.md | 5 -- .changeset/shaggy-days-peel.md | 6 -- .changeset/sharp-berries-fail.md | 34 -------- .changeset/sharp-donkeys-push.md | 12 --- .changeset/silent-dragons-fold.md | 7 -- .changeset/small-actors-sing.md | 12 --- .changeset/small-guests-invite.md | 5 -- .changeset/spicy-crews-applaud.md | 6 -- .changeset/techdocs-stale-breakfast-king.md | 19 ----- .changeset/tidy-jokes-train.md | 5 -- .changeset/violet-bobcats-wave.md | 5 -- packages/app/CHANGELOG.md | 28 +++++++ packages/app/package.json | 48 ++++++------ packages/backend-common/CHANGELOG.md | 9 +++ packages/backend-common/package.json | 8 +- packages/backend-test-utils/CHANGELOG.md | 10 +++ packages/backend-test-utils/package.json | 8 +- packages/backend/CHANGELOG.md | 19 +++++ packages/backend/package.json | 30 +++---- packages/cli/CHANGELOG.md | 6 ++ packages/cli/package.json | 14 ++-- packages/codemods/CHANGELOG.md | 9 +++ packages/codemods/package.json | 2 +- packages/core-app-api/CHANGELOG.md | 10 +++ packages/core-app-api/package.json | 12 +-- packages/core-components/CHANGELOG.md | 9 +++ packages/core-components/package.json | 10 +-- packages/core-plugin-api/CHANGELOG.md | 6 ++ packages/core-plugin-api/package.json | 10 +-- packages/create-app/CHANGELOG.md | 78 +++++++++++++++++++ packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 13 ++++ packages/dev-utils/package.json | 16 ++-- packages/integration-react/CHANGELOG.md | 9 +++ packages/integration-react/package.json | 14 ++-- packages/integration/CHANGELOG.md | 14 ++++ packages/integration/package.json | 6 +- packages/techdocs-common/CHANGELOG.md | 24 ++++++ packages/techdocs-common/package.json | 8 +- packages/test-utils-core/CHANGELOG.md | 7 ++ packages/test-utils-core/package.json | 2 +- packages/test-utils/CHANGELOG.md | 10 +++ packages/test-utils/package.json | 10 +-- plugins/api-docs/CHANGELOG.md | 12 +++ plugins/api-docs/package.json | 18 ++--- plugins/auth-backend/CHANGELOG.md | 11 +++ plugins/auth-backend/package.json | 8 +- plugins/badges/package.json | 12 +-- plugins/bitrise/CHANGELOG.md | 10 +++ plugins/bitrise/package.json | 16 ++-- plugins/catalog-backend/CHANGELOG.md | 8 ++ plugins/catalog-backend/package.json | 12 +-- plugins/catalog-import/CHANGELOG.md | 12 +++ plugins/catalog-import/package.json | 20 ++--- plugins/catalog-react/CHANGELOG.md | 12 +++ plugins/catalog-react/package.json | 16 ++-- plugins/catalog/CHANGELOG.md | 13 ++++ plugins/catalog/package.json | 20 ++--- plugins/circleci/package.json | 12 +-- plugins/cloudbuild/package.json | 12 +-- plugins/code-coverage-backend/CHANGELOG.md | 8 ++ plugins/code-coverage-backend/package.json | 8 +- plugins/code-coverage/package.json | 12 +-- plugins/config-schema/package.json | 12 +-- plugins/cost-insights/CHANGELOG.md | 10 +++ plugins/cost-insights/package.json | 14 ++-- plugins/explore/CHANGELOG.md | 10 +++ plugins/explore/package.json | 16 ++-- plugins/firehydrant/package.json | 12 +-- plugins/fossa/CHANGELOG.md | 10 +++ plugins/fossa/package.json | 16 ++-- plugins/gcp-projects/package.json | 12 +-- plugins/git-release-manager/CHANGELOG.md | 11 +++ plugins/git-release-manager/package.json | 16 ++-- plugins/github-actions/CHANGELOG.md | 11 +++ plugins/github-actions/package.json | 18 ++--- plugins/github-deployments/CHANGELOG.md | 12 +++ plugins/github-deployments/package.json | 20 ++--- plugins/gitops-profiles/package.json | 12 +-- plugins/graphiql/package.json | 12 +-- plugins/ilert/CHANGELOG.md | 10 +++ plugins/ilert/package.json | 16 ++-- plugins/jenkins/CHANGELOG.md | 10 +++ plugins/jenkins/package.json | 16 ++-- plugins/kafka/package.json | 12 +-- plugins/kubernetes-backend/CHANGELOG.md | 8 ++ plugins/kubernetes-backend/package.json | 6 +- plugins/kubernetes/CHANGELOG.md | 10 +++ plugins/kubernetes/package.json | 16 ++-- plugins/lighthouse/CHANGELOG.md | 10 +++ plugins/lighthouse/package.json | 16 ++-- plugins/newrelic/package.json | 12 +-- plugins/org/package.json | 12 +-- plugins/pagerduty/CHANGELOG.md | 11 +++ plugins/pagerduty/package.json | 16 ++-- plugins/rollbar/CHANGELOG.md | 10 +++ plugins/rollbar/package.json | 16 ++-- .../CHANGELOG.md | 10 +++ .../package.json | 10 +-- .../CHANGELOG.md | 9 +++ .../package.json | 10 +-- plugins/scaffolder-backend/CHANGELOG.md | 14 ++++ plugins/scaffolder-backend/package.json | 12 +-- plugins/scaffolder/CHANGELOG.md | 37 +++++++++ plugins/scaffolder/package.json | 20 ++--- .../CHANGELOG.md | 7 ++ .../package.json | 6 +- plugins/search-backend-module-pg/CHANGELOG.md | 10 +++ plugins/search-backend-module-pg/package.json | 8 +- plugins/search/CHANGELOG.md | 10 +++ plugins/search/package.json | 16 ++-- plugins/sentry/CHANGELOG.md | 11 +++ plugins/sentry/package.json | 16 ++-- plugins/shortcuts/package.json | 12 +-- plugins/sonarqube/CHANGELOG.md | 13 ++++ plugins/sonarqube/package.json | 16 ++-- plugins/splunk-on-call/CHANGELOG.md | 10 +++ plugins/splunk-on-call/package.json | 16 ++-- plugins/tech-radar/CHANGELOG.md | 9 +++ plugins/tech-radar/package.json | 14 ++-- plugins/techdocs-backend/CHANGELOG.md | 9 +++ plugins/techdocs-backend/package.json | 12 +-- plugins/techdocs/CHANGELOG.md | 29 +++++++ plugins/techdocs/package.json | 22 +++--- plugins/todo-backend/CHANGELOG.md | 8 ++ plugins/todo-backend/package.json | 8 +- plugins/todo/package.json | 12 +-- plugins/user-settings/package.json | 12 +-- plugins/welcome/package.json | 12 +-- plugins/xcmetrics/CHANGELOG.md | 9 +++ plugins/xcmetrics/package.json | 14 ++-- yarn.lock | 14 +++- 151 files changed, 1144 insertions(+), 757 deletions(-) delete mode 100644 .changeset/beige-colts-pump.md delete mode 100644 .changeset/big-seas-explain.md delete mode 100644 .changeset/chilled-schools-raise.md delete mode 100644 .changeset/clean-jokes-think.md delete mode 100644 .changeset/eighty-glasses-mate.md delete mode 100644 .changeset/four-buttons-design.md delete mode 100644 .changeset/green-ducks-tan.md delete mode 100644 .changeset/happy-lies-wink.md delete mode 100644 .changeset/late-frogs-yell.md delete mode 100644 .changeset/little-tools-melt.md delete mode 100644 .changeset/modern-ghosts-dress.md delete mode 100644 .changeset/neat-meals-arrive.md delete mode 100644 .changeset/ninety-worms-kick.md delete mode 100644 .changeset/perfect-seals-burn.md delete mode 100644 .changeset/pink-pugs-relate.md delete mode 100644 .changeset/plenty-carpets-jog.md delete mode 100644 .changeset/quick-fans-smash.md delete mode 100644 .changeset/rare-pugs-exercise.md delete mode 100644 .changeset/search-early-horses-sip.md delete mode 100644 .changeset/serious-kiwis-wonder.md delete mode 100644 .changeset/shaggy-days-peel.md delete mode 100644 .changeset/sharp-berries-fail.md delete mode 100644 .changeset/sharp-donkeys-push.md delete mode 100644 .changeset/silent-dragons-fold.md delete mode 100644 .changeset/small-actors-sing.md delete mode 100644 .changeset/small-guests-invite.md delete mode 100644 .changeset/spicy-crews-applaud.md delete mode 100644 .changeset/techdocs-stale-breakfast-king.md delete mode 100644 .changeset/tidy-jokes-train.md delete mode 100644 .changeset/violet-bobcats-wave.md create mode 100644 packages/test-utils-core/CHANGELOG.md create mode 100644 plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md create mode 100644 plugins/search-backend-module-elasticsearch/CHANGELOG.md create mode 100644 plugins/search-backend-module-pg/CHANGELOG.md diff --git a/.changeset/beige-colts-pump.md b/.changeset/beige-colts-pump.md deleted file mode 100644 index c85a260b31..0000000000 --- a/.changeset/beige-colts-pump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Added `resolveCatalogMembership` utility to query the catalog for additional authentication claims within sign-in resolvers. diff --git a/.changeset/big-seas-explain.md b/.changeset/big-seas-explain.md deleted file mode 100644 index e74af190cb..0000000000 --- a/.changeset/big-seas-explain.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@backstage/integration': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-ilert': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-xcmetrics': patch ---- - -Update Luxon dependency to 2.x diff --git a/.changeset/chilled-schools-raise.md b/.changeset/chilled-schools-raise.md deleted file mode 100644 index 56058b6125..0000000000 --- a/.changeset/chilled-schools-raise.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-test-utils': patch ---- - -Make sure that the unique databases names generated by `TestDatabases.create()` -don't collide. diff --git a/.changeset/clean-jokes-think.md b/.changeset/clean-jokes-think.md deleted file mode 100644 index a4fc65e6fc..0000000000 --- a/.changeset/clean-jokes-think.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Expose a new composable `TechDocsIndexPage` and a `DefaultTechDocsHome` with support for starring docs and filtering on owned, starred, owner, and tags. - -You can migrate to the new UI view by making the following changes in your `App.tsx`: - -```diff -- } /> -+ }> -+ -+ -+ } -+ /> -``` diff --git a/.changeset/eighty-glasses-mate.md b/.changeset/eighty-glasses-mate.md deleted file mode 100644 index d71ef8fd42..0000000000 --- a/.changeset/eighty-glasses-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Use a more informative error message when URL reading isn't allowed due to no reader matching the target URL. diff --git a/.changeset/four-buttons-design.md b/.changeset/four-buttons-design.md deleted file mode 100644 index 172d011057..0000000000 --- a/.changeset/four-buttons-design.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Adds ability to send an ExternalId with the assume role request to AWS diff --git a/.changeset/green-ducks-tan.md b/.changeset/green-ducks-tan.md deleted file mode 100644 index e096bfeb36..0000000000 --- a/.changeset/green-ducks-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Added UI Schema support for array items for example, support EntityPicker within an array field diff --git a/.changeset/happy-lies-wink.md b/.changeset/happy-lies-wink.md deleted file mode 100644 index 709cd3862e..0000000000 --- a/.changeset/happy-lies-wink.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -- Adds a new field `EntityNamePicker` that can be used in scaffolder templates to accept and validate an entity name. This field is registered by default, and can be used in templates by setting the `ui:field` property to `EntityNamePicker`. If you've customized your scaffolder field extensions, you can include this one by adding it when registering the scaffolder route: - -```diff -import { - ScaffolderFieldExtensions, -+ EntityNamePickerFieldExtension, -} from '@backstage/plugin-scaffolder'; - - }> - - {/* ...custom field extensions... */} - -+ - - ; -``` - -- Adds a new generic field `TextValuePicker` to be used when writing custom field extensions that use a standard UI with custom validation. An example of doing this can be found in `packages/app/src/components/scaffolder/customScaffolderExtensions.tsx`. diff --git a/.changeset/late-frogs-yell.md b/.changeset/late-frogs-yell.md deleted file mode 100644 index b3e45785e3..0000000000 --- a/.changeset/late-frogs-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': patch ---- - -Add support for serving the app with a base path other than `/`, which is enabled by including the path in `app.baseUrl`. diff --git a/.changeset/little-tools-melt.md b/.changeset/little-tools-melt.md deleted file mode 100644 index 5e55d27b7a..0000000000 --- a/.changeset/little-tools-melt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': minor ---- - -Use IdentityApi to provide Auth Token for SonarQubeClient Api calls diff --git a/.changeset/modern-ghosts-dress.md b/.changeset/modern-ghosts-dress.md deleted file mode 100644 index 6d3cdbfd3c..0000000000 --- a/.changeset/modern-ghosts-dress.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@backstage/core-app-api': patch -'@backstage/core-components': patch -'@backstage/core-plugin-api': patch -'@backstage/dev-utils': patch -'@backstage/test-utils': patch -'@backstage/test-utils-core': patch -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-search': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-tech-radar': patch -'@backstage/plugin-techdocs': patch ---- - -Switched `@types/react` dependency to request `*` rather than a specific version. diff --git a/.changeset/neat-meals-arrive.md b/.changeset/neat-meals-arrive.md deleted file mode 100644 index 292d98de26..0000000000 --- a/.changeset/neat-meals-arrive.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Updated the index page redirect to work with apps served on a different base path than `/`. - -To apply this change to an existing app, remove the `/` prefix from the target route in the `Navigate` element in `packages/app/src/App.tsx`: - -```diff -- -+ -``` diff --git a/.changeset/ninety-worms-kick.md b/.changeset/ninety-worms-kick.md deleted file mode 100644 index f350c6eb31..0000000000 --- a/.changeset/ninety-worms-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Adds an allow list of GitHub installations diff --git a/.changeset/perfect-seals-burn.md b/.changeset/perfect-seals-burn.md deleted file mode 100644 index 8d3a1c7619..0000000000 --- a/.changeset/perfect-seals-burn.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor -'@backstage/plugin-scaffolder-backend': minor ---- - -Add Bitbucket workspace and project fields to RepoUrlPicker to support Bitbucket cloud and server diff --git a/.changeset/pink-pugs-relate.md b/.changeset/pink-pugs-relate.md deleted file mode 100644 index e9de00901e..0000000000 --- a/.changeset/pink-pugs-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Switch `EventSource` implementation with header support from a Node.js API-based one to an XHR-based one. diff --git a/.changeset/plenty-carpets-jog.md b/.changeset/plenty-carpets-jog.md deleted file mode 100644 index f4586072a7..0000000000 --- a/.changeset/plenty-carpets-jog.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Use new composable `TechDocsIndexPage` and `DefaultTechDocsHome` - -Make the following changes to your `App.tsx` to migrate existing apps: - -```diff -- } /> -+ }> -+ -+ -+ } -+ /> -``` diff --git a/.changeset/quick-fans-smash.md b/.changeset/quick-fans-smash.md deleted file mode 100644 index b394b2af23..0000000000 --- a/.changeset/quick-fans-smash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-elasticsearch': patch ---- - -Fix to allow optionally reading `auth` parameter for custom hosted ElasticSearch instances. Also remove `bearer` auth config since it's currently unsupported. diff --git a/.changeset/rare-pugs-exercise.md b/.changeset/rare-pugs-exercise.md deleted file mode 100644 index e4071ee59c..0000000000 --- a/.changeset/rare-pugs-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Pass on credentials to the integrations package, so that it can properly pick the API route when using GitHub apps based auth diff --git a/.changeset/search-early-horses-sip.md b/.changeset/search-early-horses-sip.md deleted file mode 100644 index ce5612ae86..0000000000 --- a/.changeset/search-early-horses-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-pg': patch ---- - -Add `plugin-search-backend-module-pg` providing a postgres based search engine. -See the [README of `search-backend-module-pg`](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-pg/README.md) for usage instructions. diff --git a/.changeset/serious-kiwis-wonder.md b/.changeset/serious-kiwis-wonder.md deleted file mode 100644 index 0deba49616..0000000000 --- a/.changeset/serious-kiwis-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Remove repo restriction from GitHub credentials provider diff --git a/.changeset/shaggy-days-peel.md b/.changeset/shaggy-days-peel.md deleted file mode 100644 index b35d35f45c..0000000000 --- a/.changeset/shaggy-days-peel.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/create-app': patch ---- - -Migrate to using `webpack@5` 🎉 diff --git a/.changeset/sharp-berries-fail.md b/.changeset/sharp-berries-fail.md deleted file mode 100644 index a713b3fdee..0000000000 --- a/.changeset/sharp-berries-fail.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Add a complete prettier setup to the created project. Prettier used to only be added as a dependency to create apps, but there wasn't a complete setup included that makes it easy to run prettier. That has now changed, and the new `prettier:check` command can be used to check the formatting of the files in your created project. - -To apply this change to an existing app, a couple of changes need to be made. - -Create a `.prettierignore` file at the root of your repository with the following contents: - -``` -dist -dist-types -coverage -.vscode -``` - -Next update the root `package.json` by bumping the prettier version and adding the new `prettier:check` command: - -```diff - "scripts": { - ... -+ "prettier:check": "prettier --check .", - ... - }, - ... - "dependencies": { - ... -- "prettier": "^1.19.1" -+ "prettier": "^2.3.2" - } -``` - -Finally run `yarn prettier --write .` on your project to update the existing formatting. diff --git a/.changeset/sharp-donkeys-push.md b/.changeset/sharp-donkeys-push.md deleted file mode 100644 index fd6304118e..0000000000 --- a/.changeset/sharp-donkeys-push.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Removed the `/` prefix in the catalog `SidebarItem` element, as it is no longer needed. - -To apply this change to an existing app, remove the `/` prefix from the catalog and any other sidebar items in `packages/app/src/components/Root/Root.ts`: - -```diff -- -+ -``` diff --git a/.changeset/silent-dragons-fold.md b/.changeset/silent-dragons-fold.md deleted file mode 100644 index df5c271fb8..0000000000 --- a/.changeset/silent-dragons-fold.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/integration': minor ---- - -`getGitHubFileFetchUrl` and `getGitHubRequestOptions` now require a `credentials` argument. This is needed to address an issue where the raw route was chosen by the `UrlReader` when using GitHub Apps based auth. - -Deprecated the `getGitHubRequestOptions` function, which is no longer used internally. diff --git a/.changeset/small-actors-sing.md b/.changeset/small-actors-sing.md deleted file mode 100644 index 40de0f363f..0000000000 --- a/.changeset/small-actors-sing.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Switched `@types/react-dom` dependency to of the app package to request `*` rather than a specific version. - -To apply this change to an existing app, change the following in `packages/app/package.json`: - -```diff -- "@types/react-dom": "^16.9.8", -+ "@types/react-dom": "*", -``` diff --git a/.changeset/small-guests-invite.md b/.changeset/small-guests-invite.md deleted file mode 100644 index 507e0b3d12..0000000000 --- a/.changeset/small-guests-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch ---- - -Support deep linking in OpenAPI definitions. diff --git a/.changeset/spicy-crews-applaud.md b/.changeset/spicy-crews-applaud.md deleted file mode 100644 index d2707516d2..0000000000 --- a/.changeset/spicy-crews-applaud.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-react': patch ---- - -Move and rename `FavoriteEntity` component to `catalog-react` diff --git a/.changeset/techdocs-stale-breakfast-king.md b/.changeset/techdocs-stale-breakfast-king.md deleted file mode 100644 index cdbdb4d0ad..0000000000 --- a/.changeset/techdocs-stale-breakfast-king.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Stale TechDocs content (files that had previously been published but which have -since been removed) is now removed from storage at publish-time. This is now -supported by the following publishers: - -- Google GCS -- AWS S3 -- Azure Blob Storage - -You may need to apply a greater level of permissions (e.g. the ability to -delete objects in your storage provider) to any credentials/accounts used by -the TechDocs CLI or TechDocs backend in order for this change to take effect. - -For more details, see [#6132][issue-ref]. - -[issue-ref]: https://github.com/backstage/backstage/issues/6132 diff --git a/.changeset/tidy-jokes-train.md b/.changeset/tidy-jokes-train.md deleted file mode 100644 index b9d0d43bea..0000000000 --- a/.changeset/tidy-jokes-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Add Sign In and Handler resolver for Okta provider diff --git a/.changeset/violet-bobcats-wave.md b/.changeset/violet-bobcats-wave.md deleted file mode 100644 index 339d7e0e74..0000000000 --- a/.changeset/violet-bobcats-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Fix for `SidebarItem` matching the active route too broadly. diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 14927117ab..5ce4f341e1 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,33 @@ # example-app +## 0.2.41 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-cost-insights@0.11.4 + - @backstage/plugin-github-actions@0.4.16 + - @backstage/plugin-jenkins@0.5.3 + - @backstage/plugin-kubernetes@0.4.11 + - @backstage/plugin-pagerduty@0.3.11 + - @backstage/plugin-scaffolder@0.11.0 + - @backstage/plugin-sentry@0.3.18 + - @backstage/plugin-techdocs@0.10.4 + - @backstage/core-app-api@0.1.8 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-api-docs@0.6.6 + - @backstage/plugin-catalog@0.6.11 + - @backstage/plugin-catalog-import@0.5.17 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/plugin-explore@0.3.13 + - @backstage/plugin-lighthouse@0.2.23 + - @backstage/plugin-rollbar@0.3.12 + - @backstage/plugin-search@0.4.7 + - @backstage/plugin-tech-radar@0.4.5 + - @backstage/cli@0.7.8 + - @backstage/integration-react@0.1.7 + ## 0.2.40 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index df69e0835a..dcf14c88c1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,42 +1,42 @@ { "name": "example-app", - "version": "0.2.40", + "version": "0.2.41", "private": true, "bundled": true, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-api-docs": "^0.6.5", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-api-docs": "^0.6.6", "@backstage/plugin-badges": "^0.2.7", - "@backstage/plugin-catalog": "^0.6.10", - "@backstage/plugin-catalog-import": "^0.5.16", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/plugin-catalog": "^0.6.11", + "@backstage/plugin-catalog-import": "^0.5.17", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/plugin-circleci": "^0.2.21", "@backstage/plugin-cloudbuild": "^0.2.21", "@backstage/plugin-code-coverage": "^0.1.9", - "@backstage/plugin-cost-insights": "^0.11.3", - "@backstage/plugin-explore": "^0.3.12", + "@backstage/plugin-cost-insights": "^0.11.4", + "@backstage/plugin-explore": "^0.3.13", "@backstage/plugin-gcp-projects": "^0.3.2", - "@backstage/plugin-github-actions": "^0.4.15", + "@backstage/plugin-github-actions": "^0.4.16", "@backstage/plugin-graphiql": "^0.2.14", - "@backstage/plugin-jenkins": "^0.5.2", + "@backstage/plugin-jenkins": "^0.5.3", "@backstage/plugin-kafka": "^0.2.13", - "@backstage/plugin-kubernetes": "^0.4.10", - "@backstage/plugin-lighthouse": "^0.2.22", + "@backstage/plugin-kubernetes": "^0.4.11", + "@backstage/plugin-lighthouse": "^0.2.23", "@backstage/plugin-newrelic": "^0.3.2", "@backstage/plugin-org": "^0.3.19", - "@backstage/plugin-pagerduty": "0.3.10", - "@backstage/plugin-rollbar": "^0.3.11", - "@backstage/plugin-scaffolder": "^0.10.3", - "@backstage/plugin-search": "^0.4.6", - "@backstage/plugin-sentry": "^0.3.17", + "@backstage/plugin-pagerduty": "0.3.11", + "@backstage/plugin-rollbar": "^0.3.12", + "@backstage/plugin-scaffolder": "^0.11.0", + "@backstage/plugin-search": "^0.4.7", + "@backstage/plugin-sentry": "^0.3.18", "@backstage/plugin-shortcuts": "^0.1.6", - "@backstage/plugin-tech-radar": "^0.4.4", - "@backstage/plugin-techdocs": "^0.10.3", + "@backstage/plugin-tech-radar": "^0.4.5", + "@backstage/plugin-techdocs": "^0.10.4", "@backstage/plugin-todo": "^0.1.7", "@backstage/plugin-user-settings": "^0.3.3", "@backstage/search-common": "^0.1.3", @@ -60,7 +60,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.16", + "@backstage/test-utils": "^0.1.17", "@rjsf/core": "^3.0.0", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index f6f61133ea..297f07b0b4 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-common +## 0.8.9 + +### Patch Changes + +- f7ce7c565: Use a more informative error message when URL reading isn't allowed due to no reader matching the target URL. +- ce1958021: Pass on credentials to the integrations package, so that it can properly pick the API route when using GitHub apps based auth +- Updated dependencies + - @backstage/integration@0.6.0 + ## 0.8.8 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 10602fa2fa..4c7aacdf96 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.8.8", + "version": "0.8.9", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -33,7 +33,7 @@ "@backstage/config": "^0.1.6", "@backstage/config-loader": "^0.6.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", + "@backstage/integration": "^0.6.0", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", @@ -76,8 +76,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/test-utils": "^0.1.15", + "@backstage/cli": "^0.7.8", + "@backstage/test-utils": "^0.1.17", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 2faa5afbc3..5671b41fb3 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/backend-test-utils +## 0.1.5 + +### Patch Changes + +- 524045758: Make sure that the unique databases names generated by `TestDatabases.create()` + don't collide. +- Updated dependencies + - @backstage/backend-common@0.8.9 + - @backstage/cli@0.7.8 + ## 0.1.4 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 9ec11a58b7..f1cfffeb37 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.5", - "@backstage/cli": "^0.7.1", + "@backstage/backend-common": "^0.8.9", + "@backstage/cli": "^0.7.8", "@backstage/config": "^0.1.5", "knex": "^0.95.1", "mysql2": "^2.2.5", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.7.8", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index a3e08db6b3..39eabe46b0 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,24 @@ # example-backend +## 0.2.41 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.3.20 + - @backstage/integration@0.6.0 + - @backstage/plugin-scaffolder-backend@0.15.0 + - @backstage/backend-common@0.8.9 + - @backstage/plugin-kubernetes-backend@0.3.14 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.2 + - @backstage/plugin-search-backend-module-pg@0.1.1 + - @backstage/plugin-catalog-backend@0.13.2 + - @backstage/plugin-code-coverage-backend@0.1.9 + - @backstage/plugin-scaffolder-backend-module-rails@0.1.4 + - @backstage/plugin-techdocs-backend@0.9.2 + - @backstage/plugin-todo-backend@0.1.9 + - example-app@0.2.41 + ## 0.2.38 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 33221bde01..b2877dedf5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.38", + "version": "0.2.41", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,35 +27,35 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.8.7", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.17", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.5", - "@backstage/integration": "^0.5.8", + "@backstage/integration": "^0.6.0", "@backstage/plugin-app-backend": "^0.3.15", - "@backstage/plugin-auth-backend": "^0.3.18", + "@backstage/plugin-auth-backend": "^0.3.20", "@backstage/plugin-badges-backend": "^0.1.8", - "@backstage/plugin-catalog-backend": "^0.13.0", - "@backstage/plugin-code-coverage-backend": "^0.1.8", + "@backstage/plugin-catalog-backend": "^0.13.2", + "@backstage/plugin-code-coverage-backend": "^0.1.9", "@backstage/plugin-graphql-backend": "^0.1.8", "@backstage/plugin-jenkins-backend": "^0.1.2", - "@backstage/plugin-kubernetes-backend": "^0.3.11", + "@backstage/plugin-kubernetes-backend": "^0.3.14", "@backstage/plugin-kafka-backend": "^0.2.8", "@backstage/plugin-proxy-backend": "^0.2.9", "@backstage/plugin-rollbar-backend": "^0.1.13", - "@backstage/plugin-scaffolder-backend": "^0.14.1", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.3", + "@backstage/plugin-scaffolder-backend": "^0.15.0", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.4", "@backstage/plugin-search-backend": "^0.2.3", "@backstage/plugin-search-backend-node": "^0.4.0", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.1", - "@backstage/plugin-search-backend-module-pg": "^0.1.0", - "@backstage/plugin-techdocs-backend": "^0.9.0", - "@backstage/plugin-todo-backend": "^0.1.8", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.2", + "@backstage/plugin-search-backend-module-pg": "^0.1.1", + "@backstage/plugin-techdocs-backend": "^0.9.2", + "@backstage/plugin-todo-backend": "^0.1.9", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^10.2.2", "dockerode": "^3.2.1", - "example-app": "^0.2.37", + "example-app": "^0.2.41", "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^0.95.1", @@ -65,7 +65,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.5", + "@backstage/cli": "^0.7.8", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index dc4512adee..848a106db0 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli +## 0.7.8 + +### Patch Changes + +- c4ef9181a: Migrate to using `webpack@5` 🎉 + ## 0.7.7 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index c0e6d987f1..63b8db373e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.7.7", + "version": "0.7.8", "private": false, "publishConfig": { "access": "public" @@ -118,13 +118,13 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.8.8", + "@backstage/backend-common": "^0.8.9", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.9", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index b709350a73..192e6810bc 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/codemods +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.8 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.1.8 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 38602ca03d..8f87631897 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.8", + "version": "0.1.9", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index dbb26fe2a0..1ec944ac4a 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-app-api +## 0.1.8 + +### Patch Changes + +- 362657623: Add support for serving the app with a base path other than `/`, which is enabled by including the path in `app.baseUrl`. +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.1.7 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 204550323c..2b66a991f1 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.7", + "version": "0.1.8", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", + "@backstage/core-components": "^0.3.1", "@backstage/config": "^0.1.6", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,9 +44,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/test-utils": "^0.1.16", - "@backstage/test-utils-core": "^0.1.1", + "@backstage/cli": "^0.7.8", + "@backstage/test-utils": "^0.1.17", + "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index cb93e38ba5..0241234574 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/core-components +## 0.3.1 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- 55a5dbd54: Fix for `SidebarItem` matching the active route too broadly. +- Updated dependencies + - @backstage/core-plugin-api@0.1.6 + ## 0.3.0 ### Minor Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 5ae86a0030..69f2019943 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.3.0", + "version": "0.3.1", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.6", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/theme": "^0.2.9", "@material-table/core": "^3.1.0", @@ -70,9 +70,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.7", - "@backstage/cli": "^0.7.7", - "@backstage/test-utils": "^0.1.16", + "@backstage/core-app-api": "^0.1.8", + "@backstage/cli": "^0.7.8", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 5ea46a334e..2a76ab243f 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/core-plugin-api +## 0.1.6 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. + ## 0.1.5 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index a0057c1b0c..d482795c90 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.1.5", + "version": "0.1.6", "private": false, "publishConfig": { "access": "public", @@ -41,10 +41,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/test-utils": "^0.1.16", - "@backstage/test-utils-core": "^0.1.1", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/test-utils": "^0.1.17", + "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 318c508211..baee775947 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/create-app +## 0.3.35 + +### Patch Changes + +- 362ea5a72: Updated the index page redirect to work with apps served on a different base path than `/`. + + To apply this change to an existing app, remove the `/` prefix from the target route in the `Navigate` element in `packages/app/src/App.tsx`: + + ```diff + - + + + ``` + +- 80582cbec: Use new composable `TechDocsIndexPage` and `DefaultTechDocsHome` + + Make the following changes to your `App.tsx` to migrate existing apps: + + ```diff + - } /> + + }> + + + + + + } + + /> + ``` + +- c4ef9181a: Migrate to using `webpack@5` 🎉 +- 56c773909: Add a complete prettier setup to the created project. Prettier used to only be added as a dependency to create apps, but there wasn't a complete setup included that makes it easy to run prettier. That has now changed, and the new `prettier:check` command can be used to check the formatting of the files in your created project. + + To apply this change to an existing app, a couple of changes need to be made. + + Create a `.prettierignore` file at the root of your repository with the following contents: + + ``` + dist + dist-types + coverage + .vscode + ``` + + Next update the root `package.json` by bumping the prettier version and adding the new `prettier:check` command: + + ```diff + "scripts": { + ... + + "prettier:check": "prettier --check .", + ... + }, + ... + "dependencies": { + ... + - "prettier": "^1.19.1" + + "prettier": "^2.3.2" + } + ``` + + Finally run `yarn prettier --write .` on your project to update the existing formatting. + +- 9f8f8dd6b: Removed the `/` prefix in the catalog `SidebarItem` element, as it is no longer needed. + + To apply this change to an existing app, remove the `/` prefix from the catalog and any other sidebar items in `packages/app/src/components/Root/Root.ts`: + + ```diff + - + + + ``` + +- 56c773909: Switched `@types/react-dom` dependency to of the app package to request `*` rather than a specific version. + + To apply this change to an existing app, change the following in `packages/app/package.json`: + + ```diff + - "@types/react-dom": "^16.9.8", + + "@types/react-dom": "*", + ``` + ## 0.3.34 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 130c69603a..3be83ec6be 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.34", + "version": "0.3.35", "private": false, "publishConfig": { "access": "public" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 5e570164a0..86a6e44a75 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/dev-utils +## 0.2.6 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-app-api@0.1.8 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/test-utils@0.1.17 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + ## 0.2.5 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index a7efd5c778..66f4d7bba6 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.5", + "version": "0.2.6", "private": false, "publishConfig": { "access": "public", @@ -29,13 +29,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.7", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-app-api": "^0.1.8", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/catalog-model": "^0.9.0", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.1", - "@backstage/test-utils": "^0.1.16", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-catalog-react": "^0.4.2", + "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -50,7 +50,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.7.7", + "@backstage/cli": "^0.7.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index e5877d9079..281fed0211 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/integration-react +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.1.6 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 6bf8cba4a0..3217881046 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/integration": "^0.5.9", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/integration": "^0.6.0", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -33,9 +33,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 5b13f89d14..54b2d374cc 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/integration +## 0.6.0 + +### Minor Changes + +- ce1958021: `getGitHubFileFetchUrl` and `getGitHubRequestOptions` now require a `credentials` argument. This is needed to address an issue where the raw route was chosen by the `UrlReader` when using GitHub Apps based auth. + + Deprecated the `getGitHubRequestOptions` function, which is no longer used internally. + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- 68af4d556: Adds an allow list of GitHub installations +- 5fd31c2f4: Remove repo restriction from GitHub credentials provider + ## 0.5.9 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 9ff31b9706..a17572c47e 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.5.9", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,9 +37,9 @@ "luxon": "^2.0.2" }, "devDependencies": { - "@backstage/cli": "^0.7.7", + "@backstage/cli": "^0.7.8", "@backstage/config-loader": "^0.6.6", - "@backstage/test-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.17", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", "msw": "^0.29.0" diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 5f115daabf..2b925f453f 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/techdocs-common +## 0.8.1 + +### Patch Changes + +- bc405be6e: Stale TechDocs content (files that had previously been published but which have + since been removed) is now removed from storage at publish-time. This is now + supported by the following publishers: + + - Google GCS + - AWS S3 + - Azure Blob Storage + + You may need to apply a greater level of permissions (e.g. the ability to + delete objects in your storage provider) to any credentials/accounts used by + the TechDocs CLI or TechDocs backend in order for this change to take effect. + + For more details, see [#6132][issue-ref]. + + [issue-ref]: https://github.com/backstage/backstage/issues/6132 + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + ## 0.8.0 ### Minor Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 65d16bf0f7..8726c4ac37 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.8.0", + "version": "0.8.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,11 +38,11 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.8.8", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", + "@backstage/integration": "^0.6.0", "@google-cloud/storage": "^5.6.0", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", @@ -58,7 +58,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.7", + "@backstage/cli": "^0.7.8", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/test-utils-core/CHANGELOG.md b/packages/test-utils-core/CHANGELOG.md new file mode 100644 index 0000000000..a84ab931f3 --- /dev/null +++ b/packages/test-utils-core/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/test-utils-core + +## 0.1.2 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 84475d3c5d..b88184f121 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1", + "version": "0.1.2", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index a16b2a6259..08594ae824 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/test-utils +## 0.1.17 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-app-api@0.1.8 + - @backstage/core-plugin-api@0.1.6 + - @backstage/test-utils-core@0.1.2 + ## 0.1.16 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 80fb029f34..0134382b7f 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.16", + "version": "0.1.17", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.6", - "@backstage/core-plugin-api": "^0.1.4", - "@backstage/test-utils-core": "^0.1.1", + "@backstage/core-app-api": "^0.1.8", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/test-utils-core": "^0.1.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.10.1", @@ -46,7 +46,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.6", + "@backstage/cli": "^0.7.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index a3e06a1a65..728667dabd 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-api-docs +## 0.6.6 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- 0383314c9: Support deep linking in OpenAPI definitions. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog@0.6.11 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.6.5 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 18c7952fb2..f3bacf0a13 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.6.5", + "version": "0.6.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "dependencies": { "@asyncapi/react-component": "^0.23.0", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog": "^0.6.10", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog": "^0.6.11", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.12.2", @@ -52,10 +52,10 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 9183eac0ad..23a0445929 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-auth-backend +## 0.3.20 + +### Patch Changes + +- 29f7cfffb: Added `resolveCatalogMembership` utility to query the catalog for additional authentication claims within sign-in resolvers. +- 8bedb75ae: Update Luxon dependency to 2.x +- bfe0ff93f: Add Sign In and Handler resolver for Okta provider +- Updated dependencies + - @backstage/backend-common@0.8.9 + - @backstage/test-utils@0.1.17 + ## 0.3.19 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 2d8ce3657f..89116bd425 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.3.19", + "version": "0.3.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.7", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/test-utils": "^0.1.16", + "@backstage/test-utils": "^0.1.17", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", @@ -68,7 +68,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.6", + "@backstage/cli": "^0.7.8", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/badges/package.json b/plugins/badges/package.json index a46081443a..fb97217d54 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 3ba44e37d4..45050ae0e3 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-bitrise +## 0.1.10 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.1.9 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 0e81d855c8..88e6b4b000 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitrise", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 3849fec510..bd66da47b6 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend +## 0.13.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + ## 0.13.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index d67cd7553b..0bf5a08dfd 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.13.1", + "version": "0.13.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.6", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.8", + "@backstage/integration": "^0.6.0", "@backstage/plugin-search-backend-node": "^0.4.0", "@backstage/search-common": "^0.1.2", "@octokit/graphql": "^4.5.8", @@ -61,9 +61,9 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.4", - "@backstage/cli": "^0.7.6", - "@backstage/test-utils": "^0.1.16", + "@backstage/backend-test-utils": "^0.1.5", + "@backstage/cli": "^0.7.8", + "@backstage/test-utils": "^0.1.17", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 421871cb52..a60a97759f 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-import +## 0.5.17 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + ## 0.5.16 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 35e44bd070..44609323d5 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.5.16", + "version": "0.5.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "dependencies": { "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/integration": "^0.5.9", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/integration": "^0.6.0", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -54,10 +54,10 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 1a9f329f4b..f152a6c003 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-react +## 0.4.2 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- a440d3b38: Move and rename `FavoriteEntity` component to `catalog-react` +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-app-api@0.1.8 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.4.1 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 0737ece2ff..426ebb5038 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.4.1", + "version": "0.4.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "dependencies": { "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-app-api": "^0.1.7", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/integration": "^0.5.9", + "@backstage/core-app-api": "^0.1.8", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/integration": "^0.6.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index cd3d846382..dc9dc66e47 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog +## 0.6.11 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- a440d3b38: Move and rename `FavoriteEntity` component to `catalog-react` +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + ## 0.6.10 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 65b741a855..e1132ab299 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.6.10", + "version": "0.6.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,12 +32,12 @@ "dependencies": { "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/integration": "^0.6.0", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -54,10 +54,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 06e062e785..85ae5d522f 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -50,10 +50,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 504aa7cfa8..ae64b1ebc3 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 7e008b4dcf..1c8b9f18ea 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-code-coverage-backend +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + ## 0.1.8 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index fa2f38b3eb..cbebaaf5d8 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-coverage-backend", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,12 +19,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.5", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.16", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.8", + "@backstage/integration": "^0.6.0", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -36,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.7.8", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.29.0", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 75915fc86b..dd00c56239 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -22,8 +22,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", @@ -40,10 +40,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index a09f6d4775..1f63d733c1 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -35,10 +35,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 60a04574fa..7f4ccf2db8 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cost-insights +## 0.11.4 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.11.3 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 9304f6abe4..ce8abcd12c 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.11.3", + "version": "0.11.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -54,10 +54,10 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index d21b4e0401..49fdfc8cba 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-explore +## 0.3.13 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.3.12 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index d66d6e8465..a687f0f43c 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.3.12", + "version": "0.3.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/plugin-explore-react": "^0.0.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index a5531ac0bc..c4fc6a3f48 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -34,10 +34,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 194b6024cc..62d5b2c5d8 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-fossa +## 0.2.14 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.2.13 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 5c540d8405..069474d338 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-fossa", - "version": "0.2.13", + "version": "0.2.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index d265fc0b22..5dd185a8ce 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 8fab58d7d9..10f6204d92 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-git-release-manager +## 0.2.3 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.2.2 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 780961966c..a762590d94 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-git-release-manager", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/integration": "^0.5.9", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/integration": "^0.6.0", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react-hooks": "^3.4.2", "@testing-library/react": "^11.2.5", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index e5fb95c4a4..0ea9cd57dd 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.4.16 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.4.15 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 7403a6f259..9e64d7d37c 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.4.15", + "version": "0.4.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/integration": "^0.5.9", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/integration": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 6f5f7ac37f..a31d0c0b6a 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-deployments +## 0.1.14 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + ## 0.1.13 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index f034213515..62d535cb7f 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-deployments", - "version": "0.1.13", + "version": "0.1.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,12 +21,12 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/integration": "^0.6.0", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 1c7c19f446..a71eef4b3f 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index cfac7e7e5a..b8d19cad44 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 299adabda5..cad51a3f69 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-ilert +## 0.1.9 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.1.8 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 0e60c73f8f..7bc510f934 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-ilert", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@date-io/luxon": "2.x", "@material-ui/core": "^4.12.2", @@ -38,10 +38,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index c680c70726..90352a7f95 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-jenkins +## 0.5.3 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.5.2 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 829e407276..99f5fa2f51 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 37720f5334..607d980e4e 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -34,10 +34,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 8b5db3086a..ab8263abe3 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-backend +## 0.3.14 + +### Patch Changes + +- bbcd92afa: Adds ability to send an ExternalId with the assume role request to AWS +- Updated dependencies + - @backstage/backend-common@0.8.9 + ## 0.3.13 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 82347dc743..6abbd6ca61 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.3.13", + "version": "0.3.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.8", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", "@backstage/plugin-kubernetes-common": "^0.1.3", @@ -53,7 +53,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.7", + "@backstage/cli": "^0.7.8", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index d307ec2ced..c3f3b292a9 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes +## 0.4.11 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.4.10 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3a7b67f939..b86ce4b85b 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.4.10", + "version": "0.4.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/plugin-kubernetes-common": "^0.1.3", "@backstage/theme": "^0.2.9", "@kubernetes/client-node": "^0.15.0", @@ -50,10 +50,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index bde0acfa6f..d2ad6ce91e 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-lighthouse +## 0.2.23 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.2.22 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index d9bf3345d5..177c5e2964 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.22", + "version": "0.2.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 2ca36f9897..8baf70cd69 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/package.json b/plugins/org/package.json index 8cad5e31c1..c6be40fc51 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index aa2b5cd583..b07d102131 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-pagerduty +## 0.3.11 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.3.10 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 80b6e75f5c..dc918b2ae9 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.3.10", + "version": "0.3.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 5bb3f2a021..da263c3a2b 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-rollbar +## 0.3.12 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.3.11 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 68eb7f77b3..ff6db11a5e 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.3.11", + "version": "0.3.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md new file mode 100644 index 0000000000..5ed0f15033 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -0,0 +1,10 @@ +# @backstage/plugin-scaffolder-backend-module-cookiecutter + +## 0.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/plugin-scaffolder-backend@0.15.0 + - @backstage/backend-common@0.8.9 diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index f321d6b45c..252ebb8cb8 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,10 +19,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.6", + "@backstage/backend-common": "^0.8.9", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.7", - "@backstage/plugin-scaffolder-backend": "^0.14.0", + "@backstage/integration": "^0.6.0", + "@backstage/plugin-scaffolder-backend": "^0.15.0", "@backstage/config": "^0.1.5", "command-exists": "^1.2.9", "fs-extra": "10.0.0", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.3", + "@backstage/cli": "^0.7.8", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 857667bc6d..ce81cb7c46 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/plugin-scaffolder-backend@0.15.0 + - @backstage/backend-common@0.8.9 + ## 0.1.3 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 3062136a76..0ee28e4325 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,16 +20,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.6", - "@backstage/plugin-scaffolder-backend": "^0.14.0", + "@backstage/backend-common": "^0.8.9", + "@backstage/plugin-scaffolder-backend": "^0.15.0", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.8", + "@backstage/integration": "^0.6.0", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.4", + "@backstage/cli": "^0.7.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index f1ac87be05..eea4a87768 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend +## 0.15.0 + +### Minor Changes + +- e30646aeb: Add Bitbucket workspace and project fields to RepoUrlPicker to support Bitbucket cloud and server + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.1 + ## 0.14.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c67d118d47..44389c5afe 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.14.2", + "version": "0.15.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,13 +29,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.8", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.17", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.0", + "@backstage/integration": "^0.6.0", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.1", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", @@ -66,8 +66,8 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/test-utils": "^0.1.15", + "@backstage/cli": "^0.7.8", + "@backstage/test-utils": "^0.1.17", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 859407ec88..d97ce8e54c 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-scaffolder +## 0.11.0 + +### Minor Changes + +- e30646aeb: Add Bitbucket workspace and project fields to RepoUrlPicker to support Bitbucket cloud and server + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- 7894421f1: Added UI Schema support for array items for example, support EntityPicker within an array field +- fa84fe44e: - Adds a new field `EntityNamePicker` that can be used in scaffolder templates to accept and validate an entity name. This field is registered by default, and can be used in templates by setting the `ui:field` property to `EntityNamePicker`. If you've customized your scaffolder field extensions, you can include this one by adding it when registering the scaffolder route: + + ```diff + import { + ScaffolderFieldExtensions, + + EntityNamePickerFieldExtension, + } from '@backstage/plugin-scaffolder'; + + }> + + {/* ...custom field extensions... */} + + + + + ; + ``` + + - Adds a new generic field `TextValuePicker` to be used when writing custom field extensions that use a standard UI with custom validation. An example of doing this can be found in `packages/app/src/components/scaffolder/customScaffolderExtensions.tsx`. + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + ## 0.10.3 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index f9394bf7a0..84720e95a8 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.10.3", + "version": "0.11.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "@backstage/catalog-client": "^0.3.18", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/integration": "^0.6.0", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -63,10 +63,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md new file mode 100644 index 0000000000..6dd34513f0 --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/plugin-search-backend-module-elasticsearch + +## 0.0.2 + +### Patch Changes + +- 76872096b: Fix to allow optionally reading `auth` parameter for custom hosted ElasticSearch instances. Also remove `bearer` auth config since it's currently unsupported. diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index dd9ca7703c..de55d3952e 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", - "version": "0.0.1", + "version": "0.0.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.8.6", - "@backstage/cli": "^0.7.4", + "@backstage/backend-common": "^0.8.9", + "@backstage/cli": "^0.7.8", "@elastic/elasticsearch-mock": "^0.3.0" }, "files": [ diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md new file mode 100644 index 0000000000..1607585348 --- /dev/null +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -0,0 +1,10 @@ +# @backstage/plugin-search-backend-module-pg + +## 0.1.1 + +### Patch Changes + +- 9255e1430: Add `plugin-search-backend-module-pg` providing a postgres based search engine. + See the [README of `search-backend-module-pg`](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-pg/README.md) for usage instructions. +- Updated dependencies + - @backstage/backend-common@0.8.9 diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 9463e5c5a9..3b7fe0b28f 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend-module-pg", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,15 +19,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.7", + "@backstage/backend-common": "^0.8.9", "@backstage/search-common": "^0.1.2", "@backstage/plugin-search-backend-node": "^0.4.0", "lodash": "^4.17.15", "knex": "^0.95.1" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.4", - "@backstage/cli": "^0.7.6" + "@backstage/backend-test-utils": "^0.1.5", + "@backstage/cli": "^0.7.8" }, "files": [ "dist", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 81872df49e..0864dd740e 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search +## 0.4.7 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.4.6 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 1b32c49e75..a535278e93 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.4.6", + "version": "0.4.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/search-common": "^0.1.3", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index edc81f6f1a..9f5656f1e2 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-sentry +## 0.3.18 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.3.17 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index c8f69b3873..a660a15cd4 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.17", + "version": "0.3.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 898ddb5908..4ac197f589 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,10 +36,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index efc4ba3c86..a29ee6365d 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-sonarqube +## 0.2.0 + +### Minor Changes + +- 8db48b968: Use IdentityApi to provide Auth Token for SonarQubeClient Api calls + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.1.24 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index e20826b464..13d375dfae 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.24", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 25cad60053..73ba4ee6bd 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-splunk-on-call +## 0.3.8 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog-react@0.4.2 + ## 0.3.7 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index c83c1fe788..f2eff3b739 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-splunk-on-call", - "version": "0.3.7", + "version": "0.3.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,10 +46,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index d507ee7746..7b4daca110 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-tech-radar +## 0.4.5 + +### Patch Changes + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.4.4 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 0866099e18..55e5385a85 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.4.4", + "version": "0.4.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index b13e8bfbfa..1f2e5d6bc2 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-techdocs-backend +## 0.9.2 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + - @backstage/techdocs-common@0.8.1 + ## 0.9.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index ccaf0de227..d08d5141cd 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.9.1", + "version": "0.9.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,14 +30,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.8", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.17", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", + "@backstage/integration": "^0.6.0", "@backstage/search-common": "^0.1.3", - "@backstage/techdocs-common": "^0.8.0", + "@backstage/techdocs-common": "^0.8.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", @@ -50,8 +50,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/test-utils": "^0.1.15", + "@backstage/cli": "^0.7.8", + "@backstage/test-utils": "^0.1.17", "@types/dockerode": "^3.2.1", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 507dd8693f..0b088d36cd 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-techdocs +## 0.10.4 + +### Patch Changes + +- a440d3b38: Expose a new composable `TechDocsIndexPage` and a `DefaultTechDocsHome` with support for starring docs and filtering on owned, starred, owner, and tags. + + You can migrate to the new UI view by making the following changes in your `App.tsx`: + + ```diff + - } /> + + }> + + + + + + } + + /> + ``` + +- 56c773909: Switched `@types/react` dependency to request `*` rather than a specific version. +- 8a3e46591: Switch `EventSource` implementation with header support from a Node.js API-based one to an XHR-based one. +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + - @backstage/plugin-catalog@0.6.11 + - @backstage/plugin-catalog-react@0.4.2 + - @backstage/integration-react@0.1.7 + ## 0.10.3 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index bb9f8a7c95..ddb223be89 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.10.3", + "version": "0.10.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,13 +33,13 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.9", - "@backstage/integration-react": "^0.1.6", - "@backstage/plugin-catalog": "^0.6.10", - "@backstage/plugin-catalog-react": "^0.4.1", + "@backstage/integration": "^0.6.0", + "@backstage/integration-react": "^0.1.7", + "@backstage/plugin-catalog": "^0.6.11", + "@backstage/plugin-catalog-react": "^0.4.2", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -59,10 +59,10 @@ "git-url-parse": "~11.4.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index cc69ae6c3d..bc27aed097 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-todo-backend +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + ## 0.1.8 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 61e84f8377..0d59f2b073 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo-backend", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,12 +24,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.8.5", + "@backstage/backend-common": "^0.8.9", "@backstage/catalog-client": "^0.3.16", "@backstage/catalog-model": "^0.9.0", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.5.8", + "@backstage/integration": "^0.6.0", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.7.8", "@types/supertest": "^2.0.8", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/todo/package.json b/plugins/todo/package.json index d26dc79358..6cfe7790db 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -27,8 +27,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.0", - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.4.1", "@backstage/theme": "^0.2.9", @@ -40,10 +40,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index e31209f857..a55390ab86 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,11 +42,11 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", "@backstage/core-plugin-api": "^0.1.5", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 1ad3f4962a..d7e7e0e355 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -30,8 +30,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 80a9188aa3..b0123dcdb1 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-xcmetrics +## 0.2.2 + +### Patch Changes + +- 8bedb75ae: Update Luxon dependency to 2.x +- Updated dependencies + - @backstage/core-components@0.3.1 + - @backstage/core-plugin-api@0.1.6 + ## 0.2.1 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 8ebcf9a85e..11153ab6b5 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-xcmetrics", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.0", - "@backstage/core-plugin-api": "^0.1.5", + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", "@backstage/errors": "^0.1.1", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", @@ -33,10 +33,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.7", - "@backstage/core-app-api": "^0.1.7", - "@backstage/dev-utils": "^0.2.5", - "@backstage/test-utils": "^0.1.16", + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/yarn.lock b/yarn.lock index a9f93d9de1..d14fac46fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2448,6 +2448,18 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/integration@^0.5.8": + version "0.5.9" + resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.5.9.tgz#9eeb2f570794b196a8e769d8628b68937863a0fc" + integrity sha512-SfYcIHMurasJ5AlNzvq/4+8VLx5ONZ1vZqSG9zX/jvASCyrQY+Ifumaj13XBBuykTv61eHaqsLcEFtd0zgBtpQ== + dependencies: + "@backstage/config" "^0.1.6" + "@octokit/auth-app" "^3.4.0" + "@octokit/rest" "^18.5.3" + cross-fetch "^3.0.6" + git-url-parse "~11.4.4" + luxon "^1.25.0" + "@backstage/plugin-catalog-react@^0.3.0": version "0.3.1" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.3.1.tgz#d30a063a4ceb4d446310a687d19c987f55824fdb" @@ -18716,7 +18728,7 @@ luxon@2.0.2, luxon@^2.0.2: resolved "https://registry.npmjs.org/luxon/-/luxon-2.0.2.tgz#11f2cd4a11655fdf92e076b5782d7ede5bcdd133" integrity sha512-ZRioYLCgRHrtTORaZX1mx+jtxKtKuI5ZDvHNAmqpUzGqSrR+tL4FVLn/CUGMA3h0+AKD1MAxGI5GnCqR5txNqg== -luxon@^1.27.0: +luxon@^1.25.0, luxon@^1.27.0: version "1.28.0" resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== From 918b535041bbc265a9f1af9769759a0c9f3e9f40 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:53:19 +0200 Subject: [PATCH 43/46] Add missing dependencies for requests in BuildDetailsComponent Signed-off-by: Niklas Granander --- .../BuildDetailsComponent/BuildDetailsComponent.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx index 0c636b1dcd..44f43a268b 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -47,18 +47,21 @@ export const BuildDetailsComponent = ({ }: BuildDetailsProps) => { const classes = useStyles(); const client = useApi(xcmetricsApiRef); - const hostResult = useAsync(async () => client.getBuildHost(build.id), []); + const hostResult = useAsync( + async () => client.getBuildHost(build.id), + [build.id], + ); const errorsResult = useAsync( async () => client.getBuildErrors(build.id), - [], + [build.id], ); const warningsResult = useAsync( async () => client.getBuildWarnings(build.id), - [], + [build.id], ); const metadataResult = useAsync( async () => client.getBuildMetadata(build.id), - [], + [build.id], ); const buildDetails = { From 3bbe05bb0286579e06fdda6faeaee3d3579ccc25 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:54:52 +0200 Subject: [PATCH 44/46] Use named exports Signed-off-by: Niklas Granander --- plugins/xcmetrics/src/components/AccordionComponent/index.ts | 2 +- plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts | 2 +- .../xcmetrics/src/components/BuildTimelineComponent/index.ts | 2 +- plugins/xcmetrics/src/components/BuildsPage/index.ts | 2 +- .../xcmetrics/src/components/PreformattedTextComponent/index.ts | 2 +- plugins/xcmetrics/src/components/StatusIconComponent/index.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/xcmetrics/src/components/AccordionComponent/index.ts b/plugins/xcmetrics/src/components/AccordionComponent/index.ts index 1e71dc81ca..33632d6828 100644 --- a/plugins/xcmetrics/src/components/AccordionComponent/index.ts +++ b/plugins/xcmetrics/src/components/AccordionComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './AccordionComponent'; +export { AccordionComponent } from './AccordionComponent'; diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts index 574b29f32e..4e50cf23a0 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './BuildDetailsComponent'; +export { BuildDetailsComponent, withRequest } from './BuildDetailsComponent'; diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts index 2a6a612acf..fe294f8467 100644 --- a/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './BuildTimelineComponent'; +export { BuildTimelineComponent } from './BuildTimelineComponent'; diff --git a/plugins/xcmetrics/src/components/BuildsPage/index.ts b/plugins/xcmetrics/src/components/BuildsPage/index.ts index d04503499c..47fe9166df 100644 --- a/plugins/xcmetrics/src/components/BuildsPage/index.ts +++ b/plugins/xcmetrics/src/components/BuildsPage/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './BuildsPage'; +export { BuildsPage } from './BuildsPage'; diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts index e923eeb224..dbc27dd869 100644 --- a/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './PreformattedTextComponent'; +export { PreformattedTextComponent } from './PreformattedTextComponent'; diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/index.ts b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts index e680e6177c..b69415bd78 100644 --- a/plugins/xcmetrics/src/components/StatusIconComponent/index.ts +++ b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './StatusIconComponent'; +export { StatusIconComponent } from './StatusIconComponent'; From 8aed3a5beb5444ec83135f7945439e0822b59e87 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:55:47 +0200 Subject: [PATCH 45/46] Use ellipsis instead of dots Signed-off-by: Niklas Granander --- .../PreformattedTextComponent/PreformattedTextComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx index a5ad997ee8..b73e2cead5 100644 --- a/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx @@ -89,8 +89,8 @@ export const PreformattedTextComponent = ({ tabIndex={expandable ? 0 : undefined} >
-          {text.slice(0, maxChars - 3).trim()}
-          {text.length > maxChars && '...'}
+          {text.slice(0, maxChars - 1).trim()}
+          {text.length > maxChars - 1 && '…'}
         
From 9dd49cfd3b298584c408e233918f6f93047992c3 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:56:34 +0200 Subject: [PATCH 46/46] Match styles of Select for date picker Signed-off-by: Niklas Granander --- .../DatePickerComponent.tsx | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx index c6cc2eded5..854fbbd87f 100644 --- a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx +++ b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx @@ -18,6 +18,7 @@ import { createStyles, InputBase, InputProps, + makeStyles, Theme, Typography, withStyles, @@ -50,6 +51,13 @@ const BootstrapInput = withStyles((theme: Theme) => }), )(InputBase); +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + }, +}); + interface DatePickerProps { label: string; onDateChange?: (date: string) => void; @@ -59,16 +67,19 @@ export const DatePickerComponent = ({ label, onDateChange, ...inputProps -}: InputProps & DatePickerProps) => ( - <> - {label} -
- onDateChange?.(event.target.value)} - {...inputProps} - /> - -); +}: InputProps & DatePickerProps) => { + const classes = useStyles(); + + return ( +
+ {label} + onDateChange?.(event.target.value)} + {...inputProps} + /> +
+ ); +};