Merge branch 'master' into github-insights-after-downgrade-packages
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-explore': patch
|
||||
---
|
||||
|
||||
Add "Organization" tab with a diagram
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
'@backstage/create-app': patch
|
||||
'@backstage/plugin-catalog-graphql': patch
|
||||
---
|
||||
|
||||
chore: bump `ts-node` versions to 9.1.1
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
Renamed parameters to input in template schema
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Show error on task page if task does not exist.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
update plugins created to use react-use 17.2.4
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
'@backstage/core-api': patch
|
||||
'@backstage/integration-react': patch
|
||||
'@backstage/plugin-api-docs': patch
|
||||
'@backstage/plugin-badges': patch
|
||||
'@backstage/plugin-bitrise': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-circleci': patch
|
||||
'@backstage/plugin-cloudbuild': patch
|
||||
'@backstage/plugin-code-coverage': patch
|
||||
'@backstage/plugin-config-schema': patch
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
'@backstage/plugin-explore': patch
|
||||
'@backstage/plugin-fossa': patch
|
||||
'@backstage/plugin-gcp-projects': patch
|
||||
'@backstage/plugin-github-actions': patch
|
||||
'@backstage/plugin-github-deployments': patch
|
||||
'@backstage/plugin-gitops-profiles': patch
|
||||
'@backstage/plugin-graphiql': patch
|
||||
'@backstage/plugin-jenkins': patch
|
||||
'@backstage/plugin-kafka': patch
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
'@backstage/plugin-lighthouse': patch
|
||||
'@backstage/plugin-newrelic': patch
|
||||
'@backstage/plugin-org': patch
|
||||
'@backstage/plugin-pagerduty': patch
|
||||
'@backstage/plugin-register-component': patch
|
||||
'@backstage/plugin-rollbar': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-search': patch
|
||||
'@backstage/plugin-sentry': patch
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
'@backstage/plugin-splunk-on-call': patch
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-todo': patch
|
||||
'@backstage/plugin-user-settings': patch
|
||||
'@backstage/plugin-welcome': patch
|
||||
---
|
||||
|
||||
chore: bump `react-use` dependency in all packages
|
||||
@@ -167,6 +167,7 @@ mailto
|
||||
maintainership
|
||||
makefile
|
||||
md
|
||||
memcache
|
||||
microsite
|
||||
middleware
|
||||
minikube
|
||||
@@ -177,6 +178,7 @@ mkdocs
|
||||
monorepo
|
||||
monorepos
|
||||
msw
|
||||
mysql
|
||||
namespace
|
||||
namespaced
|
||||
namespaces
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ Which will look something like the following in the repo;
|
||||
```
|
||||
Awesome commit message
|
||||
|
||||
Signed-off-by: Jane Smith jane.smith@example.com
|
||||
Signed-off-by: Jane Smith <jane.smith@example.com>
|
||||
```
|
||||
|
||||
- In case you forgot to add it to the most recent commit, use `git commit --amend --signoff`
|
||||
|
||||
@@ -27,5 +27,5 @@ People that have made significant contributions to the project and earned write
|
||||
- Andrew Thauer - Wealthsimple (GitHub: [andrewthauer](https://github.com/andrewthauer))
|
||||
- Oliver Sand - SDA SE (GitHub: [Fox32](https://github.com/Fox32))
|
||||
- David Tuite - Roadie (GitHub: [dtuite](https://github.com/dtuite))
|
||||
- Adam Harvey - DXC Technology (GitHub: [adamdmharvey](https://github.com/adamdmharvey))
|
||||
- Adam Harvey - Cisco (GitHub: [adamdmharvey](https://github.com/adamdmharvey))
|
||||
- Dominik Henneke - SDA SE (GitHub: [dhenneke](https://github.com/dhenneke))
|
||||
|
||||
@@ -33,6 +33,7 @@ kubernetes:
|
||||
- type: 'gke'
|
||||
projectId: 'gke-clusters'
|
||||
region: 'europe-west1'
|
||||
skipTLSVerify: true
|
||||
```
|
||||
|
||||
### `serviceLocatorMethod`
|
||||
@@ -129,6 +130,11 @@ The Google Cloud project to look for Kubernetes clusters in.
|
||||
The Google Cloud region to look for Kubernetes clusters in. Defaults to all
|
||||
regions.
|
||||
|
||||
##### `skipTLSVerify`
|
||||
|
||||
This determines whether or not the Kubernetes client verifies the TLS
|
||||
certificate presented by the API server. Defaults to `false`.
|
||||
|
||||
### `customResources` (optional)
|
||||
|
||||
Configures which [custom resources][3] to look for when returning an entity's
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: GitHub Release Manager
|
||||
author: '@Spotify'
|
||||
authorUrl: https://github.com/spotify
|
||||
category: Release management
|
||||
description: Manage releases without having to juggle git commands
|
||||
documentation: https://github.com/backstage/backstage/tree/master/plugins/git-release-manager
|
||||
iconUrl: img/git-release-manager-logo.svg
|
||||
npmPackageName: '@backstage/plugin-git-release-manager'
|
||||
@@ -18,7 +18,7 @@
|
||||
"@spotify/prettier-config": "^10.0.0",
|
||||
"docusaurus": "^2.0.0-alpha.70",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^2.2.1"
|
||||
"prettier": "^2.3.0"
|
||||
},
|
||||
"prettier": "@spotify/prettier-config"
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ class Index extends React.Component {
|
||||
<Block.SmallTitle small>Pick a cloud, any cloud</Block.SmallTitle>
|
||||
<Block.Paragraph>
|
||||
Since Backstage uses the Kubernetes API, it's cloud agnostic —
|
||||
so it works no matter which cloud provide or managed Kubernetes
|
||||
so it works no matter which cloud provider or managed Kubernetes
|
||||
service you use, and even works in multi-cloud orgs
|
||||
</Block.Paragraph>
|
||||
</Block.TextBox>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="500px" height="500px" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Export</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="ship" transform="translate(249.680798, 211.000000) scale(-1, 1) translate(-249.680798, -211.000000) translate(35.680798, -39.000000)" fill-rule="nonzero">
|
||||
<g id="Flamez" transform="translate(279.925187, 455.280081) rotate(-90.000000) translate(-279.925187, -455.280081) translate(235.660848, 411.639183)">
|
||||
<path d="M77.6240648,9.79348504 C64.5660848,-3.26449501 42.8017456,-3.26449501 29.7437656,9.79348504 C11.6072319,27.9300187 0.725685786,80.8882481 8.85963511e-15,83.064682 C8.85963511e-15,84.5154302 8.85963511e-15,85.241116 0.725685786,85.9668017 C2.17643392,86.6924875 3.62780549,86.6924875 5.07855362,86.6924875 C7.25498753,85.9668017 60.213217,75.810318 78.3497506,56.9487219 C91.4077307,43.8901185 91.4077307,22.8514651 77.6240648,9.79348504 Z" id="Path" fill="#F79548"></path>
|
||||
<path d="M75.4476309,10.5185474 C68.9183292,3.98924564 58.036783,3.98924564 51.5074813,10.5185474 C42.0766833,19.9493454 36.9981297,46.0659289 36.9981297,46.7916147 C36.9981297,47.5173005 36.9981297,48.2423628 37.7238155,48.2423628 C38.4495012,48.9680486 38.4495012,48.9680486 39.1745636,48.9680486 C40.6253117,48.9680486 66.7418953,43.1644327 75.4476309,34.458697 C81.9769327,27.9300187 81.9769327,17.0478491 75.4476309,10.5185474 Z" id="Path" fill="#FCBB76"></path>
|
||||
</g>
|
||||
<path d="M216.186409,55.2394015 C218.49257,55.2394015 220.58047,56.1742348 222.091833,57.6855981 C223.534498,59.128263 224.451854,61.0962436 224.532269,63.2779325 L224.53803,63.5910224 L224.53803,91.005836 L353.369587,261.704121 C354.323776,262.968115 354.87911,264.447677 355.019478,265.965658 C355.159883,267.484039 354.88511,269.040862 354.179061,270.458806 C353.472845,271.876805 352.395871,273.033852 351.099552,273.836436 C349.895645,274.581806 348.502551,275.021453 347.041547,275.080457 L346.703242,275.087282 L224.53803,275.087282 L224.53803,303.904613 L346.684539,303.904613 C348.308638,303.904613 349.858968,304.375354 351.174692,305.21426 C352.490519,306.053232 353.571696,307.260417 354.25731,308.732969 C354.942424,310.205367 355.16974,311.809443 354.964363,313.356104 C354.77365,314.792333 354.20983,316.179047 353.29311,317.364769 L353.075304,317.634761 L309.451382,369.468132 C307.930114,371.274871 305.719044,372.349921 303.368443,372.436371 L303.061097,372.44202 L124.602244,372.44202 C122.240117,372.44202 119.994057,371.442358 118.41433,369.699238 L118.212103,369.467679 L74.5886595,317.634759 C73.5428392,316.392134 72.9047814,314.903039 72.6994228,313.356514 C72.494029,311.809724 72.7214803,310.205497 73.4066556,308.732969 C74.0917295,307.260659 75.1726391,306.053772 76.4882065,305.214939 C77.7099169,304.435952 79.1339993,303.974371 80.6313862,303.912406 L80.9781796,303.905237 L207.834788,303.905237 L207.834788,274.720698 L106.28616,274.720698 C103.979999,274.720698 101.892098,273.785865 100.380735,272.274502 C98.8693719,270.763138 97.9345387,268.675238 97.9345387,266.369077 C97.9345387,233.249109 111.496611,180.759139 130.891816,138.720038 C142.0216,114.595575 154.255888,95.1994886 167.266567,81.0803824 C183.02715,63.976526 199.471354,55.2394015 216.186409,55.2394015 Z M328.739655,320.607855 L98.9224399,320.607855 L128.488453,355.738155 L299.173642,355.738155 L328.739655,320.607855 Z M224.53803,118.732589 L224.53803,258.384663 L329.936488,258.384663 L224.53803,118.732589 Z M207.834165,73.0839035 C187.31365,78.5957312 165.169959,104.292863 146.058313,145.71761 C129.135086,182.399574 116.887708,227.020442 114.917014,258.016833 L114.917014,258.016833 L207.834165,258.016833 Z" id="Shape" stroke="#DDDDDD" stroke-width="2" fill="#E8E8E8" transform="translate(213.840829, 213.840711) rotate(45.000000) translate(-213.840829, -213.840711) "></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
+4
-4
@@ -5197,10 +5197,10 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
prettier@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
|
||||
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
|
||||
prettier@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
|
||||
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
|
||||
|
||||
prismjs@^1.22.0:
|
||||
version "1.23.0"
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
# example-app
|
||||
|
||||
## 0.2.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [2cd70e164]
|
||||
- Updated dependencies [0b033d07b]
|
||||
- Updated dependencies [3be844496]
|
||||
- Updated dependencies [5542de095]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [81ef1d57b]
|
||||
- Updated dependencies [ea21d46f0]
|
||||
- Updated dependencies [e3fc89df6]
|
||||
- Updated dependencies [f59a945b7]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
- @backstage/integration-react@0.1.2
|
||||
- @backstage/plugin-api-docs@0.4.13
|
||||
- @backstage/plugin-badges@0.2.1
|
||||
- @backstage/plugin-catalog@0.5.7
|
||||
- @backstage/plugin-catalog-import@0.5.6
|
||||
- @backstage/plugin-catalog-react@0.1.6
|
||||
- @backstage/plugin-circleci@0.2.14
|
||||
- @backstage/plugin-cloudbuild@0.2.14
|
||||
- @backstage/plugin-code-coverage@0.1.3
|
||||
- @backstage/plugin-cost-insights@0.9.1
|
||||
- @backstage/plugin-explore@0.3.5
|
||||
- @backstage/plugin-gcp-projects@0.2.6
|
||||
- @backstage/plugin-github-actions@0.4.6
|
||||
- @backstage/plugin-graphiql@0.2.11
|
||||
- @backstage/plugin-jenkins@0.4.3
|
||||
- @backstage/plugin-kafka@0.2.7
|
||||
- @backstage/plugin-kubernetes@0.4.4
|
||||
- @backstage/plugin-lighthouse@0.2.16
|
||||
- @backstage/plugin-newrelic@0.2.7
|
||||
- @backstage/plugin-org@0.3.13
|
||||
- @backstage/plugin-pagerduty@0.3.4
|
||||
- @backstage/plugin-rollbar@0.3.5
|
||||
- @backstage/plugin-scaffolder@0.9.4
|
||||
- @backstage/plugin-search@0.3.6
|
||||
- @backstage/plugin-sentry@0.3.10
|
||||
- @backstage/plugin-shortcuts@0.1.2
|
||||
- @backstage/plugin-tech-radar@0.3.11
|
||||
- @backstage/plugin-techdocs@0.9.2
|
||||
- @backstage/plugin-todo@0.1.1
|
||||
- @backstage/plugin-user-settings@0.2.10
|
||||
- @backstage/cli@0.6.11
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.2.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+36
-35
@@ -1,41 +1,42 @@
|
||||
{
|
||||
"name": "example-app",
|
||||
"version": "0.2.27",
|
||||
"version": "0.2.28",
|
||||
"private": true,
|
||||
"bundled": true,
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.8",
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/core": "^0.7.8",
|
||||
"@backstage/integration-react": "^0.1.1",
|
||||
"@backstage/plugin-api-docs": "^0.4.12",
|
||||
"@backstage/plugin-badges": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.5.6",
|
||||
"@backstage/plugin-catalog-import": "^0.5.5",
|
||||
"@backstage/plugin-catalog-react": "^0.1.5",
|
||||
"@backstage/plugin-circleci": "^0.2.13",
|
||||
"@backstage/plugin-cloudbuild": "^0.2.13",
|
||||
"@backstage/plugin-code-coverage": "^0.1.2",
|
||||
"@backstage/plugin-cost-insights": "^0.9.0",
|
||||
"@backstage/plugin-explore": "^0.3.4",
|
||||
"@backstage/plugin-gcp-projects": "^0.2.5",
|
||||
"@backstage/plugin-github-actions": "^0.4.5",
|
||||
"@backstage/plugin-graphiql": "^0.2.10",
|
||||
"@backstage/plugin-jenkins": "^0.4.2",
|
||||
"@backstage/plugin-kafka": "^0.2.6",
|
||||
"@backstage/plugin-kubernetes": "^0.4.3",
|
||||
"@backstage/plugin-lighthouse": "^0.2.15",
|
||||
"@backstage/plugin-newrelic": "^0.2.6",
|
||||
"@backstage/plugin-org": "^0.3.12",
|
||||
"@backstage/plugin-pagerduty": "0.3.3",
|
||||
"@backstage/plugin-rollbar": "^0.3.4",
|
||||
"@backstage/plugin-scaffolder": "^0.9.3",
|
||||
"@backstage/plugin-search": "^0.3.5",
|
||||
"@backstage/plugin-sentry": "^0.3.9",
|
||||
"@backstage/plugin-tech-radar": "^0.3.10",
|
||||
"@backstage/plugin-techdocs": "^0.9.1",
|
||||
"@backstage/plugin-todo": "^0.1.0",
|
||||
"@backstage/plugin-user-settings": "^0.2.8",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/integration-react": "^0.1.2",
|
||||
"@backstage/plugin-api-docs": "^0.4.13",
|
||||
"@backstage/plugin-badges": "^0.2.1",
|
||||
"@backstage/plugin-catalog": "^0.5.7",
|
||||
"@backstage/plugin-catalog-import": "^0.5.6",
|
||||
"@backstage/plugin-catalog-react": "^0.1.6",
|
||||
"@backstage/plugin-circleci": "^0.2.14",
|
||||
"@backstage/plugin-cloudbuild": "^0.2.14",
|
||||
"@backstage/plugin-code-coverage": "^0.1.3",
|
||||
"@backstage/plugin-cost-insights": "^0.9.1",
|
||||
"@backstage/plugin-explore": "^0.3.5",
|
||||
"@backstage/plugin-gcp-projects": "^0.2.6",
|
||||
"@backstage/plugin-github-actions": "^0.4.6",
|
||||
"@backstage/plugin-graphiql": "^0.2.11",
|
||||
"@backstage/plugin-jenkins": "^0.4.3",
|
||||
"@backstage/plugin-kafka": "^0.2.7",
|
||||
"@backstage/plugin-kubernetes": "^0.4.4",
|
||||
"@backstage/plugin-lighthouse": "^0.2.16",
|
||||
"@backstage/plugin-newrelic": "^0.2.7",
|
||||
"@backstage/plugin-org": "^0.3.13",
|
||||
"@backstage/plugin-pagerduty": "0.3.4",
|
||||
"@backstage/plugin-rollbar": "^0.3.5",
|
||||
"@backstage/plugin-scaffolder": "^0.9.4",
|
||||
"@backstage/plugin-search": "^0.3.6",
|
||||
"@backstage/plugin-sentry": "^0.3.10",
|
||||
"@backstage/plugin-shortcuts": "^0.1.2",
|
||||
"@backstage/plugin-tech-radar": "^0.3.11",
|
||||
"@backstage/plugin-techdocs": "^0.9.2",
|
||||
"@backstage/plugin-todo": "^0.1.1",
|
||||
"@backstage/plugin-user-settings": "^0.2.10",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -55,11 +56,11 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/cypress": "^7.0.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/node": "^14.14.32",
|
||||
|
||||
@@ -39,6 +39,7 @@ import { NavLink } from 'react-router-dom';
|
||||
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
|
||||
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
import { SidebarSearch } from '@backstage/plugin-search';
|
||||
import { Shortcuts } from '@backstage/plugin-shortcuts';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
@@ -91,6 +92,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
|
||||
<SidebarItem icon={MoneyIcon} to="cost-insights" text="Cost Insights" />
|
||||
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarSettings />
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
// TODO(Rugvip): This plugin is currently not part of the app element tree,
|
||||
// ideally we have an API for the context menu that permits that.
|
||||
export { badgesPlugin } from '@backstage/plugin-badges';
|
||||
export { shortcutsPlugin } from '@backstage/plugin-shortcuts';
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
# @backstage/backend-common
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 22fd8ce2a: Introducing: a standard API for App Integrators to configure cache stores and Plugin Developers to interact with them.
|
||||
|
||||
Two cache stores are currently supported.
|
||||
|
||||
- `memory`, which is a very simple in-memory key/value store, intended for local development.
|
||||
- `memcache`, which can be used to connect to a memcache host.
|
||||
|
||||
Configuring and working with cache stores is very similar to the process for database connections.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cache:
|
||||
store: memcache
|
||||
connection: user:pass@cache.example.com:11211
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { CacheManager } from '@backstage/backend-common';
|
||||
|
||||
// Instantiating a cache client for a plugin.
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
const somePluginCache = cacheManager.forPlugin('somePlugin');
|
||||
const cacheClient = somePluginCache.getClient();
|
||||
|
||||
// Using the cache client:
|
||||
const cachedValue = await cacheClient.get('someKey');
|
||||
if (cachedValue) {
|
||||
return cachedValue;
|
||||
} else {
|
||||
const someValue = await someExpensiveProcess();
|
||||
await cacheClient.set('someKey', someValue);
|
||||
}
|
||||
await cacheClient.delete('someKey');
|
||||
```
|
||||
|
||||
Cache clients deal with TTLs in milliseconds. A TTL can be provided as a defaultTtl when getting a client, or may be passed when setting specific objects. If no TTL is provided, data will be persisted indefinitely.
|
||||
|
||||
```typescript
|
||||
// Getting a client with a default TTL
|
||||
const cacheClient = somePluginCache.getClient({
|
||||
defaultTtl: 3600000,
|
||||
});
|
||||
|
||||
// Setting a TTL on a per-object basis.
|
||||
cacheClient.set('someKey', data, { ttl: 3600000 });
|
||||
```
|
||||
|
||||
Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f9fb4a205: Prep work for mysql support in backend-common
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -16,6 +16,7 @@ import { GithubCredentialsProvider } from '@backstage/integration';
|
||||
import { GitHubIntegration } from '@backstage/integration';
|
||||
import { GitLabIntegration } from '@backstage/integration';
|
||||
import * as http from 'http';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { Knex } from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { MergeResult } from 'isomorphic-git';
|
||||
@@ -62,6 +63,19 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface CacheClient {
|
||||
delete(key: string): Promise<void>;
|
||||
get(key: string): Promise<JsonValue | undefined>;
|
||||
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class CacheManager {
|
||||
forPlugin(pluginId: string): PluginCacheManager;
|
||||
static fromConfig(config: Config): CacheManager;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const coloredFormat: winston.Logform.Format;
|
||||
|
||||
@@ -238,6 +252,11 @@ export function loadBackendConfig(options: Options): Promise<Config>;
|
||||
// @public
|
||||
export function notFoundHandler(): RequestHandler;
|
||||
|
||||
// @public
|
||||
export type PluginCacheManager = {
|
||||
getClient: (options?: ClientOptions) => CacheClient;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface PluginDatabaseManager {
|
||||
getClient(): Promise<Knex>;
|
||||
|
||||
Vendored
+14
@@ -68,6 +68,20 @@ export interface Config {
|
||||
connection: string | object;
|
||||
};
|
||||
|
||||
/** Cache connection configuration, select cache type using the `store` field */
|
||||
cache?:
|
||||
| {
|
||||
store: 'memory';
|
||||
}
|
||||
| {
|
||||
store: 'memcache';
|
||||
/**
|
||||
* A memcache connection string in the form `user:pass@host:port`.
|
||||
* @secret
|
||||
*/
|
||||
connection: string;
|
||||
};
|
||||
|
||||
cors?: {
|
||||
origin?: string | string[];
|
||||
methods?: string | string[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/backend-common",
|
||||
"description": "Common functionality library for Backstage backends",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
@@ -51,6 +51,8 @@
|
||||
"git-url-parse": "^11.4.4",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
"keyv": "^4.0.3",
|
||||
"keyv-memcache": "^1.2.5",
|
||||
"knex": "^0.95.1",
|
||||
"lodash": "^4.17.15",
|
||||
"logform": "^2.1.1",
|
||||
@@ -62,7 +64,8 @@
|
||||
"stoppable": "^1.1.0",
|
||||
"tar": "^6.0.5",
|
||||
"unzipper": "^0.10.11",
|
||||
"winston": "^3.2.1"
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-connection-string": "^2.3.0"
|
||||
@@ -73,8 +76,8 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@types/archiver": "^5.1.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/concat-stream": "^1.6.0",
|
||||
@@ -94,6 +97,7 @@
|
||||
"jest": "^26.0.1",
|
||||
"mock-fs": "^4.13.0",
|
||||
"msw": "^0.21.2",
|
||||
"mysql2": "^2.2.5",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 { DefaultCacheClient } from './CacheClient';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
describe('CacheClient', () => {
|
||||
let client: Keyv;
|
||||
const b64 = (k: string) => Buffer.from(k).toString('base64');
|
||||
|
||||
beforeEach(() => {
|
||||
client = new Keyv();
|
||||
client.get = jest.fn();
|
||||
client.set = jest.fn();
|
||||
client.delete = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('CacheClient.get', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.get(key);
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(b64(key));
|
||||
});
|
||||
|
||||
it('calls client with normalized key (very long key)', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'x'.repeat(251);
|
||||
|
||||
await sut.get(key);
|
||||
|
||||
const spy = client.get as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).not.toEqual(b64(key));
|
||||
expect(actualKey.length).toBeLessThan(250);
|
||||
});
|
||||
|
||||
it('rejects on underlying error', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.get = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.get('someKey')).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it('resolves what underlying client resolves', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedValue = 'some value';
|
||||
client.get = jest.fn().mockResolvedValue(expectedValue);
|
||||
|
||||
const actualValue = await sut.get('someKey');
|
||||
|
||||
return expect(actualValue).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheClient.set', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.set(key, {});
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).toEqual(b64(key));
|
||||
});
|
||||
|
||||
it('passes ttl to client when given', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedTtl = 3600;
|
||||
|
||||
await sut.set('someKey', {}, { ttl: expectedTtl });
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualTtl = spy.mock.calls[0][2];
|
||||
expect(actualTtl).toEqual(expectedTtl);
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.set = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.set('someKey', {})).rejects.toEqual(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheClient.delete', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.delete(key);
|
||||
|
||||
const spy = client.delete as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).toEqual(b64(key));
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.delete = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.delete('someKey')).rejects.toEqual(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 { JsonValue } from '@backstage/config';
|
||||
import { createHash } from 'crypto';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
type CacheClientArgs = {
|
||||
client: Keyv;
|
||||
};
|
||||
|
||||
type CacheSetOptions = {
|
||||
/**
|
||||
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
|
||||
* was set up (or no TTL if none are provided).
|
||||
*/
|
||||
ttl?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A pre-configured, storage agnostic cache client suitable for use by
|
||||
* Backstage plugins.
|
||||
*/
|
||||
export interface CacheClient {
|
||||
/**
|
||||
* Reads data from a cache store for the given key. If no data was found,
|
||||
* returns undefined.
|
||||
*/
|
||||
get(key: string): Promise<JsonValue | undefined>;
|
||||
|
||||
/**
|
||||
* Writes the given data to a cache store, associated with the given key. An
|
||||
* optional TTL may also be provided, otherwise it defaults to the TTL that
|
||||
* was provided when the client was instantiated.
|
||||
*/
|
||||
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes the given key from the cache store.
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic, concrete implementation of the CacheClient, suitable for almost
|
||||
* all uses in Backstage.
|
||||
*/
|
||||
export class DefaultCacheClient implements CacheClient {
|
||||
private readonly client: Keyv;
|
||||
|
||||
constructor({ client }: CacheClientArgs) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<JsonValue | undefined> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
return await this.client.get(k);
|
||||
}
|
||||
|
||||
async set(
|
||||
key: string,
|
||||
value: JsonValue,
|
||||
opts: CacheSetOptions = {},
|
||||
): Promise<void> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
await this.client.set(k, value, opts.ttl);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
await this.client.delete(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures keys are well-formed for any/all cache stores.
|
||||
*/
|
||||
private getNormalizedKey(candidateKey: string): string {
|
||||
// Remove potentially invalid characters.
|
||||
const wellFormedKey = Buffer.from(candidateKey).toString('base64');
|
||||
|
||||
// Memcache in particular doesn't do well with keys > 250 bytes.
|
||||
if (wellFormedKey.length < 250) {
|
||||
return wellFormedKey;
|
||||
}
|
||||
|
||||
return createHash('md5').update(candidateKey).digest('base64');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 { ConfigReader } from '@backstage/config';
|
||||
import Keyv from 'keyv';
|
||||
/* @ts-expect-error */
|
||||
import KeyvMemcache from 'keyv-memcache';
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import { CacheManager } from './CacheManager';
|
||||
import { NoStore } from './NoStore';
|
||||
|
||||
jest.createMockFromModule('keyv');
|
||||
jest.mock('keyv');
|
||||
jest.createMockFromModule('keyv-memcache');
|
||||
jest.mock('keyv-memcache');
|
||||
jest.mock('./CacheClient', () => {
|
||||
return {
|
||||
DefaultCacheClient: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('CacheManager', () => {
|
||||
const defaultConfigOptions = {
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'memory',
|
||||
},
|
||||
},
|
||||
};
|
||||
const defaultConfig = () => new ConfigReader(defaultConfigOptions);
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('CacheManager.fromConfig', () => {
|
||||
it('accesses the backend.cache key', () => {
|
||||
const getOptionalString = jest.fn();
|
||||
const config = defaultConfig();
|
||||
config.getOptionalString = getOptionalString;
|
||||
|
||||
CacheManager.fromConfig(config);
|
||||
|
||||
expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store');
|
||||
expect(getOptionalString.mock.calls[1][0]).toEqual(
|
||||
'backend.cache.connection',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not require the backend.cache key', () => {
|
||||
const config = new ConfigReader({ backend: {} });
|
||||
expect(() => {
|
||||
CacheManager.fromConfig(config);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
it('throws on unknown cache store', () => {
|
||||
const config = new ConfigReader({
|
||||
backend: { cache: { store: 'notreal' } },
|
||||
});
|
||||
expect(() => {
|
||||
CacheManager.fromConfig(config);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheManager.forPlugin', () => {
|
||||
const manager = CacheManager.fromConfig(defaultConfig());
|
||||
|
||||
it('connects to a cache store scoped to the plugin', async () => {
|
||||
const pluginId = 'test1';
|
||||
manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const client = DefaultCacheClient as jest.Mock;
|
||||
expect(client).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('provides different plugins different cache clients', async () => {
|
||||
const plugin1Id = 'test1';
|
||||
const plugin2Id = 'test2';
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl });
|
||||
manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const client = DefaultCacheClient as jest.Mock;
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
expect(cache).toHaveBeenCalledTimes(2);
|
||||
expect(client).toHaveBeenCalledTimes(2);
|
||||
|
||||
const plugin1CallArgs = cache.mock.calls[0];
|
||||
const plugin2CallArgs = cache.mock.calls[1];
|
||||
expect(plugin1CallArgs[0].namespace).not.toEqual(
|
||||
plugin2CallArgs[0].namespace,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheManager.forPlugin stores', () => {
|
||||
it('returns none client when no cache is configured', () => {
|
||||
const manager = CacheManager.fromConfig(
|
||||
new ConfigReader({ backend: {} }),
|
||||
);
|
||||
const expectedNamespace = 'test-plugin';
|
||||
manager.forPlugin(expectedNamespace).getClient();
|
||||
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
const mockCalls = cache.mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0].store).toBeInstanceOf(NoStore);
|
||||
});
|
||||
|
||||
it('returns memory client when explicitly configured', () => {
|
||||
const manager = CacheManager.fromConfig(defaultConfig());
|
||||
const expectedTtl = 3600;
|
||||
const expectedNamespace = 'test-plugin';
|
||||
manager
|
||||
.forPlugin(expectedNamespace)
|
||||
.getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
const mockCalls = cache.mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0]).toMatchObject({
|
||||
ttl: expectedTtl,
|
||||
namespace: expectedNamespace,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a memcache client when configured', () => {
|
||||
const expectedHost = '127.0.0.1:11211';
|
||||
const manager = CacheManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'memcache',
|
||||
connection: expectedHost,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = (Keyv as unknown) as jest.Mock;
|
||||
const mockCacheCalls = cache.mock.calls.splice(-1);
|
||||
expect(mockCacheCalls[0][0]).toMatchObject({
|
||||
ttl: expectedTtl,
|
||||
});
|
||||
expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache);
|
||||
const memcache = KeyvMemcache as jest.Mock;
|
||||
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
|
||||
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
|
||||
});
|
||||
});
|
||||
});
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 { Config } from '@backstage/config';
|
||||
import Keyv from 'keyv';
|
||||
// @ts-expect-error
|
||||
import KeyvMemcache from 'keyv-memcache';
|
||||
import { DefaultCacheClient, CacheClient } from './CacheClient';
|
||||
import { NoStore } from './NoStore';
|
||||
import { PluginCacheManager } from './types';
|
||||
|
||||
/**
|
||||
* Implements a Cache Manager which will automatically create new cache clients
|
||||
* for plugins when requested. All requested cache clients are created with the
|
||||
* connection details provided.
|
||||
*/
|
||||
export class CacheManager {
|
||||
/**
|
||||
* Keys represent supported `backend.cache.store` values, mapped to factories
|
||||
* that return Keyv instances appropriate to the store.
|
||||
*/
|
||||
private readonly storeFactories = {
|
||||
memcache: this.getMemcacheClient,
|
||||
memory: this.getMemoryClient,
|
||||
none: this.getNoneClient,
|
||||
};
|
||||
|
||||
private readonly store: keyof CacheManager['storeFactories'];
|
||||
private readonly connection: string;
|
||||
|
||||
/**
|
||||
* Creates a new CacheManager instance by reading from the `backend` config
|
||||
* section, specifically the `.cache` key.
|
||||
*
|
||||
* @param config The loaded application configuration.
|
||||
*/
|
||||
static fromConfig(config: Config): CacheManager {
|
||||
// If no `backend.cache` config is provided, instantiate the CacheManager
|
||||
// with a "NoStore" cache client.
|
||||
const store = config.getOptionalString('backend.cache.store') || 'none';
|
||||
const connectionString =
|
||||
config.getOptionalString('backend.cache.connection') || '';
|
||||
return new CacheManager(store, connectionString);
|
||||
}
|
||||
|
||||
private constructor(store: string, connectionString: string) {
|
||||
if (!this.storeFactories.hasOwnProperty(store)) {
|
||||
throw new Error(`Unknown cache store: ${store}`);
|
||||
}
|
||||
this.store = store as keyof CacheManager['storeFactories'];
|
||||
this.connection = connectionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CacheManagerInstance for consumption by plugins.
|
||||
*
|
||||
* @param pluginId The plugin that the cache manager should be created for. Plugin names should be unique.
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginCacheManager {
|
||||
return {
|
||||
getClient: (opts = {}): CacheClient => {
|
||||
const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl);
|
||||
return new DefaultCacheClient({
|
||||
client: concreteClient,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv {
|
||||
return this.storeFactories[this.store].call(this, pluginId, ttl);
|
||||
}
|
||||
|
||||
private getMemcacheClient(
|
||||
pluginId: string,
|
||||
defaultTtl: number | undefined,
|
||||
): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
ttl: defaultTtl,
|
||||
store: new KeyvMemcache(this.connection),
|
||||
});
|
||||
}
|
||||
|
||||
private getMemoryClient(
|
||||
pluginId: string,
|
||||
defaultTtl: number | undefined,
|
||||
): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
ttl: defaultTtl,
|
||||
});
|
||||
}
|
||||
|
||||
private getNoneClient(pluginId: string): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
store: new NoStore(),
|
||||
});
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Storage class compatible with Keyv which always results in a no-op. This is
|
||||
* used when no cache store is configured in a Backstage backend instance.
|
||||
*/
|
||||
export class NoStore extends Map<string, any> {
|
||||
clear(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
delete(_key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
get(_key: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
has(_key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
set(_key: string, _value: any): this {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { CacheClient } from './CacheClient';
|
||||
export { CacheManager } from './CacheManager';
|
||||
export type { PluginCacheManager } from './types';
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CacheClient } from './CacheClient';
|
||||
|
||||
type ClientOptions = {
|
||||
/**
|
||||
* An optional default TTL (in milliseconds) to be set when getting a client
|
||||
* instance. If not provided, data will persist indefinitely by default (or
|
||||
* can be configured per entry at set-time).
|
||||
*/
|
||||
defaultTtl?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The PluginCacheManager manages access to cache stores that Plugins get.
|
||||
*/
|
||||
export type PluginCacheManager = {
|
||||
/**
|
||||
* getClient provides backend plugins cache connections for itself.
|
||||
*
|
||||
* The purpose of this method is to allow plugins to get isolated data
|
||||
* stores so that plugins are discouraged from cache-level integration
|
||||
* and/or cache key collisions.
|
||||
*/
|
||||
getClient: (options?: ClientOptions) => CacheClient;
|
||||
};
|
||||
@@ -46,11 +46,11 @@ describe('database connection', () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('tries to create a mysql connection as a passthrough', () => {
|
||||
it('returns a mysql connection', () => {
|
||||
expect(() =>
|
||||
createDatabaseClient(
|
||||
new ConfigReader({
|
||||
client: 'mysql',
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
host: '127.0.0.1',
|
||||
user: 'foo',
|
||||
@@ -59,7 +59,7 @@ describe('database connection', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrowError(/Cannot find module 'mysql'/);
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('accepts overrides', () => {
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { Config } from '@backstage/config';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { mergeDatabaseConfig } from './config';
|
||||
import { createMysqlDatabaseClient, ensureMysqlDatabaseExists } from './mysql';
|
||||
import { createPgDatabaseClient, ensurePgDatabaseExists } from './postgres';
|
||||
import { createSqliteDatabaseClient } from './sqlite3';
|
||||
|
||||
@@ -36,6 +37,8 @@ export function createDatabaseClient(
|
||||
|
||||
if (client === 'pg') {
|
||||
return createPgDatabaseClient(dbConfig, overrides);
|
||||
} else if (client === 'mysql' || client === 'mysql2') {
|
||||
return createMysqlDatabaseClient(dbConfig, overrides);
|
||||
} else if (client === 'sqlite3') {
|
||||
return createSqliteDatabaseClient(dbConfig, overrides);
|
||||
}
|
||||
@@ -60,6 +63,8 @@ export async function ensureDatabaseExists(
|
||||
|
||||
if (client === 'pg') {
|
||||
return ensurePgDatabaseExists(dbConfig, ...databases);
|
||||
} else if (client === 'mysql' || client === 'mysql2') {
|
||||
return ensureMysqlDatabaseExists(dbConfig, ...databases);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
buildMysqlDatabaseConfig,
|
||||
createMysqlDatabaseClient,
|
||||
getMysqlConnectionConfig,
|
||||
parseMysqlConnectionString,
|
||||
} from './mysql';
|
||||
|
||||
describe('mysql', () => {
|
||||
const createMockConnection = () => ({
|
||||
host: 'acme',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
database: 'foodb',
|
||||
});
|
||||
|
||||
const createMockConnectionString = () => 'mysql://foo:bar@acme:3306/foodb';
|
||||
|
||||
const createConfig = (connection: any): Config =>
|
||||
new ConfigReader({ client: 'mysql2', connection });
|
||||
|
||||
describe('buildMysqlDatabaseConfig', () => {
|
||||
it('builds a mysql config', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
|
||||
expect(buildMysqlDatabaseConfig(createConfig(mockConnection))).toEqual({
|
||||
client: 'mysql2',
|
||||
connection: mockConnection,
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('builds a connection string config', () => {
|
||||
const mockConnectionString = createMockConnectionString();
|
||||
|
||||
expect(
|
||||
buildMysqlDatabaseConfig(createConfig(mockConnectionString)),
|
||||
).toEqual({
|
||||
client: 'mysql2',
|
||||
connection: mockConnectionString,
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides the database name', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
|
||||
expect(
|
||||
buildMysqlDatabaseConfig(createConfig(mockConnection), {
|
||||
connection: { database: 'other_db' },
|
||||
}),
|
||||
).toEqual({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
...mockConnection,
|
||||
database: 'other_db',
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('adds additional config settings', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
|
||||
expect(
|
||||
buildMysqlDatabaseConfig(createConfig(mockConnection), {
|
||||
connection: { database: 'other_db' },
|
||||
pool: { min: 0, max: 7 },
|
||||
debug: true,
|
||||
}),
|
||||
).toEqual({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
...mockConnection,
|
||||
database: 'other_db',
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
pool: { min: 0, max: 7 },
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides the database from connection string', () => {
|
||||
const mockConnectionString = createMockConnectionString();
|
||||
const mockConnection = createMockConnection();
|
||||
|
||||
expect(
|
||||
buildMysqlDatabaseConfig(createConfig(mockConnectionString), {
|
||||
connection: { database: 'other_db' },
|
||||
}),
|
||||
).toEqual({
|
||||
client: 'mysql2',
|
||||
connection: {
|
||||
...mockConnection,
|
||||
port: 3306,
|
||||
database: 'other_db',
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMysqlConnectionConfig', () => {
|
||||
it('returns the connection object back', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
const config = createConfig(mockConnection);
|
||||
|
||||
expect(getMysqlConnectionConfig(config)).toEqual(mockConnection);
|
||||
});
|
||||
|
||||
it('does not parse the connection string', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
const config = createConfig(mockConnection);
|
||||
|
||||
expect(getMysqlConnectionConfig(config, true)).toEqual(mockConnection);
|
||||
});
|
||||
|
||||
it('automatically parses the connection string', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
const mockConnectionString = createMockConnectionString();
|
||||
const config = createConfig(mockConnectionString);
|
||||
|
||||
expect(getMysqlConnectionConfig(config)).toEqual({
|
||||
...mockConnection,
|
||||
port: 3306,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses the connection string', () => {
|
||||
const mockConnection = createMockConnection();
|
||||
const mockConnectionString = createMockConnectionString();
|
||||
const config = createConfig(mockConnectionString);
|
||||
|
||||
expect(getMysqlConnectionConfig(config, true)).toEqual({
|
||||
...mockConnection,
|
||||
port: 3306,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMysqlDatabaseClient', () => {
|
||||
it('creates a mysql knex instance', () => {
|
||||
expect(
|
||||
createMysqlDatabaseClient(
|
||||
createConfig({
|
||||
host: 'acme',
|
||||
user: 'foo',
|
||||
password: 'bar',
|
||||
database: 'foodb',
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMysqlConnectionString', () => {
|
||||
it('parses a connection string uri', () => {
|
||||
expect(
|
||||
parseMysqlConnectionString(
|
||||
'mysql://u:pass@foobar:3307/dbname?ssl=require',
|
||||
),
|
||||
).toEqual({
|
||||
host: 'foobar',
|
||||
user: 'u',
|
||||
password: 'pass',
|
||||
port: 3307,
|
||||
database: 'dbname',
|
||||
ssl: 'require',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import knexFactory, { Knex } from 'knex';
|
||||
import { mergeDatabaseConfig } from './config';
|
||||
import yn from 'yn';
|
||||
|
||||
/**
|
||||
* Creates a knex mysql database connection
|
||||
*
|
||||
* @param dbConfig The database config
|
||||
* @param overrides Additional options to merge with the config
|
||||
*/
|
||||
export function createMysqlDatabaseClient(
|
||||
dbConfig: Config,
|
||||
overrides?: Knex.Config,
|
||||
) {
|
||||
const knexConfig = buildMysqlDatabaseConfig(dbConfig, overrides);
|
||||
const database = knexFactory(knexConfig);
|
||||
return database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a knex mysql database connection
|
||||
*
|
||||
* @param dbConfig The database config
|
||||
* @param overrides Additional options to merge with the config
|
||||
*/
|
||||
export function buildMysqlDatabaseConfig(
|
||||
dbConfig: Config,
|
||||
overrides?: Knex.Config,
|
||||
) {
|
||||
return mergeDatabaseConfig(
|
||||
dbConfig.get(),
|
||||
{
|
||||
connection: getMysqlConnectionConfig(dbConfig, !!overrides),
|
||||
useNullAsDefault: true,
|
||||
},
|
||||
overrides,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mysql connection config
|
||||
*
|
||||
* @param dbConfig The database config
|
||||
* @param parseConnectionString Flag to explicitly control connection string parsing
|
||||
*/
|
||||
export function getMysqlConnectionConfig(
|
||||
dbConfig: Config,
|
||||
parseConnectionString?: boolean,
|
||||
): Knex.MySqlConnectionConfig | string {
|
||||
const connection = dbConfig.get('connection') as any;
|
||||
const isConnectionString =
|
||||
typeof connection === 'string' || connection instanceof String;
|
||||
const autoParse = typeof parseConnectionString !== 'boolean';
|
||||
|
||||
const shouldParseConnectionString = autoParse
|
||||
? isConnectionString
|
||||
: parseConnectionString && isConnectionString;
|
||||
|
||||
return shouldParseConnectionString
|
||||
? parseMysqlConnectionString(connection as string)
|
||||
: connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a mysql connection string.
|
||||
*
|
||||
* e.g. mysql://examplename:somepassword@examplehost:3306/dbname
|
||||
* @param connectionString The mysql connection string
|
||||
*/
|
||||
export function parseMysqlConnectionString(
|
||||
connectionString: string,
|
||||
): Knex.MySqlConnectionConfig {
|
||||
try {
|
||||
const {
|
||||
protocol,
|
||||
username,
|
||||
password,
|
||||
port,
|
||||
hostname,
|
||||
pathname,
|
||||
searchParams,
|
||||
} = new URL(connectionString);
|
||||
|
||||
if (protocol !== 'mysql:') {
|
||||
throw new Error(`Unknown protocol ${protocol}`);
|
||||
} else if (!username || !password) {
|
||||
throw new Error(`Missing username/password`);
|
||||
} else if (!pathname.match(/^\/[^/]+$/)) {
|
||||
throw new Error(`Expected single path segment`);
|
||||
}
|
||||
|
||||
const result: Knex.MySqlConnectionConfig = {
|
||||
user: username,
|
||||
password,
|
||||
host: hostname,
|
||||
port: Number(port || 3306),
|
||||
database: decodeURIComponent(pathname.substr(1)),
|
||||
};
|
||||
|
||||
const ssl = searchParams.get('ssl');
|
||||
if (ssl) {
|
||||
result.ssl = ssl;
|
||||
}
|
||||
|
||||
const debug = searchParams.get('debug');
|
||||
if (debug) {
|
||||
result.debug = yn(debug);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new InputError(
|
||||
`Error while parsing MySQL connection string, ${e}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the missing mysql database if it does not exist
|
||||
*
|
||||
* @param dbConfig The database config
|
||||
* @param databases The names of the databases to create
|
||||
*/
|
||||
export async function ensureMysqlDatabaseExists(
|
||||
dbConfig: Config,
|
||||
...databases: Array<string>
|
||||
) {
|
||||
const admin = createMysqlDatabaseClient(dbConfig, {
|
||||
connection: {
|
||||
database: (null as unknown) as string,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const ensureDatabase = async (database: string) => {
|
||||
await admin.raw(`CREATE DATABASE IF NOT EXISTS ??`, [database]);
|
||||
};
|
||||
await Promise.all(databases.map(ensureDatabase));
|
||||
} finally {
|
||||
await admin.destroy();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './cache';
|
||||
export * from './config';
|
||||
export * from './database';
|
||||
export * from './discovery';
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# example-backend
|
||||
|
||||
## 0.2.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [82ca1ac22]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- Updated dependencies [9a207f052]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [fd39d4662]
|
||||
- Updated dependencies [f9f9d633d]
|
||||
- @backstage/plugin-scaffolder-backend@0.11.1
|
||||
- @backstage/backend-common@0.8.0
|
||||
- @backstage/catalog-model@0.7.9
|
||||
- @backstage/plugin-catalog-backend@0.9.0
|
||||
- @backstage/plugin-kubernetes-backend@0.3.7
|
||||
- example-app@0.2.28
|
||||
- @backstage/plugin-app-backend@0.3.13
|
||||
- @backstage/plugin-auth-backend@0.3.10
|
||||
- @backstage/plugin-badges-backend@0.1.4
|
||||
- @backstage/plugin-code-coverage-backend@0.1.5
|
||||
- @backstage/plugin-graphql-backend@0.1.8
|
||||
- @backstage/plugin-kafka-backend@0.2.5
|
||||
- @backstage/plugin-proxy-backend@0.2.8
|
||||
- @backstage/plugin-rollbar-backend@0.1.11
|
||||
- @backstage/plugin-search-backend@0.1.5
|
||||
- @backstage/plugin-techdocs-backend@0.8.1
|
||||
- @backstage/plugin-todo-backend@0.1.5
|
||||
|
||||
## 0.2.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-backend",
|
||||
"version": "0.2.27",
|
||||
"version": "0.2.28",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -27,30 +27,30 @@
|
||||
"migrate:create": "knex migrate:make -x ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/catalog-client": "^0.3.11",
|
||||
"@backstage/catalog-model": "^0.7.8",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/plugin-app-backend": "^0.3.12",
|
||||
"@backstage/plugin-auth-backend": "^0.3.9",
|
||||
"@backstage/plugin-badges-backend": "^0.1.3",
|
||||
"@backstage/plugin-catalog-backend": "^0.8.2",
|
||||
"@backstage/plugin-code-coverage-backend": "^0.1.4",
|
||||
"@backstage/plugin-graphql-backend": "^0.1.7",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.3.6",
|
||||
"@backstage/plugin-kafka-backend": "^0.2.4",
|
||||
"@backstage/plugin-proxy-backend": "^0.2.7",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.10",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.11.0",
|
||||
"@backstage/plugin-search-backend": "^0.1.4",
|
||||
"@backstage/plugin-app-backend": "^0.3.13",
|
||||
"@backstage/plugin-auth-backend": "^0.3.10",
|
||||
"@backstage/plugin-badges-backend": "^0.1.4",
|
||||
"@backstage/plugin-catalog-backend": "^0.9.0",
|
||||
"@backstage/plugin-code-coverage-backend": "^0.1.5",
|
||||
"@backstage/plugin-graphql-backend": "^0.1.8",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.3.7",
|
||||
"@backstage/plugin-kafka-backend": "^0.2.5",
|
||||
"@backstage/plugin-proxy-backend": "^0.2.8",
|
||||
"@backstage/plugin-rollbar-backend": "^0.1.11",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.11.1",
|
||||
"@backstage/plugin-search-backend": "^0.1.5",
|
||||
"@backstage/plugin-search-backend-node": "^0.1.3",
|
||||
"@backstage/plugin-techdocs-backend": "^0.8.0",
|
||||
"@backstage/plugin-todo-backend": "^0.1.4",
|
||||
"@backstage/plugin-techdocs-backend": "^0.8.1",
|
||||
"@backstage/plugin-todo-backend": "^0.1.5",
|
||||
"@gitbeaker/node": "^28.0.2",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"azure-devops-node-api": "^10.1.1",
|
||||
"dockerode": "^3.2.1",
|
||||
"example-app": "^0.2.27",
|
||||
"example-app": "^0.2.28",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"knex": "^0.95.1",
|
||||
@@ -60,7 +60,7 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import {
|
||||
CacheManager,
|
||||
createServiceBuilder,
|
||||
getRootLogger,
|
||||
loadBackendConfig,
|
||||
@@ -59,11 +60,13 @@ function makeCreateEnv(config: Config) {
|
||||
root.info(`Created UrlReader ${reader}`);
|
||||
|
||||
const databaseManager = SingleConnectionDatabaseManager.fromConfig(config);
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = root.child({ type: 'plugin', plugin });
|
||||
const database = databaseManager.forPlugin(plugin);
|
||||
return { logger, database, config, reader, discovery };
|
||||
const cache = cacheManager.forPlugin(plugin);
|
||||
return { logger, cache, database, config, reader, discovery };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
PluginCacheManager,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
UrlReader,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
cache: PluginCacheManager;
|
||||
database: PluginDatabaseManager;
|
||||
config: Config;
|
||||
reader: UrlReader;
|
||||
|
||||
@@ -134,6 +134,7 @@ export class CatalogClient implements CatalogApi {
|
||||
throw new Error(`Location wasn't added: ${target}`);
|
||||
}
|
||||
|
||||
// TODO(jhaals): This will throw using the experimental catalog since all discovered entities are deferred.
|
||||
if (entities.length === 0) {
|
||||
throw new Error(
|
||||
`Location was added but has no entities specified yet: ${target}`,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @backstage/catalog-model
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 10c008a3a: Renamed parameters to input in template schema
|
||||
- 16be1d093: Improve error messages when schema validation fails
|
||||
|
||||
## 0.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/catalog-model",
|
||||
"version": "0.7.8",
|
||||
"version": "0.7.9",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"yup": "^0.29.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/lodash": "^4.14.151",
|
||||
|
||||
@@ -168,4 +168,16 @@ components:
|
||||
(entity as any).spec.system = '';
|
||||
await expect(validator.check(entity)).rejects.toThrow(/system/);
|
||||
});
|
||||
|
||||
it('rejects additional properties', async () => {
|
||||
(entity as any).annotations = 'Test';
|
||||
await expect(validator.check(entity)).rejects.toThrow(
|
||||
/additional properties/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects with useful error message', async () => {
|
||||
(entity as any).annotations = 'Test';
|
||||
await expect(validator.check(entity)).rejects.toThrow(/annotations/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,13 @@ export function ajvCompiledJsonSchemaValidator(
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`Malformed ${kind}, ${error.dataPath || '<root>'} ${error.message}`,
|
||||
`Malformed ${kind}, ${error.dataPath || '<root>'} ${error.message}${
|
||||
error.params
|
||||
? ` - ${Object.entries(error.params)
|
||||
.map(([key, val]) => `${key}: ${val}`)
|
||||
.join(', ')}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @backstage/cli
|
||||
|
||||
## 0.6.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2cd70e164: Add context for versions:bump on what version it was bumped to. Updated tests for the same.
|
||||
- 3be844496: chore: bump `ts-node` versions to 9.1.1
|
||||
- e3fc89df6: update plugins created to use react-use 17.2.4
|
||||
|
||||
## 0.6.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/cli",
|
||||
"description": "CLI for developing Backstage plugins and apps",
|
||||
"version": "0.6.10",
|
||||
"version": "0.6.11",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -117,11 +117,11 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/core": "^0.7.8",
|
||||
"@backstage/dev-utils": "^0.1.13",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/dev-utils": "^0.1.14",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@types/diff": "^5.0.0",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('bump', () => {
|
||||
'bumping @backstage/theme in b to ^2.0.0',
|
||||
'Running yarn install to install new versions',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage/theme',
|
||||
' @backstage/theme : 1.0.0 ~> 2.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
@@ -195,8 +195,14 @@ export default async () => {
|
||||
);
|
||||
console.log();
|
||||
|
||||
for (const name of Array.from(breakingUpdates.keys()).sort()) {
|
||||
console.log(` ${chalk.yellow(name)}`);
|
||||
for (const [name, { from, to }] of Array.from(
|
||||
breakingUpdates.entries(),
|
||||
).sort()) {
|
||||
console.log(
|
||||
` ${chalk.yellow(name)} : ${chalk.yellow(from)} ~> ${chalk.yellow(
|
||||
to,
|
||||
)}`,
|
||||
);
|
||||
|
||||
let path;
|
||||
if (name.startsWith('@backstage/plugin-')) {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@backstage/test-utils": "^{{version '@backstage/test-utils'}}",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"msw": "^0.21.2",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @backstage/core-api
|
||||
|
||||
## 0.2.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- 675a569a9: chore: bump `react-use` dependency in all packages
|
||||
|
||||
## 0.2.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core-api",
|
||||
"description": "Internal Core API used by Backstage plugins and apps",
|
||||
"version": "0.2.17",
|
||||
"version": "0.2.18",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -42,13 +42,13 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.9",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@backstage/test-utils-core": "^0.1.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @backstage/core
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- 889d89b6e: Fix state persisted in the URL make search input in the table toolbar lose their
|
||||
focus.
|
||||
- 3f988cb63: Add count of older messages when multiple messages exist in AlertDisplay
|
||||
- 675a569a9: chore: bump `react-use` dependency in all packages
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core-api@0.2.18
|
||||
|
||||
## 0.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/core",
|
||||
"description": "Core API used by Backstage plugins and apps",
|
||||
"version": "0.7.8",
|
||||
"version": "0.7.9",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/core-api": "^0.2.17",
|
||||
"@backstage/core-api": "^0.2.18",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
@@ -52,6 +52,7 @@
|
||||
"immer": "^9.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"material-table": "^1.69.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.9.4",
|
||||
"rc-progress": "^3.0.0",
|
||||
@@ -70,11 +71,11 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/d3-selection": "^2.0.0",
|
||||
"@types/d3-shape": "^2.0.0",
|
||||
|
||||
@@ -62,4 +62,46 @@ describe('<AlertDisplay />', () => {
|
||||
|
||||
expect(queryByText(TEST_MESSAGE)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with multiple messages', () => {
|
||||
let apiRegistry: ApiRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
apiRegistry = ApiRegistry.from([
|
||||
[
|
||||
alertApiRef,
|
||||
{
|
||||
post() {},
|
||||
alert$() {
|
||||
return Observable.of(
|
||||
{ message: 'message one' },
|
||||
{ message: 'message two' },
|
||||
{ message: 'message three' },
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders first message', async () => {
|
||||
const { queryByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<AlertDisplay />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(queryByText('message one')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a count of remaining messages', async () => {
|
||||
const { queryByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<AlertDisplay />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(queryByText('(2 older messages)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Snackbar, IconButton } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { AlertMessage, useApi, alertApiRef } from '@backstage/core-api';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
// TODO: improve on this and promote to a shared component for use by all apps.
|
||||
export const AlertDisplay = () => {
|
||||
@@ -46,11 +47,7 @@ export const AlertDisplay = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open
|
||||
message={firstMessage.message.toString()}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
>
|
||||
<Snackbar open anchorOrigin={{ vertical: 'top', horizontal: 'center' }}>
|
||||
<Alert
|
||||
action={
|
||||
<IconButton
|
||||
@@ -64,7 +61,15 @@ export const AlertDisplay = () => {
|
||||
}
|
||||
severity={firstMessage.severity}
|
||||
>
|
||||
{firstMessage.message.toString()}
|
||||
<span>
|
||||
{firstMessage.message.toString()}
|
||||
{messages.length > 1 && (
|
||||
<em>{` (${messages.length - 1} older ${pluralize(
|
||||
'message',
|
||||
messages.length - 1,
|
||||
)})`}</em>
|
||||
)}
|
||||
</span>
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
makeStyles,
|
||||
Typography,
|
||||
useTheme,
|
||||
withStyles,
|
||||
} from '@material-ui/core';
|
||||
// Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51
|
||||
import AddBox from '@material-ui/icons/AddBox';
|
||||
@@ -49,6 +50,7 @@ import MTable, {
|
||||
} from 'material-table';
|
||||
import React, {
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -100,19 +102,20 @@ function extractValueByField(data: any, field: string): any | undefined {
|
||||
return value;
|
||||
}
|
||||
|
||||
const useHeaderStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
const StyledMTableHeader = withStyles(theme => ({
|
||||
header: {
|
||||
padding: theme.spacing(1, 2, 1, 2.5),
|
||||
borderTop: `1px solid ${theme.palette.grey.A100}`,
|
||||
borderBottom: `1px solid ${theme.palette.grey.A100}`,
|
||||
color: theme.palette.textSubtle,
|
||||
// withStyles hasn't a generic overload for theme
|
||||
color: (theme as BackstageTheme).palette.textSubtle,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
position: 'static',
|
||||
wordBreak: 'normal',
|
||||
},
|
||||
}));
|
||||
}))(MTableHeader);
|
||||
|
||||
const useToolbarStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
const StyledMTableToolbar = withStyles(theme => ({
|
||||
root: {
|
||||
padding: theme.spacing(3, 0, 2.5, 2.5),
|
||||
},
|
||||
@@ -124,7 +127,7 @@ const useToolbarStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
searchField: {
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
}))(MTableToolbar);
|
||||
|
||||
const useFilterStyles = makeStyles<BackstageTheme>(() => ({
|
||||
root: {
|
||||
@@ -208,6 +211,59 @@ export interface TableProps<T extends object = {}>
|
||||
onStateChange?: (state: TableState) => any;
|
||||
}
|
||||
|
||||
export function TableToolbar(toolbarProps: {
|
||||
toolbarRef: MutableRefObject<any>;
|
||||
setSearch: (value: string) => void;
|
||||
onSearchChanged: (value: string) => void;
|
||||
toggleFilters: () => void;
|
||||
hasFilters: boolean;
|
||||
selectedFiltersLength: number;
|
||||
}) {
|
||||
const {
|
||||
toolbarRef,
|
||||
setSearch,
|
||||
hasFilters,
|
||||
selectedFiltersLength,
|
||||
toggleFilters,
|
||||
} = toolbarProps;
|
||||
const filtersClasses = useFilterStyles();
|
||||
const onSearchChanged = useCallback(
|
||||
(searchText: string) => {
|
||||
toolbarProps.onSearchChanged(searchText);
|
||||
setSearch(searchText);
|
||||
},
|
||||
[toolbarProps, setSearch],
|
||||
);
|
||||
|
||||
if (hasFilters) {
|
||||
return (
|
||||
<div className={filtersClasses.root}>
|
||||
<div className={filtersClasses.root}>
|
||||
<IconButton onClick={toggleFilters} aria-label="filter list">
|
||||
<FilterList />
|
||||
</IconButton>
|
||||
<Typography className={filtersClasses.title}>
|
||||
Filters ({selectedFiltersLength})
|
||||
</Typography>
|
||||
</div>
|
||||
<StyledMTableToolbar
|
||||
{...toolbarProps}
|
||||
ref={toolbarRef}
|
||||
onSearchChanged={onSearchChanged}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledMTableToolbar
|
||||
{...toolbarProps}
|
||||
ref={toolbarRef}
|
||||
onSearchChanged={onSearchChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Table<T extends object = {}>({
|
||||
columns,
|
||||
options,
|
||||
@@ -219,10 +275,7 @@ export function Table<T extends object = {}>({
|
||||
onStateChange,
|
||||
...props
|
||||
}: TableProps<T>) {
|
||||
const headerClasses = useHeaderStyles();
|
||||
const toolbarClasses = useToolbarStyles();
|
||||
const tableClasses = useTableStyles();
|
||||
const filtersClasses = useFilterStyles();
|
||||
|
||||
const { data, ...propsWithoutData } = props;
|
||||
|
||||
@@ -230,9 +283,12 @@ export function Table<T extends object = {}>({
|
||||
|
||||
const calculatedInitialState = { ...defaultInitialState, ...initialState };
|
||||
|
||||
const [filtersOpen, toggleFilters] = useState(
|
||||
const [filtersOpen, setFiltersOpen] = useState(
|
||||
calculatedInitialState.filtersOpen,
|
||||
);
|
||||
const toggleFilters = useCallback(() => setFiltersOpen(v => !v), [
|
||||
setFiltersOpen,
|
||||
]);
|
||||
const [selectedFiltersLength, setSelectedFiltersLength] = useState(0);
|
||||
const [tableData, setTableData] = useState(data as any[]);
|
||||
const [selectedFilters, setSelectedFilters] = useState(
|
||||
@@ -379,64 +435,32 @@ export function Table<T extends object = {}>({
|
||||
}));
|
||||
};
|
||||
|
||||
const hasFilters = !!filters?.length;
|
||||
const Toolbar = useCallback(
|
||||
toolbarProps => {
|
||||
const onSearchChanged = (searchText: string) => {
|
||||
toolbarProps.onSearchChanged(searchText);
|
||||
setSearch(searchText);
|
||||
};
|
||||
|
||||
if (filters?.length) {
|
||||
return (
|
||||
<div className={filtersClasses.root}>
|
||||
<div className={filtersClasses.root}>
|
||||
<IconButton
|
||||
onClick={() => toggleFilters(el => !el)}
|
||||
aria-label="filter list"
|
||||
>
|
||||
<FilterList />
|
||||
</IconButton>
|
||||
<Typography className={filtersClasses.title}>
|
||||
Filters ({selectedFiltersLength})
|
||||
</Typography>
|
||||
</div>
|
||||
<MTableToolbar
|
||||
classes={toolbarClasses}
|
||||
{...toolbarProps}
|
||||
ref={toolbarRef}
|
||||
onSearchChanged={onSearchChanged}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MTableToolbar
|
||||
classes={toolbarClasses}
|
||||
<TableToolbar
|
||||
toolbarRef={toolbarRef}
|
||||
setSearch={setSearch}
|
||||
hasFilters={hasFilters}
|
||||
selectedFiltersLength={selectedFiltersLength}
|
||||
toggleFilters={toggleFilters}
|
||||
{...toolbarProps}
|
||||
ref={toolbarRef}
|
||||
onSearchChanged={onSearchChanged}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
filters?.length,
|
||||
selectedFiltersLength,
|
||||
toggleFilters,
|
||||
toolbarClasses,
|
||||
filtersClasses,
|
||||
setSearch,
|
||||
toolbarRef,
|
||||
],
|
||||
[toggleFilters, hasFilters, selectedFiltersLength, setSearch, toolbarRef],
|
||||
);
|
||||
|
||||
const hasNoRows = typeof data !== 'function' && data.length === 0;
|
||||
const columnCount = columns.length;
|
||||
const Body = useCallback(
|
||||
bodyProps => {
|
||||
if (emptyContent && typeof data !== 'function' && data.length === 0) {
|
||||
if (emptyContent && hasNoRows) {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={columns.length}>{emptyContent}</td>
|
||||
<td colSpan={columnCount}>{emptyContent}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
@@ -444,7 +468,7 @@ export function Table<T extends object = {}>({
|
||||
|
||||
return <MTableBody {...bodyProps} />;
|
||||
},
|
||||
[data, emptyContent, columns],
|
||||
[hasNoRows, emptyContent, columnCount],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -458,9 +482,7 @@ export function Table<T extends object = {}>({
|
||||
)}
|
||||
<MTable<T>
|
||||
components={{
|
||||
Header: headerProps => (
|
||||
<MTableHeader classes={headerClasses} {...headerProps} />
|
||||
),
|
||||
Header: StyledMTableHeader,
|
||||
Toolbar,
|
||||
Body,
|
||||
}}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import qs from 'qs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
function stringify(queryParams: any): string {
|
||||
@@ -58,36 +58,37 @@ type SetQueryParams<T> = (params: T) => void;
|
||||
|
||||
export function useQueryParamState<T>(
|
||||
stateName: string,
|
||||
debounceTime: number = 100,
|
||||
/** @deprecated Don't configure a custom debouceTime */
|
||||
debounceTime: number = 250,
|
||||
): [T | undefined, SetQueryParams<T>] {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const searchParamsString = searchParams.toString();
|
||||
const [queryParamState, setQueryParamState] = useState<T>(
|
||||
extractState(location.search, stateName),
|
||||
extractState(searchParamsString, stateName),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newState = extractState(location.search, stateName);
|
||||
const newState = extractState(searchParamsString, stateName);
|
||||
|
||||
setQueryParamState(oldState =>
|
||||
isEqual(newState, oldState) ? oldState : newState,
|
||||
);
|
||||
}, [location, stateName]);
|
||||
}, [searchParamsString, setQueryParamState, stateName]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
const queryString = joinQueryString(
|
||||
location.search,
|
||||
searchParamsString,
|
||||
stateName,
|
||||
queryParamState,
|
||||
);
|
||||
|
||||
if (location.search !== queryString) {
|
||||
navigate({ ...location, search: `?${queryString}` }, { replace: true });
|
||||
if (searchParamsString !== queryString) {
|
||||
setSearchParams(queryString, { replace: true });
|
||||
}
|
||||
},
|
||||
debounceTime,
|
||||
[queryParamState],
|
||||
[setSearchParams, queryParamState, searchParamsString, stateName],
|
||||
);
|
||||
|
||||
return [queryParamState, setQueryParamState];
|
||||
|
||||
@@ -1,5 +1,52 @@
|
||||
# @backstage/create-app
|
||||
|
||||
## 0.3.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3be844496: chore: bump `ts-node` versions to 9.1.1
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [2cd70e164]
|
||||
- Updated dependencies [0b033d07b]
|
||||
- Updated dependencies [3be844496]
|
||||
- Updated dependencies [5542de095]
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [82ca1ac22]
|
||||
- Updated dependencies [81ef1d57b]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- Updated dependencies [e3fc89df6]
|
||||
- Updated dependencies [9a207f052]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [fd39d4662]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
- @backstage/integration-react@0.1.2
|
||||
- @backstage/test-utils@0.1.11
|
||||
- @backstage/plugin-api-docs@0.4.13
|
||||
- @backstage/plugin-catalog@0.5.7
|
||||
- @backstage/plugin-catalog-import@0.5.6
|
||||
- @backstage/plugin-explore@0.3.5
|
||||
- @backstage/plugin-github-actions@0.4.6
|
||||
- @backstage/plugin-lighthouse@0.2.16
|
||||
- @backstage/plugin-scaffolder@0.9.4
|
||||
- @backstage/plugin-scaffolder-backend@0.11.1
|
||||
- @backstage/plugin-search@0.3.6
|
||||
- @backstage/plugin-tech-radar@0.3.11
|
||||
- @backstage/plugin-techdocs@0.9.2
|
||||
- @backstage/plugin-user-settings@0.2.10
|
||||
- @backstage/cli@0.6.11
|
||||
- @backstage/backend-common@0.8.0
|
||||
- @backstage/catalog-model@0.7.9
|
||||
- @backstage/plugin-catalog-backend@0.9.0
|
||||
- @backstage/plugin-app-backend@0.3.13
|
||||
- @backstage/plugin-auth-backend@0.3.10
|
||||
- @backstage/plugin-proxy-backend@0.2.8
|
||||
- @backstage/plugin-rollbar-backend@0.1.11
|
||||
- @backstage/plugin-techdocs-backend@0.8.1
|
||||
|
||||
## 0.3.21
|
||||
|
||||
### Patch Changes
|
||||
@@ -15,8 +62,6 @@
|
||||
|
||||
+ import { DockerContainerRunner } from '@backstage/backend-common';
|
||||
|
||||
// ...
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
@@ -76,7 +121,6 @@
|
||||
+ SingleHostDiscovery,
|
||||
+ } from '@backstage/backend-common';
|
||||
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
@@ -933,26 +977,25 @@
|
||||
|
||||
Update imports and remove the usage of the deprecated `app.getRoutes()`.
|
||||
|
||||
```diff
|
||||
-import { Router as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
+import { TechdocsPage } from '@backstage/plugin-techdocs';
|
||||
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
|
||||
-import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
|
||||
-import { SearchPage as SearchRouter } from '@backstage/plugin-search';
|
||||
-import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
|
||||
+import { TechRadarPage } from '@backstage/plugin-tech-radar';
|
||||
+import { SearchPage } from '@backstage/plugin-search';
|
||||
+import { UserSettingsPage } from '@backstage/plugin-user-settings';
|
||||
+import { ApiExplorerPage } from '@backstage/plugin-api-docs';
|
||||
import { EntityPage } from './components/catalog/EntityPage';
|
||||
import { scaffolderPlugin, ScaffolderPage } from '@backstage/plugin-scaffolder';
|
||||
```
|
||||
```diff
|
||||
- import { Router as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
+ import { TechdocsPage } from '@backstage/plugin-techdocs';
|
||||
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
|
||||
- import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
|
||||
- import { SearchPage as SearchRouter } from '@backstage/plugin-search';
|
||||
- import { Router as SettingsRouter } from '@backstage/plugin-user-settings';
|
||||
+ import { TechRadarPage } from '@backstage/plugin-tech-radar';
|
||||
+ import { SearchPage } from '@backstage/plugin-search';
|
||||
+ import { UserSettingsPage } from '@backstage/plugin-user-settings';
|
||||
+ import { ApiExplorerPage } from '@backstage/plugin-api-docs';
|
||||
import { EntityPage } from './components/catalog/EntityPage';
|
||||
import { scaffolderPlugin, ScaffolderPage } from '@backstage/plugin-scaffolder';
|
||||
|
||||
const AppProvider = app.getProvider();
|
||||
const AppRouter = app.getRouter();
|
||||
-const deprecatedAppRoutes = app.getRoutes();
|
||||
|
||||
````
|
||||
const AppProvider = app.getProvider();
|
||||
const AppRouter = app.getRouter();
|
||||
- const deprecatedAppRoutes = app.getRoutes();
|
||||
```
|
||||
|
||||
As well as update or add the following routes:
|
||||
|
||||
@@ -975,7 +1018,7 @@ As well as update or add the following routes:
|
||||
- {deprecatedAppRoutes}
|
||||
+ <Route path="/search" element={<SearchPage />} />
|
||||
+ <Route path="/settings" element={<UserSettingsPage />} />
|
||||
````
|
||||
```
|
||||
|
||||
If you have added additional plugins with registered routes or are using `Router` components from other plugins, these should be migrated to use the `*Page` components as well. See [this commit](https://github.com/backstage/backstage/commit/abd655e42d4ed416b70848ffdb1c4b99d189f13b) for more examples of how to migrate.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/create-app",
|
||||
"description": "Create app package for Backstage",
|
||||
"version": "0.3.21",
|
||||
"version": "0.3.22",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @backstage/dev-utils
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
- @backstage/integration-react@0.1.2
|
||||
- @backstage/test-utils@0.1.11
|
||||
- @backstage/plugin-catalog-react@0.1.6
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.1.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/dev-utils",
|
||||
"description": "Utilities for developing Backstage plugins.",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -29,17 +29,17 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.7.0",
|
||||
"@backstage/catalog-model": "^0.7.3",
|
||||
"@backstage/integration-react": "^0.1.1",
|
||||
"@backstage/plugin-catalog-react": "^0.1.1",
|
||||
"@backstage/test-utils": "^0.1.8",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/integration-react": "^0.1.2",
|
||||
"@backstage/plugin-catalog-react": "^0.1.6",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@backstage/theme": "^0.2.3",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/react": "^16.9",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"react-router-dom": "6.0.0-beta.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.3",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @backstage/integration-react
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- 675a569a9: chore: bump `react-use` dependency in all packages
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/integration-react",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/core": "^0.7.0",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/integration": "^0.5.0",
|
||||
"@backstage/theme": "^0.2.3",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
@@ -32,12 +32,12 @@
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.3",
|
||||
"@backstage/dev-utils": "^0.1.13",
|
||||
"@backstage/test-utils": "^0.1.8",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/dev-utils": "^0.1.14",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"msw": "^0.21.2",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @backstage/techdocs-common
|
||||
|
||||
## 0.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e04f1ccfb: Fixed a bug that prevented loading static assets from GCS, S3, Azure, and OpenStackSwift whose keys contain spaces or other special characters.
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- Updated dependencies [16be1d093]
|
||||
- @backstage/backend-common@0.8.0
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -71,9 +71,9 @@ export class BlockBlobClient {
|
||||
download() {
|
||||
const filePath = path.join(rootDir, this.blobName);
|
||||
const emitter = new EventEmitter();
|
||||
process.nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
emitter.emit('data', Buffer.from(fs.readFileSync(filePath)));
|
||||
emitter.emit('data', fs.readFileSync(filePath));
|
||||
emitter.emit('end');
|
||||
} else {
|
||||
emitter.emit(
|
||||
@@ -81,7 +81,7 @@ export class BlockBlobClient {
|
||||
new Error(`The file ${filePath} does not exist !`),
|
||||
);
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
return Promise.resolve({
|
||||
readableStreamBody: emitter,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { EventEmitter } from 'events';
|
||||
import { Readable } from 'stream';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
@@ -58,19 +58,21 @@ class GCSFile {
|
||||
}
|
||||
|
||||
createReadStream() {
|
||||
const emitter = new EventEmitter();
|
||||
const readable = new Readable();
|
||||
readable._read = () => {};
|
||||
|
||||
process.nextTick(() => {
|
||||
if (fs.existsSync(this.localFilePath)) {
|
||||
emitter.emit('data', Buffer.from(fs.readFileSync(this.localFilePath)));
|
||||
emitter.emit('end');
|
||||
readable.emit('data', fs.readFileSync(this.localFilePath));
|
||||
readable.emit('end');
|
||||
} else {
|
||||
emitter.emit(
|
||||
readable.emit(
|
||||
'error',
|
||||
new Error(`The file ${this.localFilePath} does not exist !`),
|
||||
);
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
return readable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.6.0",
|
||||
"version": "0.6.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
@@ -38,8 +38,8 @@
|
||||
"dependencies": {
|
||||
"@azure/identity": "^1.2.2",
|
||||
"@azure/storage-blob": "^12.4.0",
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/catalog-model": "^0.7.8",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.2",
|
||||
@@ -60,14 +60,16 @@
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/pkgcloud": "^1.7.4",
|
||||
"@types/recursive-readdir": "^2.2.0"
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
@@ -272,4 +274,49 @@ describe('AwsS3Publish', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('docsRouter', () => {
|
||||
let app: express.Express;
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
beforeEach(() => {
|
||||
app = express().use(publisher.docsRouter());
|
||||
|
||||
mockFs.restore();
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
img: {
|
||||
'with spaces.png': 'found it',
|
||||
},
|
||||
'some folder': {
|
||||
'also with spaces.js': 'found it too',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should pass expected object path to bucket', async () => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
} = entity;
|
||||
|
||||
// Ensures leading slash is trimmed and encoded path is decoded.
|
||||
const pngResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
|
||||
);
|
||||
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
|
||||
'found it',
|
||||
);
|
||||
const jsResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
|
||||
);
|
||||
expect(jsResponse.text).toEqual('found it too');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,9 +263,9 @@ export class AwsS3Publish implements PublisherBase {
|
||||
*/
|
||||
docsRouter(): express.Handler {
|
||||
return async (req, res) => {
|
||||
// Trim the leading forward slash
|
||||
// Decode and trim the leading forward slash
|
||||
// filePath example - /default/Component/documented-component/index.html
|
||||
const filePath = req.path.replace(/^\//, '');
|
||||
const filePath = decodeURI(req.path.replace(/^\//, ''));
|
||||
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = path.extname(filePath);
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
@@ -329,4 +331,49 @@ describe('publishing with valid credentials', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('docsRouter', () => {
|
||||
let app: express.Express;
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
beforeEach(() => {
|
||||
app = express().use(publisher.docsRouter());
|
||||
|
||||
mockFs.restore();
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
img: {
|
||||
'with spaces.png': 'found it',
|
||||
},
|
||||
'some folder': {
|
||||
'also with spaces.js': 'found it too',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should pass expected object path to bucket', async () => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
} = entity;
|
||||
|
||||
// Ensures leading slash is trimmed and encoded path is decoded.
|
||||
const pngResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
|
||||
);
|
||||
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
|
||||
'found it',
|
||||
);
|
||||
const jsResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
|
||||
);
|
||||
expect(jsResponse.text).toEqual('found it too');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,9 +254,9 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
*/
|
||||
docsRouter(): express.Handler {
|
||||
return (req, res) => {
|
||||
// Trim the leading forward slash
|
||||
// Decode and trim the leading forward slash
|
||||
// filePath example - /default/Component/documented-component/index.html
|
||||
const filePath = req.path.replace(/^\//, '');
|
||||
const filePath = decodeURI(req.path.replace(/^\//, ''));
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = platformPath.extname(filePath);
|
||||
const responseHeaders = getHeadersForFileExtension(fileExtension);
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
EntityName,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
@@ -271,4 +273,47 @@ describe('GoogleGCSPublish', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('docsRouter', () => {
|
||||
let app: express.Express;
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
beforeEach(() => {
|
||||
app = express().use(publisher.docsRouter());
|
||||
|
||||
mockFs.restore();
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
img: {
|
||||
'with spaces.png': 'found it',
|
||||
},
|
||||
'some folder': {
|
||||
'also with spaces.js': 'found it too',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should pass expected object path to bucket', async () => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
} = entity;
|
||||
|
||||
// Ensures leading slash is trimmed and encoded path is decoded.
|
||||
const pngResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
|
||||
);
|
||||
expect(pngResponse.text).toEqual('found it');
|
||||
const jsResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
|
||||
);
|
||||
expect(jsResponse.text).toEqual('found it too');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,9 +187,9 @@ export class GoogleGCSPublish implements PublisherBase {
|
||||
*/
|
||||
docsRouter(): express.Handler {
|
||||
return (req, res) => {
|
||||
// Trim the leading forward slash
|
||||
// Decode and trim the leading forward slash
|
||||
// filePath example - /default/Component/documented-component/index.html
|
||||
const filePath = req.path.replace(/^\//, '');
|
||||
const filePath = decodeURI(req.path.replace(/^\//, ''));
|
||||
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = path.extname(filePath);
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
@@ -277,4 +279,49 @@ describe('OpenStackSwiftPublish', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('docsRouter', () => {
|
||||
let app: express.Express;
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
beforeEach(() => {
|
||||
app = express().use(publisher.docsRouter());
|
||||
|
||||
mockFs.restore();
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
img: {
|
||||
'with spaces.png': 'found it',
|
||||
},
|
||||
'some folder': {
|
||||
'also with spaces.js': 'found it too',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should pass expected object path to bucket', async () => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
} = entity;
|
||||
|
||||
// Ensures leading slash is trimmed and encoded path is decoded.
|
||||
const pngResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
|
||||
);
|
||||
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
|
||||
'found it',
|
||||
);
|
||||
const jsResponse = await request(app).get(
|
||||
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
|
||||
);
|
||||
expect(jsResponse.text).toEqual('found it too');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,10 +221,9 @@ export class OpenStackSwiftPublish implements PublisherBase {
|
||||
*/
|
||||
docsRouter(): express.Handler {
|
||||
return async (req, res) => {
|
||||
// Trim the leading forward slash
|
||||
// Decode and trim the leading forward slash
|
||||
// filePath example - /default/Component/documented-component/index.html
|
||||
|
||||
const filePath = req.path.replace(/^\//, '');
|
||||
const filePath = decodeURI(req.path.replace(/^\//, ''));
|
||||
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = path.extname(filePath);
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @backstage/test-utils
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core-api@0.2.18
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/test-utils",
|
||||
"description": "Utilities to test Backstage plugins and apps.",
|
||||
"version": "0.1.10",
|
||||
"version": "0.1.11",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
@@ -29,13 +29,13 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core-api": "^0.2.16",
|
||||
"@backstage/core-api": "^0.2.18",
|
||||
"@backstage/test-utils-core": "^0.1.1",
|
||||
"@backstage/theme": "^0.2.3",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/react": "^16.9",
|
||||
"msw": "^0.21.3",
|
||||
"react": "^16.12.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.7",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32"
|
||||
},
|
||||
|
||||
@@ -18,10 +18,13 @@ import {
|
||||
storageApiRef,
|
||||
errorApiRef,
|
||||
createApiFactory,
|
||||
AlertApiForwarder,
|
||||
alertApiRef,
|
||||
} from '@backstage/core-api';
|
||||
import { MockErrorApi, MockStorageApi } from './apis';
|
||||
|
||||
export const mockApis = [
|
||||
createApiFactory(errorApiRef, new MockErrorApi()),
|
||||
createApiFactory(storageApiRef, MockStorageApi.create()),
|
||||
createApiFactory(alertApiRef, new AlertApiForwarder()),
|
||||
];
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @backstage/plugin-api-docs
|
||||
|
||||
## 0.4.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- 889d89b6e: Fix state persisted in the URL make search input in the table toolbar lose their
|
||||
focus.
|
||||
- 675a569a9: chore: bump `react-use` dependency in all packages
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
- @backstage/plugin-catalog-react@0.1.6
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.4.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-api-docs",
|
||||
"version": "0.4.12",
|
||||
"version": "0.4.13",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -30,9 +30,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@asyncapi/react-component": "^0.23.0",
|
||||
"@backstage/catalog-model": "^0.7.7",
|
||||
"@backstage/core": "^0.7.8",
|
||||
"@backstage/plugin-catalog-react": "^0.1.4",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/plugin-catalog-react": "^0.1.6",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@material-icons/font": "^1.0.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
@@ -49,12 +49,12 @@
|
||||
"swagger-ui-react": "^3.37.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/dev-utils": "^0.1.13",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/dev-utils": "^0.1.14",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/swagger-ui-react": "^3.23.3",
|
||||
|
||||
@@ -156,7 +156,6 @@ export const ApiExplorerTable = ({
|
||||
}: ExplorerTableProps) => {
|
||||
const [queryParamState, setQueryParamState] = useQueryParamState<TableState>(
|
||||
'apiTable',
|
||||
500,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @backstage/plugin-app-backend
|
||||
|
||||
## 0.3.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- @backstage/backend-common@0.8.0
|
||||
|
||||
## 0.3.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-app-backend",
|
||||
"version": "0.3.12",
|
||||
"version": "0.3.13",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -29,7 +29,7 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/config-loader": "^0.6.1",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@types/express": "^4.17.6",
|
||||
@@ -40,7 +40,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"msw": "^0.20.5",
|
||||
"supertest": "^6.1.3"
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @backstage/plugin-auth-backend
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- Updated dependencies [16be1d093]
|
||||
- @backstage/test-utils@0.1.11
|
||||
- @backstage/backend-common@0.8.0
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend",
|
||||
"version": "0.3.9",
|
||||
"version": "0.3.10",
|
||||
"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.7.0",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/catalog-client": "^0.3.11",
|
||||
"@backstage/catalog-model": "^0.7.8",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@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.6.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express-session": "^1.17.2",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @backstage/plugin-badges-backend
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- Updated dependencies [16be1d093]
|
||||
- @backstage/backend-common@0.8.0
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-badges-backend",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -30,9 +30,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/catalog-client": "^0.3.11",
|
||||
"@backstage/catalog-model": "^0.7.8",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@types/express": "^4.17.6",
|
||||
@@ -45,7 +45,7 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @backstage/plugin-badges
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- 675a569a9: chore: bump `react-use` dependency in all packages
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
- @backstage/plugin-catalog-react@0.1.6
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-badges",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,10 +20,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.6",
|
||||
"@backstage/core": "^0.7.8",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/plugin-catalog-react": "^0.1.3",
|
||||
"@backstage/plugin-catalog-react": "^0.1.6",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -34,12 +34,12 @@
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/dev-utils": "^0.1.13",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/dev-utils": "^0.1.14",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"cross-fetch": "^3.0.6",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @backstage/plugin-bitrise
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8
|
||||
- 675a569a9: chore: bump `react-use` dependency in all packages
|
||||
- Updated dependencies [062bbf90f]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [889d89b6e]
|
||||
- Updated dependencies [16be1d093]
|
||||
- Updated dependencies [3f988cb63]
|
||||
- Updated dependencies [675a569a9]
|
||||
- @backstage/core@0.7.9
|
||||
- @backstage/plugin-catalog-react@0.1.6
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-bitrise",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -20,9 +20,9 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.2",
|
||||
"@backstage/core": "^0.7.8",
|
||||
"@backstage/plugin-catalog-react": "^0.1.2",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/core": "^0.7.9",
|
||||
"@backstage/plugin-catalog-react": "^0.1.6",
|
||||
"@backstage/theme": "^0.2.7",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
@@ -37,12 +37,12 @@
|
||||
"recharts": "^1.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/dev-utils": "^0.1.13",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/dev-utils": "^0.1.14",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/recharts": "^1.8.15",
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# @backstage/plugin-catalog-backend
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9a207f052: Port `GithubOrgReaderProcessor` to support configuration via
|
||||
[`integrations`](https://backstage.io/docs/integrations/github/locations) in
|
||||
addition to [`catalog.processors.githubOrg.providers`](https://backstage.io/docs/integrations/github/org#configuration).
|
||||
The `integrations` package supports authentication with both personal access
|
||||
tokens and GitHub apps.
|
||||
|
||||
This deprecates the `catalog.processors.githubOrg.providers` configuration.
|
||||
A [`integrations` configuration](https://backstage.io/docs/integrations/github/locations)
|
||||
for the same host takes precedence over the provider configuration.
|
||||
You might need to add additional scopes for the credentials.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [22fd8ce2a]
|
||||
- Updated dependencies [10c008a3a]
|
||||
- Updated dependencies [f9fb4a205]
|
||||
- Updated dependencies [16be1d093]
|
||||
- @backstage/backend-common@0.8.0
|
||||
- @backstage/catalog-model@0.7.9
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
+2
@@ -114,6 +114,8 @@ export interface Config {
|
||||
processors?: {
|
||||
/**
|
||||
* GithubOrgReaderProcessor configuration
|
||||
*
|
||||
* @deprecated Configure a GitHub integration instead.
|
||||
*/
|
||||
githubOrg?: {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-catalog-backend",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -30,8 +30,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.0.0-beta.3",
|
||||
"@backstage/backend-common": "^0.7.0",
|
||||
"@backstage/catalog-model": "^0.7.8",
|
||||
"@backstage/backend-common": "^0.8.0",
|
||||
"@backstage/catalog-model": "^0.7.9",
|
||||
"@backstage/config": "^0.1.5",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.2",
|
||||
@@ -63,8 +63,8 @@
|
||||
"yup": "^0.29.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/test-utils": "^0.1.9",
|
||||
"@backstage/cli": "^0.6.11",
|
||||
"@backstage/test-utils": "^0.1.11",
|
||||
"@types/core-js": "^2.5.4",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/lodash": "^4.14.151",
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import {
|
||||
GitHubIntegration,
|
||||
ScmIntegrations,
|
||||
ScmIntegrationsGroup,
|
||||
} from '@backstage/integration';
|
||||
import { GithubOrgReaderProcessor, parseUrl } from './GithubOrgReaderProcessor';
|
||||
|
||||
describe('GithubOrgReaderProcessor', () => {
|
||||
@@ -29,6 +34,20 @@ describe('GithubOrgReaderProcessor', () => {
|
||||
});
|
||||
|
||||
describe('implementation', () => {
|
||||
let integrations: ScmIntegrations;
|
||||
let github: jest.Mocked<ScmIntegrationsGroup<GitHubIntegration>>;
|
||||
|
||||
beforeEach(() => {
|
||||
github = {
|
||||
byHost: jest.fn(),
|
||||
byUrl: jest.fn(),
|
||||
list: jest.fn(),
|
||||
};
|
||||
integrations = ({
|
||||
github,
|
||||
} as Partial<ScmIntegrations>) as ScmIntegrations;
|
||||
});
|
||||
|
||||
it('rejects unknown types', async () => {
|
||||
const processor = new GithubOrgReaderProcessor({
|
||||
providers: [
|
||||
@@ -37,6 +56,7 @@ describe('GithubOrgReaderProcessor', () => {
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
},
|
||||
],
|
||||
integrations,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
const location: LocationSpec = {
|
||||
@@ -48,7 +68,7 @@ describe('GithubOrgReaderProcessor', () => {
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
it('rejects unknown targets from providers', async () => {
|
||||
const processor = new GithubOrgReaderProcessor({
|
||||
providers: [
|
||||
{
|
||||
@@ -56,6 +76,24 @@ describe('GithubOrgReaderProcessor', () => {
|
||||
apiBaseUrl: 'https://api.github.com',
|
||||
},
|
||||
],
|
||||
integrations,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
const location: LocationSpec = {
|
||||
type: 'github-org',
|
||||
target: 'https://not.github.com/apa',
|
||||
};
|
||||
await expect(
|
||||
processor.readLocation(location, false, () => {}),
|
||||
).rejects.toThrow(
|
||||
/There is no GitHub Org provider that matches https:\/\/not.github.com\/apa/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unknown targets from integrations', async () => {
|
||||
const processor = new GithubOrgReaderProcessor({
|
||||
providers: [],
|
||||
integrations,
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
const location: LocationSpec = {
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
@@ -28,22 +32,33 @@ import * as results from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
import { buildOrgHierarchy } from './util/org';
|
||||
|
||||
type GraphQL = typeof graphql;
|
||||
|
||||
/**
|
||||
* Extracts teams and users out of a GitHub org.
|
||||
*/
|
||||
export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
private readonly providers: ProviderConfig[];
|
||||
private readonly integrations: ScmIntegrations;
|
||||
private readonly logger: Logger;
|
||||
|
||||
static fromConfig(config: Config, options: { logger: Logger }) {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
return new GithubOrgReaderProcessor({
|
||||
...options,
|
||||
providers: readGithubConfig(config),
|
||||
integrations,
|
||||
});
|
||||
}
|
||||
|
||||
constructor(options: { providers: ProviderConfig[]; logger: Logger }) {
|
||||
constructor(options: {
|
||||
providers: ProviderConfig[];
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger;
|
||||
}) {
|
||||
this.providers = options.providers;
|
||||
this.integrations = options.integrations;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
@@ -56,24 +71,8 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
return false;
|
||||
}
|
||||
|
||||
const provider = this.providers.find(p =>
|
||||
location.target.startsWith(`${p.target}/`),
|
||||
);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`There is no GitHub Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.githubOrg.providers.`,
|
||||
);
|
||||
}
|
||||
|
||||
const client = await this.createClient(location.target);
|
||||
const { org } = parseUrl(location.target);
|
||||
const client = !provider.token
|
||||
? graphql
|
||||
: graphql.defaults({
|
||||
baseUrl: provider.apiBaseUrl,
|
||||
headers: {
|
||||
authorization: `token ${provider.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Read out all of the raw data
|
||||
const startTimestamp = Date.now();
|
||||
@@ -112,6 +111,67 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async createClient(orgUrl: string): Promise<GraphQL> {
|
||||
let client = await this.createClientFromIntegrations(orgUrl);
|
||||
|
||||
if (!client) {
|
||||
client = await this.createClientFromProvider(orgUrl);
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
throw new Error(
|
||||
`There is no GitHub Org provider that matches ${orgUrl}. Please add a configuration for an integration or add an entry for it under catalog.processors.githubOrg.providers.`,
|
||||
);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private async createClientFromProvider(
|
||||
orgUrl: string,
|
||||
): Promise<GraphQL | undefined> {
|
||||
const provider = this.providers.find(p =>
|
||||
orgUrl.startsWith(`${p.target}/`),
|
||||
);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
'GithubOrgReaderProcessor uses provider defined in catalog.processors.githubOrg.providers, migrate to integrations instead. See https://backstage.io/docs/integrations/github/locations',
|
||||
);
|
||||
|
||||
return !provider.token
|
||||
? graphql
|
||||
: graphql.defaults({
|
||||
baseUrl: provider.apiBaseUrl,
|
||||
headers: {
|
||||
authorization: `token ${provider.token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async createClientFromIntegrations(
|
||||
orgUrl: string,
|
||||
): Promise<GraphQL | undefined> {
|
||||
const gitHubConfig = this.integrations.github.byUrl(orgUrl)?.config;
|
||||
if (!gitHubConfig) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const credentialsProvider = GithubCredentialsProvider.create(gitHubConfig);
|
||||
|
||||
const { headers } = await credentialsProvider.getCredentials({
|
||||
url: orgUrl,
|
||||
});
|
||||
|
||||
return graphql.defaults({
|
||||
baseUrl: gitHubConfig.apiBaseUrl,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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 { DefaultLocationService } from './DefaultLocationService';
|
||||
import { CatalogProcessingOrchestrator, LocationStore } from './types';
|
||||
|
||||
describe('DefaultLocationServiceTest', () => {
|
||||
const orchestrator: jest.Mocked<CatalogProcessingOrchestrator> = {
|
||||
process: jest.fn(),
|
||||
};
|
||||
const store: jest.Mocked<LocationStore> = {
|
||||
deleteLocation: jest.fn(),
|
||||
createLocation: jest.fn(),
|
||||
listLocations: jest.fn(),
|
||||
getLocation: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
const locationService = new DefaultLocationService(store, orchestrator);
|
||||
describe('createLocation', () => {
|
||||
it('should support dry run', async () => {
|
||||
orchestrator.process.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
state: new Map(),
|
||||
completedEntity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: 'foo',
|
||||
},
|
||||
},
|
||||
deferredEntities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
relations: [],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
orchestrator.process.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
state: new Map(),
|
||||
completedEntity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'bar',
|
||||
},
|
||||
},
|
||||
deferredEntities: [],
|
||||
relations: [],
|
||||
errors: [],
|
||||
});
|
||||
|
||||
await locationService.createLocation(
|
||||
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
|
||||
true,
|
||||
);
|
||||
|
||||
expect(orchestrator.process).toBeCalledWith({
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'url:https://backstage.io/catalog-info.yaml',
|
||||
'backstage.io/managed-by-origin-location':
|
||||
'url:https://backstage.io/catalog-info.yaml',
|
||||
},
|
||||
name: 'generated-bbad4f61e08f24e25d5c5e68e13e164f760aff06',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
target: 'https://backstage.io/catalog-info.yaml',
|
||||
type: 'url',
|
||||
},
|
||||
},
|
||||
state: expect.anything(),
|
||||
});
|
||||
|
||||
expect(orchestrator.process).toBeCalledWith({
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'bar' },
|
||||
},
|
||||
state: expect.anything(),
|
||||
});
|
||||
expect(orchestrator.process).toBeCalledTimes(2);
|
||||
expect(store.createLocation).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should create location', async () => {
|
||||
const locationSpec = {
|
||||
type: 'url',
|
||||
target: 'https://backstage.io/catalog-info.yaml',
|
||||
};
|
||||
|
||||
store.createLocation.mockResolvedValue({
|
||||
...locationSpec,
|
||||
id: '123',
|
||||
});
|
||||
|
||||
await expect(
|
||||
locationService.createLocation(locationSpec, false),
|
||||
).resolves.toEqual({
|
||||
entities: [],
|
||||
location: {
|
||||
id: '123',
|
||||
target: 'https://backstage.io/catalog-info.yaml',
|
||||
type: 'url',
|
||||
},
|
||||
});
|
||||
expect(store.createLocation).toBeCalledWith({
|
||||
target: 'https://backstage.io/catalog-info.yaml',
|
||||
type: 'url',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('listLocations', () => {
|
||||
it('should call locationStore.deleteLocation', async () => {
|
||||
await locationService.listLocations();
|
||||
expect(store.listLocations).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteLocation', () => {
|
||||
it('should call locationStore.deleteLocation', async () => {
|
||||
await locationService.deleteLocation('123');
|
||||
expect(store.deleteLocation).toBeCalledWith('123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocation', () => {
|
||||
it('should call locationStore.getLocation', async () => {
|
||||
await locationService.getLocation('123');
|
||||
expect(store.getLocation).toBeCalledWith('123');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
LocationStore,
|
||||
CatalogProcessingOrchestrator,
|
||||
} from './types';
|
||||
import { locationSpecToMetadataName } from './util';
|
||||
|
||||
export class DefaultLocationService implements LocationService {
|
||||
constructor(
|
||||
@@ -37,36 +38,8 @@ export class DefaultLocationService implements LocationService {
|
||||
dryRun: boolean,
|
||||
): Promise<{ location: Location; entities: Entity[] }> {
|
||||
if (dryRun) {
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: `${spec.type}:${spec.target}`,
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
[LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`,
|
||||
[ORIGIN_LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
location: { type: spec.type, target: spec.target },
|
||||
},
|
||||
};
|
||||
const processed = await this.orchestrator.process({
|
||||
entity,
|
||||
eager: true,
|
||||
state: new Map(),
|
||||
});
|
||||
if (processed.ok) {
|
||||
return {
|
||||
location: { ...spec, id: `${spec.type}:${spec.target}` },
|
||||
entities: [processed.completedEntity],
|
||||
};
|
||||
}
|
||||
|
||||
throw Error('error handling not implemented.');
|
||||
return this.dryRunCreateLocation(spec);
|
||||
}
|
||||
|
||||
const location = await this.store.createLocation(spec);
|
||||
return { location, entities: [] };
|
||||
}
|
||||
@@ -80,4 +53,53 @@ export class DefaultLocationService implements LocationService {
|
||||
deleteLocation(id: string): Promise<void> {
|
||||
return this.store.deleteLocation(id);
|
||||
}
|
||||
|
||||
private async dryRunCreateLocation(
|
||||
spec: LocationSpec,
|
||||
): Promise<{ location: Location; entities: Entity[] }> {
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: locationSpecToMetadataName({
|
||||
type: spec.type,
|
||||
target: spec.target,
|
||||
}),
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
[LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`,
|
||||
[ORIGIN_LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: spec.type,
|
||||
target: spec.target,
|
||||
},
|
||||
};
|
||||
const unprocessedEntities: Entity[] = [entity];
|
||||
const entities: Entity[] = [];
|
||||
const state = new Map(); // ignored
|
||||
while (unprocessedEntities.length) {
|
||||
const currentEntity = unprocessedEntities.pop();
|
||||
if (!currentEntity) {
|
||||
continue;
|
||||
}
|
||||
const processed = await this.orchestrator.process({
|
||||
entity: currentEntity,
|
||||
state,
|
||||
});
|
||||
|
||||
if (processed.ok) {
|
||||
unprocessedEntities.push(...processed.deferredEntities);
|
||||
entities.push(processed.completedEntity);
|
||||
} else {
|
||||
throw Error(processed.errors.map(String).join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
location: { ...spec, id: `${spec.type}:${spec.target}` },
|
||||
entities,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ export async function createNextRouter(
|
||||
res.status(201).json(output);
|
||||
})
|
||||
.get('/locations', async (_req, res) => {
|
||||
const output = await locationService.listLocations();
|
||||
res.status(200).json(output);
|
||||
const locations = await locationService.listLocations();
|
||||
res.status(200).json(locations.map(l => ({ data: l })));
|
||||
})
|
||||
|
||||
.get('/locations/:id', async (req, res) => {
|
||||
|
||||
@@ -59,7 +59,6 @@ export interface EntityProvider {
|
||||
|
||||
export type EntityProcessingRequest = {
|
||||
entity: Entity;
|
||||
eager?: boolean;
|
||||
state: Map<string, JsonObject>; // Versions for multiple deployments etc
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user