Merge branch 'master' of github.com:codingdiaz/backstage into feat/tools-collator-auth

This commit is contained in:
Christopher Diaz
2023-05-18 08:47:41 -04:00
44 changed files with 1388 additions and 391 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
fixed refresh problem when backstage backend disconnects without any feedback to user. Now we send a generic message and try to reconnect after 15 seconds
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Forward `Authorization` header for `EventSource` when credentials are available.
+16
View File
@@ -0,0 +1,16 @@
---
'@backstage/plugin-linguist-backend': minor
---
**BREAKING**: Removed public constructor from `LinguistBackendApi`. Removed export of `LinguistBackendDatabase` and `LinguistBackendStore`
Several improvements to the Linguist backend have been made:
- Added tests for the `LinguistBackendDatabase` and `LinguistBackendApi`
- Added support for using SQLite as a database, helpful for local development
- Removed the default from the `processes_date` column
- Converted the `LinguistBackendApi` into an Interface
- Added the `LinguistBackendClient` which implements the `LinguistBackendApi` Interface
- Unprocessed entities will get processed before stale entities
- Entities in the Linguist database but not in the Catalog anymore will be deleted
- Improved the README's headings
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Change deprecated local references to import from shared `plugin-techdocs-react` plugin
+3
View File
@@ -316,6 +316,7 @@ scaffolded
scaffolder
Scaffolder
scrollbar
sdks
seb
semlas
semver
@@ -343,6 +344,8 @@ stdout
storable
stringified
stringify
subcommand
subcommands
subcomponent
subcomponents
subfolder
@@ -82,10 +82,20 @@ export const createAuthMiddleware = async (
};
```
Install cookie-parser:
```bash
# From your Backstage root directory
yarn add --cwd packages/backend cookie-parser
```
Update routes in `packages/backend/src/index.ts`:
```typescript
// packages/backend/src/index.ts from a create-app deployment
import { createAuthMiddleware } from './authMiddleware';
import cookieParser from 'cookie-parser';
// ...
+2
View File
@@ -243,6 +243,8 @@ integrations:
token: ${GITHUB_TOKEN} # this will use the environment variable GITHUB_TOKEN
```
> Note: If you've updated the configuration for your integration, it's likely that the backend will need a restart to apply these changes. To do this, stop the running instance in your terminal with `Control-C`, then start it again with `yarn dev`. Once the backend has restarted, retry the operation.
Some helpful links, for if you want to learn more about:
- [Other available integrations](../integrations/)
+5 -5
View File
@@ -4,7 +4,7 @@
### Minor Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
### Patch Changes
@@ -61,7 +61,7 @@
### Minor Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- c15e0cedbe1: The `AuthConnector` interface now supports specifying a set of scopes when
refreshing a session. The `DefaultAuthConnector` implementation passes the
`scope` query parameter to the auth-backend plugin appropriately. The
@@ -106,7 +106,7 @@
- 67140d9f96f: Upgrade `react-virtualized-auto-sizer´ to version `^1.0.11\`
- 6e0b71493df: Switched internal declaration of `DependencyGraphTypes` to use `namespace`.
- c8779cc1d09: Updated `LogLine` component, which is used by the `LogViewer`, to turn URLs into clickable links. This feature is on by default
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 1e4f5e91b8e: Bump `zod` and `zod-to-json-schema` dependencies.
- 29ba8267d69: Updated dependency `@material-ui/lab` to `4.0.0-alpha.61`.
- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
@@ -170,7 +170,7 @@
### Minor Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
### Patch Changes
@@ -1245,7 +1245,7 @@
- d8f774c30df: Enforce the secret visibility of certificates and client secrets in the auth backend. Also, document all known options for each auth plugin.
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 475abd1dc3f: The `microsoft` (i.e. Azure) auth provider now supports negotiating tokens for
Azure resources besides Microsoft Graph (e.g. AKS, Virtual Machines, Machine
+5 -5
View File
@@ -4,7 +4,7 @@
### Minor Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
### Patch Changes
@@ -19,7 +19,7 @@
### Minor Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
### Patch Changes
@@ -33,7 +33,7 @@
### Minor Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
### Patch Changes
@@ -336,7 +336,7 @@
### Patch Changes
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
- 7245e744ab1: Fixed the font color on `BackstageHeaderLabel` to respect the active page theme.
- Updated dependencies
@@ -530,7 +530,7 @@
### Patch Changes
- d8f774c30df: Enforce the secret visibility of certificates and client secrets in the auth backend. Also, document all known options for each auth plugin.
- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- 7908d72e033: Introduce a new global config parameter, `enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
- Updated dependencies
- @backstage/backend-common@0.18.4-next.0
- @backstage/config@1.0.7
+1 -1
View File
@@ -86,7 +86,7 @@ Contributed by [@sennyeya](https://github.com/sennyeya) in [#15667](https://gith
### Experimental inline auth flows
Theres new support for running auth flows inline in the current page instead of in a separate popup window, enabled via the `auth.enableExperimentalRedirectFlow` app-config parameter. This will allow the use of some providers that do not support popup flows. Its still an experimental feature, but do try it out if this applies to you!
Theres new support for running auth flows inline in the current page instead of in a separate popup window, enabled via the `enableExperimentalRedirectFlow` app-config parameter. This will allow the use of some providers that do not support popup flows. Its still an experimental feature, but do try it out if this applies to you!
Contributed by [@headphonejames](https://github.com/headphonejames) in [#15841](https://github.com/backstage/backstage/pull/15841)
+80
View File
@@ -0,0 +1,80 @@
---
id: v1.14.0
title: v1.14.0
description: Backstage Release v1.14.0
---
These are the release notes for the v1.14.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for their hard work in getting this release developed and done.
## Highlights
This release has an important security fix, along with a lot of squashed bugs and exciting additions! Enjoy.
### **BREAKING**: Tweaks to the OpenAPI tooling
Theres been further work made on the OpenAPI tooling! One of the changes is that the commands for this feature are grouped under a `schema openapi` subcommand. This lets us structure things a bit better for the future. The generated file now also has a `.generated.ts` extension, to more easily be able to keep it apart in linting and similar.
### **BREAKING**: Kubernetes plugin log viewer and tweaks
Theres been some awesome additions made to the Kubernetes plugin, including the ability to show pod logs! To support this, some breaking changes were made in some of its interfaces. Check out [#17120](https://github.com/backstage/backstage/pull/17120) for some details of what changed.
### **DEPRECATION**: React 16
We are preparing for React 18, and as part of doing that, we are now officially deprecating support for React 16. Nothing will break for you just yet, but if you are still on React 16 your `backstage-cli` commands will start to show helpful warning messages guiding you to bump to version 17 when you can.
See [#17752](https://github.com/backstage/backstage/pull/17752) for some details about this, and links to the relevant issues.
### New plugin: DevTools
This plugin is focused on integrators rather than end users. It allows you to see useful information about your Backstage installation, such as what operating system and NodeJS version its running on, what Backstage and individual package versions you have, and more! This may serve as a foundation for adding even more ops focused features in the future.
Contributed by [@awanlin](https://github.com/awanlin) in [#17393](https://github.com/backstage/backstage/pull/17393)
### Migration to AWS-SDK version 3
Theres been work done under the hood to migrate AWS features over to using v3 of their client SDKs. While this should mostly go below the radar and ultimately work the same as it did before, do reach out to us if you encounter any problems that might be related to this migration.
Theres actually a minor interface breakage as part of this, where `AwsIamKubernetesAuthTranslator` has some methods removed, but thats one which may not have a lot of use outside of the package.
Contributed by [@aochsner](https://github.com/aochsner), see PRs linked at the bottom of issue [#16470](https://github.com/backstage/backstage/issues/16470)
### Persistent session store for the auth backend
For auth providers that use session storage, those sessions are now persisted in the auth backend database, instead of in local memory. This should make session handling work better across scaled auth backend instances.
### Markdown Output Support for Software Templates
You can now output Markdown from the Software Template runs which can provide more data to the end user after a job has completed in the outputs section of `scaffolder/next`
Contributed by [@voximity](https://github.com/voximity) in [#17641](https://github.com/backstage/backstage/pull/17641)
### Refactored Configuration Loading
The configuration loading system has been refactored to make it easier to extend the system with additional sources of configuration. See the [initial PR](https://github.com/backstage/backstage/pull/17209) and the [`@backstage/config-loader` changelog](https://github.com/backstage/backstage/blob/master/packages/config-loader/CHANGELOG.md#130) for more details.
### Catalog database performance tweaks
We found some hotspots in the catalog database operations, and streamlined them. Hopefully this should be noticeable in the form of slightly lower CPU and IOPS usage.
## Security Fixes
A [security flaw in the `vm2` package](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-32314) used by the Scaffolder plugin was patched by our automated security processes in [#17810](https://github.com/backstage/backstage/pull/17810). The fixed version of `vm2` was already covered by the required version range of the Scaffolder plugin, so adopters are able to address this vulnerability separately without upgrading Backstage itself, as part of their regular security processes.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.14.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://mailchi.mp/spotify/backstage-community) if you want to be informed about what is happening in the world of Backstage.
+10
View File
@@ -0,0 +1,10 @@
---
title: APIs with 3scale
author: Red Hat
authorUrl: https://redhat.com
category: Discovery
description: Synchronize 3scale content into the Backstage catalog.
documentation: https://janus-idp.io/plugins/3scale
iconUrl: http://janus-idp.io/images/plugins/3scale.svg
npmPackageName: '@janus-idp/backstage-plugin-3scale-backend'
addedDate: '2023-05-15'
@@ -0,0 +1,10 @@
---
title: Container Image Registry for JFrog Artifactory
author: Red Hat
authorUrl: https://redhat.com
category: Image
description: View container image details from JFrog Artifactory in Backstage.
documentation: https://janus-idp.io/plugins/jfrog-artifactory
iconUrl: http://janus-idp.io/images/plugins/jfrog-artifactory.svg
npmPackageName: '@janus-idp/backstage-plugin-jfrog-artifactory'
addedDate: '2023-05-15'
+10
View File
@@ -0,0 +1,10 @@
---
title: Authentication and Authorization with Keycloak
author: Red Hat
authorUrl: https://redhat.com
category: Authentication
description: Load users and groups from Keycloak, enabling use of multiple authentication providers to be applied to Backstage entities.
documentation: https://janus-idp.io/plugins/keycloak
iconUrl: http://janus-idp.io/images/plugins/keycloak.svg
npmPackageName: '@janus-idp/backstage-plugin-keycloak-backend'
addedDate: '2023-05-15'
+10
View File
@@ -0,0 +1,10 @@
---
title: Multi Cluster View with OCM
author: Red Hat
authorUrl: https://redhat.com
category: OCM
description: View clusters from OCM's MultiClusterHub and MultiCluster Engine in Backstage.
documentation: https://janus-idp.io/plugins/ocm
iconUrl: http://janus-idp.io/images/plugins/ocm.svg
npmPackageName: '@janus-idp/backstage-plugin-ocm'
addedDate: '2023-05-15'
+10
View File
@@ -0,0 +1,10 @@
---
title: Container Image Registry for Quay
author: Red Hat
authorUrl: https://redhat.com
category: Image
description: View container image details from Quay in Backstage.
documentation: https://janus-idp.io/plugins/quay
iconUrl: http://janus-idp.io/images/plugins/quay.svg
npmPackageName: '@janus-idp/backstage-plugin-quay'
addedDate: '2023-05-15'
+10
View File
@@ -0,0 +1,10 @@
---
title: Pipelines with Tekton
author: Red Hat
authorUrl: https://redhat.com
category: CI
description: Easily view Tekton PipelineRun status for your services in Backstage.
documentation: https://janus-idp.io/plugins/tekton
iconUrl: http://janus-idp.io/images/plugins/tekton.svg
npmPackageName: '@janus-idp/backstage-plugin-tekton'
addedDate: '2023-05-15'
+10
View File
@@ -0,0 +1,10 @@
---
title: Application Topology for Kubernetes
author: Red Hat
authorUrl: https://redhat.com
category: Kubernetes
description: Visualize the deployment status and related resources of your applications deployed on any Kubernetes cluster.
documentation: https://janus-idp.io/plugins/topology
iconUrl: http://janus-idp.io/images/plugins/topology.svg
npmPackageName: '@janus-idp/backstage-plugin-topology'
addedDate: '2023-05-15'
+1 -1
View File
@@ -173,7 +173,7 @@ module.exports = {
position: 'left',
},
{
to: 'docs/releases/v1.12.0',
to: 'docs/releases/v1.14.0',
label: 'Releases',
position: 'left',
},
+4 -4
View File
@@ -17,7 +17,7 @@
"docusaurus": "docusaurus"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "0.0.0-5569",
"@docusaurus/module-type-aliases": "0.0.0-5580",
"@spotify/prettier-config": "^14.0.0",
"@tsconfig/docusaurus": "^1.0.6",
"@types/webpack-env": "^1.18.0",
@@ -28,9 +28,9 @@
},
"prettier": "@spotify/prettier-config",
"dependencies": {
"@docusaurus/core": "0.0.0-5569",
"@docusaurus/plugin-client-redirects": "0.0.0-5569",
"@docusaurus/preset-classic": "0.0.0-5569",
"@docusaurus/core": "0.0.0-5580",
"@docusaurus/plugin-client-redirects": "0.0.0-5580",
"@docusaurus/preset-classic": "0.0.0-5580",
"@swc/core": "^1.3.46",
"clsx": "^1.1.1",
"docusaurus-plugin-sass": "^0.2.3",
+1
View File
@@ -1,6 +1,7 @@
{
"releases": {
"Release Notes": [
"releases/v1.14.0",
"releases/v1.13.0",
"releases/v1.12.0",
"releases/v1.11.0",
+279 -191
View File
@@ -1620,9 +1620,9 @@ __metadata:
languageName: node
linkType: hard
"@docusaurus/core@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/core@npm:0.0.0-5569"
"@docusaurus/core@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/core@npm:0.0.0-5580"
dependencies:
"@babel/core": ^7.20.12
"@babel/generator": ^7.21.1
@@ -1634,13 +1634,13 @@ __metadata:
"@babel/runtime": ^7.21.0
"@babel/runtime-corejs3": ^7.21.0
"@babel/traverse": ^7.21.2
"@docusaurus/cssnano-preset": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/cssnano-preset": 0.0.0-5580
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/react-loadable": 5.5.2
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-common": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-common": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
"@slorber/static-site-generator-webpack-plugin": ^4.0.7
"@svgr/webpack": ^6.5.1
autoprefixer: ^10.4.13
@@ -1700,41 +1700,41 @@ __metadata:
react-dom: ^16.8.4 || ^17.0.0
bin:
docusaurus: bin/docusaurus.mjs
checksum: 573746ba85750381b110a98cfb529d8c961523b735c1945016bf8f79ba2cde38d08ecda88dc1be4852d8c9a49121e23ec964b05354fd6f6d26e6f31e71f175c1
checksum: 1160559109aa834bcd3dc0210ca824ad6cbdea363951c5cba690c1882c78a7da6e1e52b8a5c35fef3bfc23f9f66e84880c354727b7987d371cac8ec6fb77d1bb
languageName: node
linkType: hard
"@docusaurus/cssnano-preset@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5569"
"@docusaurus/cssnano-preset@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5580"
dependencies:
cssnano-preset-advanced: ^5.3.10
postcss: ^8.4.21
postcss-sort-media-queries: ^4.3.0
tslib: ^2.5.0
checksum: 11408a0f9eb1a4a83b2045a0172ef1fbf0523a99fa9d81a8efe9a3a968b449887616ad8d95b0abb55b2bf82a178193c20c0304942c4f2ba51b693b9209031204
checksum: 4a46af1e9327d6ebbed3f0f6e60cc1ca59d5c07d39be35c42c13b0a64795b0b1565585adfca063798be79100aca425ee0c12f28a816b56686fb97726bec00f40
languageName: node
linkType: hard
"@docusaurus/logger@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/logger@npm:0.0.0-5569"
"@docusaurus/logger@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/logger@npm:0.0.0-5580"
dependencies:
chalk: ^4.1.2
tslib: ^2.5.0
checksum: 26be75d7180d9b9d525ce5014e1e24c69fd54d1707cabc5acc76b47e3282cfb185613fb4d27f32b5cced92fe80384c68e2d991cf4cabbecca331f570c3e10d9b
checksum: 5792b6672623b70d38871713c47ba88cc0df6c8d224c28ac58d0512b8bf1b672786cc2fa0b1a79cc045c473e1e14b5a9cce4b01e623dd252893294d6aff9d1c2
languageName: node
linkType: hard
"@docusaurus/mdx-loader@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/mdx-loader@npm:0.0.0-5569"
"@docusaurus/mdx-loader@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/mdx-loader@npm:0.0.0-5580"
dependencies:
"@babel/parser": ^7.21.2
"@babel/traverse": ^7.21.2
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
"@mdx-js/mdx": ^2.1.5
escape-html: ^1.0.3
estree-util-value-to-estree: ^2.1.0
@@ -1744,6 +1744,7 @@ __metadata:
image-size: ^1.0.2
mdast-util-mdx: ^2.0.0
mdast-util-to-string: ^3.0.0
rehype-raw: ^6.1.1
remark-comment: ^1.0.0
remark-directive: ^2.0.1
remark-emoji: ^2.2.0
@@ -1757,16 +1758,16 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: bcf3b58ea3840d648ddc81c090c9905d559f642d8dc495f3fa86b377ae0bdabb6e1486ae2b258efa459a8bcf1da729a4a3a0c144f0de5dc083155b6d5c83099c
checksum: b32812e0bca91a29761fda742a790f67077bb7e8860e20e83f009e297ca49017192fa14051ab82f1a3e66800de9e5a0b561b32ce2dffb4eecc0aee48116d3490
languageName: node
linkType: hard
"@docusaurus/module-type-aliases@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5569"
"@docusaurus/module-type-aliases@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5580"
dependencies:
"@docusaurus/react-loadable": 5.5.2
"@docusaurus/types": 0.0.0-5569
"@docusaurus/types": 0.0.0-5580
"@types/history": ^4.7.11
"@types/react": "*"
"@types/react-router-config": "*"
@@ -1776,19 +1777,19 @@ __metadata:
peerDependencies:
react: "*"
react-dom: "*"
checksum: 08168317c606e44f8ead775bfc23feaeb3a03178a19a07504130a32cbf189926c08ef40cdc4f00a6df0983383ca0145a42df9112451b15a19067d8b6b10d5f89
checksum: b5b3544f2f2d395c1d406a1b3f03f719aee3aa3fd7603c54af3b09c4fb8bb153cf2ee6697bc9cfc9fb2550a11a3e474a52c9f2c132924e2be60662ffc6379ace
languageName: node
linkType: hard
"@docusaurus/plugin-client-redirects@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5569"
"@docusaurus/plugin-client-redirects@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-common": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-common": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
eta: ^2.0.1
fs-extra: ^11.1.0
lodash: ^4.17.21
@@ -1796,21 +1797,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: f082956089fc889ea67b89c5c19ad6cd0bc9bb35b663a701f05905786938b1302b8667c2b6d0354bcf9efd35496a5920e1871b488fbc6285d0e36cfe3187e84e
checksum: 7414d2026446862505d0c679ad3757107f28100ea76309d2a024ebe78865f2fcf3e3b85c3117361142e319362b5a5b22e6bce968763fdfd0741b6a60d2eb3f4b
languageName: node
linkType: hard
"@docusaurus/plugin-content-blog@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5569"
"@docusaurus/plugin-content-blog@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-common": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-common": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
cheerio: ^1.0.0-rc.12
feed: ^4.2.2
fs-extra: ^11.1.0
@@ -1823,21 +1824,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: b9eef1200e1b9bf1cfa7c4d4cb62b7344306d3a096c0cbfc36afd4b973a940b1bf9d6dc62124a0028bbd466313c7ee219444ebe2856588748d2fc33d1f48ae80
checksum: 0c5d37fa84298ce12b2a0f5c1dea1ee7d72d4b28c545fd081f3c3e5f93f15a980d877dcf88718aa1f4b8a03abf3d0c8c0df10d0a2ef22aac7a8a96d2786f91c2
languageName: node
linkType: hard
"@docusaurus/plugin-content-docs@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5569"
"@docusaurus/plugin-content-docs@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/module-type-aliases": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/module-type-aliases": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
"@types/react-router-config": ^5.0.6
combine-promises: ^1.1.0
fs-extra: ^11.1.0
@@ -1850,133 +1851,133 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 544d51dbb6c9037916aa8e3ed493624029ffdac0d0da70cfdb74f1d431a8da071eeeb0d567be6f63d48f9922f1594f08f950478a4ea427fae8001ff0b4ac5d83
checksum: fa58801bf0cebf055ed11adbadc3f27f7ed0ddb95174ab60bab76a21f759dc7abdfdd15f32141f7e259090c456634ee2556ac9a7daaaf7f5f6e3666d9f247d00
languageName: node
linkType: hard
"@docusaurus/plugin-content-pages@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5569"
"@docusaurus/plugin-content-pages@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
fs-extra: ^11.1.0
tslib: ^2.5.0
webpack: ^5.76.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 16fa680c069d57d9d299ef515487a773c5b89d605cfe5b843392a7bce5e59b539b1855d7e0f2e11878b3021038bff6468b5f4adb9af7dc06884d7207772c2f95
checksum: 42bf814d9bf5540b8b2f8045f1577c49d3fca4a85069865695ff5dd9fa3f935283f9d46f920c4dd0a28190cf3b342d432b0985636bae9f05280835f73aec8c87
languageName: node
linkType: hard
"@docusaurus/plugin-debug@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-debug@npm:0.0.0-5569"
"@docusaurus/plugin-debug@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-debug@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
fs-extra: ^11.1.0
react-json-view: ^1.21.3
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 0e3b8f5a92bc2b2ab193d76be6174b521238da7b4627775a852a4b4ff8690af28ab4586adbb84fc03abc6da276efc7474eed9350577b457e532defeb638d3184
checksum: dc58659adab3c859daa2e0dfc0b5b588d490a4d2bb313dcfdc9e92d559d0c4145c686b001805ad44a1f2a0f55f87524637a34844a4438cfeec479a2c24ec1c4d
languageName: node
linkType: hard
"@docusaurus/plugin-google-analytics@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5569"
"@docusaurus/plugin-google-analytics@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: a3e55d3a616009db499ca61493f9cf308c8ae012ec1ff680bff20c3b0b486f8d5d9552fd5c3d7b198e6c72d8e13ace7808d978bd8d56496953d2fd51d5ce0c32
checksum: f66a737a2b8b08648f28a279784e527fff34e2dba6f3022ec83b01a7f1781bfa0f208fac1db1fc919c1a01d405755c0816304cf4912e7b434b3924d5a968e6b0
languageName: node
linkType: hard
"@docusaurus/plugin-google-gtag@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5569"
"@docusaurus/plugin-google-gtag@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
"@types/gtag.js": ^0.0.12
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: d0a7f58bea6d5bfd786120ab6c75ab10a4d67bae1d60aadedba649a516f2f9e49237a96a7764f964c79084ae500b9520761cbf6aab60bd5e43bb976f04dd3194
checksum: 6b05e7c2a8e4d757660acca1131a0e3abfd21c5544a31e93bd08ff9c75deb5f3b131ed0e47283bbc2f102e00947ab7a64ee4d5a53f300e98fc82ec13c4a6eda1
languageName: node
linkType: hard
"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5569"
"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: acafad7eea5ba06c62addcef4f3cfaa6c43193827ec4b6c2d39ed2799df5301defa8ac8c73d49e35c328bd118c78412745f088764d97eac00da090be8e7906e5
checksum: 8904b150b2faa05dc49fd957ca8c392605e53073d031dfedb098be2abae924c3faf4f5e26ecfea91fe452695699801de9882a85a4371a1a35ca05fd57de232ae
languageName: node
linkType: hard
"@docusaurus/plugin-sitemap@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5569"
"@docusaurus/plugin-sitemap@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-common": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-common": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
fs-extra: ^11.1.0
sitemap: ^7.1.1
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 53fd5944a1cfbbe252e3f4398acf8aea3605ca3c5fb9ec48700ce6c162744bae541ccecd1d71e884777dfc2e98c6fa9a01b0f98829e6018e59003c3d4f4a1612
checksum: 4243cf4ed649d8e9a29b122ce23f1a6194cc5d7fc244e33326320315d8ff2756b18b9d85a0f0d49df33b2e68224c1b747e87ad5fa9944e7572bc761f641c5b77
languageName: node
linkType: hard
"@docusaurus/preset-classic@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/preset-classic@npm:0.0.0-5569"
"@docusaurus/preset-classic@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/preset-classic@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/plugin-content-blog": 0.0.0-5569
"@docusaurus/plugin-content-docs": 0.0.0-5569
"@docusaurus/plugin-content-pages": 0.0.0-5569
"@docusaurus/plugin-debug": 0.0.0-5569
"@docusaurus/plugin-google-analytics": 0.0.0-5569
"@docusaurus/plugin-google-gtag": 0.0.0-5569
"@docusaurus/plugin-google-tag-manager": 0.0.0-5569
"@docusaurus/plugin-sitemap": 0.0.0-5569
"@docusaurus/theme-classic": 0.0.0-5569
"@docusaurus/theme-common": 0.0.0-5569
"@docusaurus/theme-search-algolia": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/plugin-content-blog": 0.0.0-5580
"@docusaurus/plugin-content-docs": 0.0.0-5580
"@docusaurus/plugin-content-pages": 0.0.0-5580
"@docusaurus/plugin-debug": 0.0.0-5580
"@docusaurus/plugin-google-analytics": 0.0.0-5580
"@docusaurus/plugin-google-gtag": 0.0.0-5580
"@docusaurus/plugin-google-tag-manager": 0.0.0-5580
"@docusaurus/plugin-sitemap": 0.0.0-5580
"@docusaurus/theme-classic": 0.0.0-5580
"@docusaurus/theme-common": 0.0.0-5580
"@docusaurus/theme-search-algolia": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: a20013db1bd1c77e6fc2b901326d62f3003b6f6e6191f26aae6968cc2402837c0e84cc54571849d2af811fdae74d9359c0e96775d61b9f3efbbe881898e7f9a3
checksum: 3aa48aea40e33c2c7adcad54918e117b1f1d917eba110889d85a273e82b4bbf0d73d9e5bb8c5aaadd846ec341981bbe701014cbd93ddf55c83245ce1bc4a56a6
languageName: node
linkType: hard
@@ -1992,22 +1993,22 @@ __metadata:
languageName: node
linkType: hard
"@docusaurus/theme-classic@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/theme-classic@npm:0.0.0-5569"
"@docusaurus/theme-classic@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/theme-classic@npm:0.0.0-5580"
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/module-type-aliases": 0.0.0-5569
"@docusaurus/plugin-content-blog": 0.0.0-5569
"@docusaurus/plugin-content-docs": 0.0.0-5569
"@docusaurus/plugin-content-pages": 0.0.0-5569
"@docusaurus/theme-common": 0.0.0-5569
"@docusaurus/theme-translations": 0.0.0-5569
"@docusaurus/types": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-common": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/module-type-aliases": 0.0.0-5580
"@docusaurus/plugin-content-blog": 0.0.0-5580
"@docusaurus/plugin-content-docs": 0.0.0-5580
"@docusaurus/plugin-content-pages": 0.0.0-5580
"@docusaurus/theme-common": 0.0.0-5580
"@docusaurus/theme-translations": 0.0.0-5580
"@docusaurus/types": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-common": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
"@mdx-js/react": ^2.1.5
clsx: ^1.2.1
copy-text-to-clipboard: ^3.0.1
@@ -2024,21 +2025,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: ee25082a4abc0d812cc54ee1bea642766f903cfa47d28c6a6efbc53cdaacf67bee594a4745c7ae97fe3ba6ddb136de36dd0c8cda2c310c5b40c9023bee70fcbb
checksum: 6a8f99d5143017d4754601fbf6487b697dc77945f730763917578abe0a1ef694607f888d824146aef86fdc2a620f0ddbd4925a23dcfc15b57d8338c1de73d526
languageName: node
linkType: hard
"@docusaurus/theme-common@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/theme-common@npm:0.0.0-5569"
"@docusaurus/theme-common@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/theme-common@npm:0.0.0-5580"
dependencies:
"@docusaurus/mdx-loader": 0.0.0-5569
"@docusaurus/module-type-aliases": 0.0.0-5569
"@docusaurus/plugin-content-blog": 0.0.0-5569
"@docusaurus/plugin-content-docs": 0.0.0-5569
"@docusaurus/plugin-content-pages": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-common": 0.0.0-5569
"@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/module-type-aliases": 0.0.0-5580
"@docusaurus/plugin-content-blog": 0.0.0-5580
"@docusaurus/plugin-content-docs": 0.0.0-5580
"@docusaurus/plugin-content-pages": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-common": 0.0.0-5580
"@types/history": ^4.7.11
"@types/react": "*"
"@types/react-router-config": "*"
@@ -2051,22 +2052,22 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 26a2d7dd6fd8b97e67db3bc093fcb9ce6f14d561aeccf94c3334d18a9a2900483dcb0ec4bb28d31e910bea459f20e4445207d5156c9cc02bd72601eb0256e3cc
checksum: 99524cb36a520568f63140734daaea8d56b35c0c8de3c36cb0102a93a0714f31a68aa5302607b9113a68e1b9fb7d73fd53ac80aa82ce48bf6f2d9e304ce21da2
languageName: node
linkType: hard
"@docusaurus/theme-search-algolia@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5569"
"@docusaurus/theme-search-algolia@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5580"
dependencies:
"@docsearch/react": ^3.3.3
"@docusaurus/core": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/plugin-content-docs": 0.0.0-5569
"@docusaurus/theme-common": 0.0.0-5569
"@docusaurus/theme-translations": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/utils-validation": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/plugin-content-docs": 0.0.0-5580
"@docusaurus/theme-common": 0.0.0-5580
"@docusaurus/theme-translations": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
"@docusaurus/utils-validation": 0.0.0-5580
algoliasearch: ^4.15.0
algoliasearch-helper: ^3.12.0
clsx: ^1.2.1
@@ -2078,23 +2079,23 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 16178b7b9278c91eaedbb1629192dc2255939de6f509b721305c4c1795283e0fd4f859b305ce051ec5efdbe71884ceede6239445916b60ebec1858e3b5161d8f
checksum: efc6054ad979b9fde43bec8befeb41c0f9ca609832b51e4da42fdcdfd7e4ee599fd3e6949c576d9ff348c0ffd044a4dbb07638a1990f57586f493ba394369c96
languageName: node
linkType: hard
"@docusaurus/theme-translations@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/theme-translations@npm:0.0.0-5569"
"@docusaurus/theme-translations@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/theme-translations@npm:0.0.0-5580"
dependencies:
fs-extra: ^11.1.0
tslib: ^2.5.0
checksum: 695f94ddc66d54b649042f0fb0a88b692613d10898ee3306c6c9b52340373493ac9bcceec9263332454d7c9179339517e40b083e949510f9e48ec834bc61fbb3
checksum: 1555956d2a7cf3122ded374937559ab19e6a6620fd0910d6bdbf48be78a10e595cfd9a0b9157c129f36f0bfae4297f07991f071a394c87cb9d2facee04597dd2
languageName: node
linkType: hard
"@docusaurus/types@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/types@npm:0.0.0-5569"
"@docusaurus/types@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/types@npm:0.0.0-5580"
dependencies:
"@types/history": ^4.7.11
"@types/react": "*"
@@ -2107,13 +2108,13 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
checksum: 956b7af3a75b3632d1ff330d018353b38b22b3f4af8b39f452fcde33d74c70312dadd3e3bb40e04ecbe02e2462a67bfcbd4ef631462a9c710616c83186930b69
checksum: 5df67f562c5d516eb020ecf63e6343e1d6d18e2716b1b530a24c5d63b48229623222861c1a69f0e4715c34a768f7c05599340f1af18ca1259709467fb1149af9
languageName: node
linkType: hard
"@docusaurus/utils-common@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/utils-common@npm:0.0.0-5569"
"@docusaurus/utils-common@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/utils-common@npm:0.0.0-5580"
dependencies:
tslib: ^2.5.0
peerDependencies:
@@ -2121,28 +2122,28 @@ __metadata:
peerDependenciesMeta:
"@docusaurus/types":
optional: true
checksum: 7d98e31beea34be91de7fdc971033439e412f535617707c79c4d2c0653c769ff4ce7521bd87f038811ccbae3b1d5c0e5d26eab9935ee8bc3515b926cbe175365
checksum: 238b2f12607834164fa82f9a3154703b75609719888cf96f06c9f70c17ab76a46babcfd66dce4f2722ecaaf47742a40a606101f0578f2345845fa23b6f262b5d
languageName: node
linkType: hard
"@docusaurus/utils-validation@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/utils-validation@npm:0.0.0-5569"
"@docusaurus/utils-validation@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/utils-validation@npm:0.0.0-5580"
dependencies:
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/utils": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5580
"@docusaurus/utils": 0.0.0-5580
joi: ^17.8.3
js-yaml: ^4.1.0
tslib: ^2.5.0
checksum: b16730923e0d6fa627ca3e3105c152e814d540badb93f0a2ae1194672a76f8269ed48ed5c8f3259c0e0b9dfc6dc939035e0ac2e675b283ac4d414c642a95c893
checksum: cf0602b2411738adc47f5596fd4a3076137aa754aca24a4630700dc6355fed07e9a66ec093e438fa1199d9076bebae036317d073a604e4f0666f8390ceb73b5d
languageName: node
linkType: hard
"@docusaurus/utils@npm:0.0.0-5569":
version: 0.0.0-5569
resolution: "@docusaurus/utils@npm:0.0.0-5569"
"@docusaurus/utils@npm:0.0.0-5580":
version: 0.0.0-5580
resolution: "@docusaurus/utils@npm:0.0.0-5580"
dependencies:
"@docusaurus/logger": 0.0.0-5569
"@docusaurus/logger": 0.0.0-5580
"@svgr/webpack": ^6.5.1
escape-string-regexp: ^4.0.0
file-loader: ^6.2.0
@@ -2163,7 +2164,7 @@ __metadata:
peerDependenciesMeta:
"@docusaurus/types":
optional: true
checksum: 33e044f9b5328cf9d1b17637fa41a6138c5e18b5f3d73389bbb9f71735a273a9e27660d9a9103766406549edfa0691e2524cc84e6ebc2b0d308991df3bee0dd9
checksum: c9b085d3724cba748b3db763f056742aace45cc58a636e23513ee6a7db781558377de5507b651b3566562fa71eebb42ac835034cdcbcc2c77edf9b4009c6a215
languageName: node
linkType: hard
@@ -2991,6 +2992,13 @@ __metadata:
languageName: node
linkType: hard
"@types/parse5@npm:^6.0.0":
version: 6.0.3
resolution: "@types/parse5@npm:6.0.3"
checksum: ddb59ee4144af5dfcc508a8dcf32f37879d11e12559561e65788756b95b33e6f03ea027d88e1f5408f9b7bfb656bf630ace31a2169edf44151daaf8dd58df1b7
languageName: node
linkType: hard
"@types/prop-types@npm:*":
version: 15.7.5
resolution: "@types/prop-types@npm:15.7.5"
@@ -3737,10 +3745,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "backstage-microsite@workspace:."
dependencies:
"@docusaurus/core": 0.0.0-5569
"@docusaurus/module-type-aliases": 0.0.0-5569
"@docusaurus/plugin-client-redirects": 0.0.0-5569
"@docusaurus/preset-classic": 0.0.0-5569
"@docusaurus/core": 0.0.0-5580
"@docusaurus/module-type-aliases": 0.0.0-5580
"@docusaurus/plugin-client-redirects": 0.0.0-5580
"@docusaurus/preset-classic": 0.0.0-5580
"@spotify/prettier-config": ^14.0.0
"@swc/core": ^1.3.46
"@tsconfig/docusaurus": ^1.0.6
@@ -6132,6 +6140,21 @@ __metadata:
languageName: node
linkType: hard
"hast-util-from-parse5@npm:^7.0.0":
version: 7.1.2
resolution: "hast-util-from-parse5@npm:7.1.2"
dependencies:
"@types/hast": ^2.0.0
"@types/unist": ^2.0.0
hastscript: ^7.0.0
property-information: ^6.0.0
vfile: ^5.0.0
vfile-location: ^4.0.0
web-namespaces: ^2.0.0
checksum: 7b4ed5b508b1352127c6719f7b0c0880190cf9859fe54ccaf7c9228ecf623d36cef3097910b3874d2fe1aac6bf4cf45d3cc2303daac3135a05e9ade6534ddddb
languageName: node
linkType: hard
"hast-util-parse-selector@npm:^3.0.0":
version: 3.1.1
resolution: "hast-util-parse-selector@npm:3.1.1"
@@ -6141,6 +6164,25 @@ __metadata:
languageName: node
linkType: hard
"hast-util-raw@npm:^7.2.0":
version: 7.2.3
resolution: "hast-util-raw@npm:7.2.3"
dependencies:
"@types/hast": ^2.0.0
"@types/parse5": ^6.0.0
hast-util-from-parse5: ^7.0.0
hast-util-to-parse5: ^7.0.0
html-void-elements: ^2.0.0
parse5: ^6.0.0
unist-util-position: ^4.0.0
unist-util-visit: ^4.0.0
vfile: ^5.0.0
web-namespaces: ^2.0.0
zwitch: ^2.0.0
checksum: 21857eea3ffb8fd92d2d9be7793b56d0b2c40db03c4cfa14828855ae41d7c584917aa83efb7157220b2e41e25e95f81f24679ac342c35145e5f1c1d39015f81f
languageName: node
linkType: hard
"hast-util-to-estree@npm:^2.0.0":
version: 2.3.2
resolution: "hast-util-to-estree@npm:2.3.2"
@@ -6164,6 +6206,20 @@ __metadata:
languageName: node
linkType: hard
"hast-util-to-parse5@npm:^7.0.0":
version: 7.1.0
resolution: "hast-util-to-parse5@npm:7.1.0"
dependencies:
"@types/hast": ^2.0.0
comma-separated-tokens: ^2.0.0
property-information: ^6.0.0
space-separated-tokens: ^2.0.0
web-namespaces: ^2.0.0
zwitch: ^2.0.0
checksum: 3a7f2175a3db599bbae7e49ba73d3e5e688e5efca7590ff50130ba108ad649f728402815d47db49146f6b94c14c934bf119915da9f6964e38802c122bcc8af6b
languageName: node
linkType: hard
"hast-util-whitespace@npm:^2.0.0":
version: 2.0.1
resolution: "hast-util-whitespace@npm:2.0.1"
@@ -6171,7 +6227,7 @@ __metadata:
languageName: node
linkType: hard
"hastscript@npm:^7.1.0":
"hastscript@npm:^7.0.0, hastscript@npm:^7.1.0":
version: 7.2.0
resolution: "hastscript@npm:7.2.0"
dependencies:
@@ -6276,6 +6332,13 @@ __metadata:
languageName: node
linkType: hard
"html-void-elements@npm:^2.0.0":
version: 2.0.1
resolution: "html-void-elements@npm:2.0.1"
checksum: 06d41f13b9d5d6e0f39861c4bec9a9196fa4906d56cd5cf6cf54ad2e52a85bf960cca2bf9600026bde16c8331db171bedba5e5a35e2e43630c8f1d497b2fb658
languageName: node
linkType: hard
"html-webpack-plugin@npm:^5.5.0":
version: 5.5.0
resolution: "html-webpack-plugin@npm:5.5.0"
@@ -8735,6 +8798,13 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^6.0.0":
version: 6.0.1
resolution: "parse5@npm:6.0.1"
checksum: 7d569a176c5460897f7c8f3377eff640d54132b9be51ae8a8fa4979af940830b2b0c296ce75e5bd8f4041520aadde13170dbdec44889975f906098ea0002f4bd
languageName: node
linkType: hard
"parse5@npm:^7.0.0":
version: 7.1.2
resolution: "parse5@npm:7.1.2"
@@ -9901,6 +9971,17 @@ __metadata:
languageName: node
linkType: hard
"rehype-raw@npm:^6.1.1":
version: 6.1.1
resolution: "rehype-raw@npm:6.1.1"
dependencies:
"@types/hast": ^2.0.0
hast-util-raw: ^7.2.0
unified: ^10.0.0
checksum: a1f9d309e609f49fb1f1e06e722705f4dd2e569653a89f756eaccb33b612cf1bb511216a81d10a619d11d047afc161e4b3cb99b957df05a8ba8fdbd5843f949a
languageName: node
linkType: hard
"relateurl@npm:^0.2.7":
version: 0.2.7
resolution: "relateurl@npm:0.2.7"
@@ -11526,6 +11607,13 @@ __metadata:
languageName: node
linkType: hard
"web-namespaces@npm:^2.0.0":
version: 2.0.1
resolution: "web-namespaces@npm:2.0.1"
checksum: b6d9f02f1a43d0ef0848a812d89c83801d5bbad57d8bb61f02eb6d7eb794c3736f6cc2e1191664bb26136594c8218ac609f4069722c6f56d9fc2d808fa9271c6
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
+2 -2
View File
@@ -117,8 +117,8 @@ export interface Config {
};
/**
$ Enable redirect authentication flow type, instead of a popup for authentication
* default value: 'false'
* Enable redirect authentication flow type, instead of a popup for authentication.
* @defaultValue false
* @visibility frontend
*/
enableExperimentalRedirectFlow?: boolean;
+9 -5
View File
@@ -57,7 +57,11 @@ Here's how to get the backend up and running:
4. Now run `yarn start-backend` from the repo root
5. Finally open `http://localhost:7007/api/linguist/health` in a browser and it should return `{"status":"ok"}`
## Batch Size
## Plugin Option
The Linguist backend has various plugin options that you can provide to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` file that will allow you to configure various aspects of how it works. The following sections go into the details of these options
### Batch Size
The Linguist backend is setup to process entities by acting as a queue where it will pull down all the applicable entities from the Catalog and add them to it's database (saving just the `entityRef`). Then it will grab the `n` oldest entities that have not been processed to determine their languages and process them. To control the batch size simply provide that to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` like this:
@@ -67,7 +71,7 @@ return createRouter({ schedule: schedule, batchSize: 40 }, { ...env });
**Note:** The default batch size is 20
## Kind
### Kind
The default setup only processes entities of kind `['API', 'Component', 'Template']`. To control the `kind` that are processed provide that to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` like this:
@@ -75,7 +79,7 @@ The default setup only processes entities of kind `['API', 'Component', 'Templat
return createRouter({ schedule: schedule, kind: ['Component'] }, { ...env });
```
## Refresh
### Refresh
The default setup will only generate the language breakdown for entities with the linguist annotation that have not been generated yet. If you want this process to also refresh the data you can do so by adding the `age` (as a `HumanDuration`) in your `packages/backend/src/plugins/linguist.ts` when you call `createRouter`:
@@ -85,7 +89,7 @@ return createRouter({ schedule: schedule, age: { days: 30 } }, { ...env });
With the `age` setup like this if the language breakdown is older than 15 days it will get regenerated. It's recommended that if you choose to use this configuration to set it to a large value - 30, 90, or 180 - as this data generally does not change drastically.
## Linguist JS options
### Linguist JS options
The default setup will use the default [linguist-js](https://www.npmjs.com/package/linguist-js) options, a full list of the available options can be found [here](https://www.npmjs.com/package/linguist-js#API).
@@ -96,7 +100,7 @@ return createRouter(
);
```
## Use Source Location
### Use Source Location
You may wish to use the `backstage.io/source-location` annotation over using the `backstage.io/linguist` as you may not be able to quickly add that annotation to your Entities. To do this you'll just need to set the `useSourceLocation` boolean to `true` in your `packages/backend/src/plugins/linguist.ts` when you call `createRouter`:
+1 -47
View File
@@ -3,16 +3,13 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { EntityResults } from '@backstage/plugin-linguist-common';
import express from 'express';
import { HumanDuration } from '@backstage/types';
import { Knex } from 'knex';
import { Languages } from '@backstage/plugin-linguist-common';
import { Logger } from 'winston';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { ProcessedEntity } from '@backstage/plugin-linguist-common';
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { TokenManager } from '@backstage/backend-common';
import { UrlReader } from '@backstage/backend-common';
@@ -24,56 +21,13 @@ export function createRouter(
): Promise<express.Router>;
// @public (undocumented)
export class LinguistBackendApi {
constructor(
logger: Logger,
store: LinguistBackendStore,
urlReader: UrlReader,
discovery: PluginEndpointDiscovery,
tokenManager: TokenManager,
age?: HumanDuration,
batchSize?: number,
useSourceLocation?: boolean,
kind?: string[],
linguistJsOptions?: Record<string, unknown>,
);
export interface LinguistBackendApi {
// (undocumented)
getEntityLanguages(entityRef: string): Promise<Languages>;
// (undocumented)
processEntities(): Promise<void>;
}
// @public (undocumented)
export class LinguistBackendDatabase implements LinguistBackendStore {
constructor(db: Knex);
// (undocumented)
static create(knex: Knex): Promise<LinguistBackendStore>;
// (undocumented)
getEntityResults(entityRef: string): Promise<Languages>;
// (undocumented)
getProcessedEntities(): Promise<ProcessedEntity[] | []>;
// (undocumented)
getUnprocessedEntities(): Promise<string[] | []>;
// (undocumented)
insertEntityResults(entityLanguages: EntityResults): Promise<string>;
// (undocumented)
insertNewEntity(entityRef: string): Promise<void>;
}
// @public (undocumented)
export interface LinguistBackendStore {
// (undocumented)
getEntityResults(entityRef: string): Promise<Languages>;
// (undocumented)
getProcessedEntities(): Promise<ProcessedEntity[] | []>;
// (undocumented)
getUnprocessedEntities(): Promise<string[] | []>;
// (undocumented)
insertEntityResults(entityLanguages: EntityResults): Promise<string>;
// (undocumented)
insertNewEntity(entityRef: string): Promise<void>;
}
// @public (undocumented)
export interface PluginOptions {
// (undocumented)
@@ -13,16 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { kindOrDefault } from './LinguistBackendApi';
describe('kindOrDefault', () => {
it('should return default kind when undefined', () => {
expect(kindOrDefault()).toEqual(['API', 'Component', 'Template']);
});
it('should return the default kind when empty', () => {
expect(kindOrDefault([])).toEqual(['API', 'Component', 'Template']);
});
it('should return provided kind when not empty', () => {
expect(kindOrDefault(['API'])).toEqual(['API']);
});
});
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
// Sqlite does not support this raw SQL
if (!knex.client.config.client.includes('sqlite3')) {
await knex.raw(
'ALTER TABLE entity_result ALTER COLUMN processed_date DROP DEFAULT;',
);
}
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
// Sqlite does not support this raw SQL
if (!knex.client.config.client.includes('sqlite3')) {
await knex.raw(
'ALTER TABLE entity_result ALTER COLUMN processed_date SET DEFAULT now();',
);
}
};
+1
View File
@@ -44,6 +44,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
"msw": "^1.0.0",
@@ -0,0 +1,416 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
getVoidLogger,
ReadTreeResponse,
ServerTokenManager,
UrlReader,
} from '@backstage/backend-common';
import { CatalogApi, GetEntitiesResponse } from '@backstage/catalog-client';
import { Results } from 'linguist-js/dist/types';
import { DateTime } from 'luxon';
import { LinguistBackendStore } from '../db';
import { kindOrDefault, LinguistBackendClient } from './LinguistBackendClient';
import fs from 'fs-extra';
import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common';
const linguistResultMock = Promise.resolve({
files: {
count: 4,
bytes: 6010,
results: {
'/src/index.ts': 'TypeScript',
'/src/cli.js': 'JavaScript',
'/readme.md': 'Markdown',
'/no-lang': null,
},
},
languages: {
count: 3,
bytes: 6000,
results: {
JavaScript: { type: 'programming', bytes: 1000, color: '#f1e05a' },
TypeScript: { type: 'programming', bytes: 2000, color: '#2b7489' },
Markdown: { type: 'prose', bytes: 3000, color: '#083fa1' },
},
},
unknown: {
count: 1,
bytes: 10,
filenames: {
'no-lang': 10,
},
extensions: {},
},
} as Results);
describe('kindOrDefault', () => {
it('should return default kind when undefined', () => {
expect(kindOrDefault()).toEqual(['API', 'Component', 'Template']);
});
it('should return the default kind when empty', () => {
expect(kindOrDefault([])).toEqual(['API', 'Component', 'Template']);
});
it('should return provided kind when not empty', () => {
expect(kindOrDefault(['API'])).toEqual(['API']);
});
});
describe('Linguist backend API', () => {
const logger = getVoidLogger();
const store: jest.Mocked<LinguistBackendStore> = {
insertEntityResults: jest.fn(),
insertNewEntity: jest.fn(),
getEntityResults: jest.fn(),
getProcessedEntities: jest.fn(),
getUnprocessedEntities: jest.fn(),
getAllEntities: jest.fn(),
deleteEntity: jest.fn(),
};
const urlReader: jest.Mocked<UrlReader> = {
readTree: jest.fn(),
search: jest.fn(),
readUrl: jest.fn(),
};
const catalogApi: jest.Mocked<CatalogApi> = {
getEntities: jest.fn(),
getEntityByRef: jest.fn(),
} as any;
const tokenManager = ServerTokenManager.noop();
const api = new LinguistBackendClient(
logger,
store,
urlReader,
tokenManager,
catalogApi,
);
beforeEach(() => {
jest.resetAllMocks();
});
it('should get languages for an entity', async () => {
store.getEntityResults.mockResolvedValue({
languageCount: 1,
totalBytes: 2205,
processedDate: '2023-02-15T20:10:21.378Z',
breakdown: [
{
name: 'YAML',
percentage: 100,
bytes: 2205,
type: 'data',
color: '#cb171e',
},
],
});
const entityRef = 'template:default/create-react-app-template';
const languages = await api.getEntityLanguages(entityRef);
expect(languages).toEqual({
languageCount: 1,
totalBytes: 2205,
processedDate: '2023-02-15T20:10:21.378Z',
breakdown: [
{
name: 'YAML',
percentage: 100,
bytes: 2205,
type: 'data',
color: '#cb171e',
},
],
});
});
it('should insert new entities', async () => {
const testEntityListResponse: GetEntitiesResponse = {
items: [
{
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: 'service-one',
},
kind: 'Component',
},
{
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: 'service-two',
},
kind: 'Component',
},
{
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: 'service-three',
},
kind: 'Component',
},
],
};
catalogApi.getEntities.mockResolvedValue(testEntityListResponse);
await api.addNewEntities();
expect(store.insertNewEntity).toHaveBeenCalledTimes(3);
});
it('should delete entities not in Catalog', async () => {
store.getAllEntities.mockResolvedValue([
'component:default/service-one',
'component:default/stale-service-two',
]);
catalogApi.getEntityByRef.mockResolvedValueOnce({
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: 'service-one',
},
kind: 'Component',
});
await api.cleanEntities();
expect(store.deleteEntity).toHaveBeenCalledTimes(1);
});
it('should get default entity overview', async () => {
store.getProcessedEntities.mockResolvedValue([
{
entityRef: 'component:default/service-one',
processedDate: DateTime.now().toJSDate(),
},
{
entityRef: 'component:default/stale-service-two',
processedDate: DateTime.now().minus({ days: 45 }).toJSDate(),
},
]);
store.getUnprocessedEntities.mockResolvedValue([
'component:default/service-three',
'component:default/service-four',
'component:default/service-five',
]);
const overview = await api.getEntitiesOverview();
expect(overview.entityCount).toEqual(5);
expect(overview.processedCount).toEqual(2);
expect(overview.staleCount).toEqual(0);
expect(overview.pendingCount).toEqual(3);
expect(overview.filteredEntities).toEqual([
'component:default/service-three',
'component:default/service-four',
'component:default/service-five',
]);
});
it('should get entity overview with stale items', async () => {
const apiWithAge = new LinguistBackendClient(
logger,
store,
urlReader,
tokenManager,
catalogApi,
{ days: 5 },
);
store.getProcessedEntities.mockResolvedValue([
{
entityRef: 'component:default/service-one',
processedDate: DateTime.now().toJSDate(),
},
{
entityRef: 'component:default/stale-service-two',
processedDate: DateTime.now().minus({ days: 45 }).toJSDate(),
},
]);
store.getUnprocessedEntities.mockResolvedValue([
'component:default/service-three',
'component:default/service-four',
'component:default/service-five',
]);
const overview = await apiWithAge.getEntitiesOverview();
expect(overview.entityCount).toEqual(5);
expect(overview.processedCount).toEqual(2);
expect(overview.staleCount).toEqual(1);
expect(overview.pendingCount).toEqual(4);
expect(overview.filteredEntities).toEqual([
'component:default/service-three',
'component:default/service-four',
'component:default/service-five',
'component:default/stale-service-two',
]);
});
it('should generate and save languages for an entity', async () => {
const spy = jest
.spyOn(api, 'getLinguistResults')
.mockImplementation(() => linguistResultMock);
urlReader.readTree.mockResolvedValueOnce({
files: async () => [
{
content: async () => Buffer.from('-- XXX: code-data', 'utf8'),
path: 'my-file.js',
},
],
dir: async () => '/temp/my-code',
} as ReadTreeResponse);
const fsSpy = jest.spyOn(fs, 'remove');
await api.generateEntityLanguages(
'component:default/fake-service',
'https://some.fake/service/',
);
expect(api.getLinguistResults).toHaveBeenCalled();
expect(store.insertEntityResults).toHaveBeenCalled();
expect(fs.remove).toHaveBeenCalled();
spy.mockClear();
fsSpy.mockClear();
});
it('should generate languages for entities using default', async () => {
store.getProcessedEntities.mockResolvedValue([
{
entityRef: 'component:default/service-one',
processedDate: DateTime.now().toJSDate(),
},
{
entityRef: 'component:default/stale-service-two',
processedDate: DateTime.now().minus({ days: 45 }).toJSDate(),
},
]);
store.getUnprocessedEntities.mockResolvedValue([
'component:default/service-three',
'component:default/service-four',
'component:default/service-five',
]);
const entity = {
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: 'service-one',
annotations: {
[LINGUIST_ANNOTATION]: 'https://some.fake/service/',
},
},
kind: 'Component',
};
catalogApi.getEntityByRef.mockResolvedValue(entity);
const resultsSpy = jest
.spyOn(api, 'getLinguistResults')
.mockImplementation(() => linguistResultMock);
urlReader.readTree.mockResolvedValue({
files: async () => [
{
content: async () => Buffer.from('-- XXX: code-data', 'utf8'),
path: 'my-file.js',
},
],
dir: async () => '/temp/my-code',
} as ReadTreeResponse);
const fsSpy = jest.spyOn(fs, 'remove');
const generateEntityLanguages = jest.spyOn(api, 'generateEntityLanguages');
await api.generateEntitiesLanguages();
expect(generateEntityLanguages).toHaveBeenCalledTimes(3);
generateEntityLanguages.mockClear();
resultsSpy.mockClear();
fsSpy.mockClear();
});
it('should generate languages for entities using defined batch size', async () => {
const apiWithBatchSize = new LinguistBackendClient(
logger,
store,
urlReader,
tokenManager,
catalogApi,
undefined,
2,
);
store.getProcessedEntities.mockResolvedValue([
{
entityRef: 'component:default/service-one',
processedDate: DateTime.now().toJSDate(),
},
{
entityRef: 'component:default/stale-service-two',
processedDate: DateTime.now().minus({ days: 45 }).toJSDate(),
},
]);
store.getUnprocessedEntities.mockResolvedValue([
'component:default/service-three',
'component:default/service-four',
'component:default/service-five',
]);
const entity = {
apiVersion: 'backstage.io/v1beta1',
metadata: {
name: 'service-one',
annotations: {
[LINGUIST_ANNOTATION]: 'https://some.fake/service/',
},
},
kind: 'Component',
};
catalogApi.getEntityByRef.mockResolvedValue(entity);
const resultsSpy = jest
.spyOn(apiWithBatchSize, 'getLinguistResults')
.mockImplementation(() => linguistResultMock);
urlReader.readTree.mockResolvedValue({
files: async () => [
{
content: async () => Buffer.from('-- XXX: code-data', 'utf8'),
path: 'my-file.js',
},
],
dir: async () => '/temp/my-code',
} as ReadTreeResponse);
const fsSpy = jest.spyOn(fs, 'remove');
const generateEntityLanguages = jest.spyOn(
apiWithBatchSize,
'generateEntityLanguages',
);
await apiWithBatchSize.generateEntitiesLanguages();
expect(generateEntityLanguages).toHaveBeenCalledTimes(2);
generateEntityLanguages.mockClear();
resultsSpy.mockClear();
fsSpy.mockClear();
});
});
@@ -22,14 +22,10 @@ import {
} from '@backstage/plugin-linguist-common';
import {
CATALOG_FILTER_EXISTS,
CatalogClient,
GetEntitiesRequest,
CatalogApi,
} from '@backstage/catalog-client';
import {
PluginEndpointDiscovery,
TokenManager,
UrlReader,
} from '@backstage/backend-common';
import { TokenManager, UrlReader } from '@backstage/backend-common';
import { DateTime } from 'luxon';
import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common';
@@ -43,16 +39,22 @@ import {
} from '@backstage/catalog-model';
import { assertError } from '@backstage/errors';
import { HumanDuration } from '@backstage/types';
import { Results } from 'linguist-js/dist/types';
/** @public */
export class LinguistBackendApi {
export interface LinguistBackendApi {
getEntityLanguages(entityRef: string): Promise<Languages>;
processEntities(): Promise<void>;
}
/** @public */
export class LinguistBackendClient implements LinguistBackendApi {
private readonly logger: Logger;
private readonly store: LinguistBackendStore;
private readonly urlReader: UrlReader;
private readonly discovery: PluginEndpointDiscovery;
private readonly tokenManager: TokenManager;
private readonly catalogClient: CatalogClient;
private readonly catalogApi: CatalogApi;
private readonly age?: HumanDuration;
private readonly batchSize?: number;
private readonly useSourceLocation?: boolean;
@@ -62,8 +64,8 @@ export class LinguistBackendApi {
logger: Logger,
store: LinguistBackendStore,
urlReader: UrlReader,
discovery: PluginEndpointDiscovery,
tokenManager: TokenManager,
catalogApi: CatalogApi,
age?: HumanDuration,
batchSize?: number,
useSourceLocation?: boolean,
@@ -73,9 +75,8 @@ export class LinguistBackendApi {
this.logger = logger;
this.store = store;
this.urlReader = urlReader;
this.discovery = discovery;
this.tokenManager = tokenManager;
this.catalogClient = new CatalogClient({ discoveryApi: this.discovery });
this.catalogApi = catalogApi;
this.batchSize = batchSize;
this.age = age;
this.useSourceLocation = useSourceLocation;
@@ -83,23 +84,25 @@ export class LinguistBackendApi {
this.linguistJsOptions = linguistJsOptions;
}
public async getEntityLanguages(entityRef: string): Promise<Languages> {
async getEntityLanguages(entityRef: string): Promise<Languages> {
this.logger?.debug(`Getting languages for entity "${entityRef}"`);
return this.store.getEntityResults(entityRef);
}
public async processEntities() {
async processEntities(): Promise<void> {
this.logger?.info('Updating list of entities');
await this.addNewEntities();
this.logger?.info('Processing applicable entities through Linguist');
this.logger?.info('Cleaning list of entities');
await this.cleanEntities();
this.logger?.info('Processing applicable entities through Linguist');
await this.generateEntitiesLanguages();
}
private async addNewEntities() {
/** @internal */
async addNewEntities(): Promise<void> {
const annotationKey = this.useSourceLocation
? ANNOTATION_SOURCE_LOCATION
: LINGUIST_ANNOTATION;
@@ -112,7 +115,7 @@ export class LinguistBackendApi {
};
const { token } = await this.tokenManager.getToken();
const response = await this.catalogClient.getEntities(request, { token });
const response = await this.catalogApi.getEntities(request, { token });
const entities = response.items;
entities.forEach(entity => {
@@ -121,7 +124,25 @@ export class LinguistBackendApi {
});
}
private async generateEntitiesLanguages() {
/** @internal */
async cleanEntities(): Promise<void> {
this.logger?.info('Cleaning entities in Linguist queue');
const allEntities = await this.store.getAllEntities();
for (const entityRef of allEntities) {
const result = await this.catalogApi.getEntityByRef(entityRef);
if (!result) {
this.logger?.info(
`Entity ${entityRef} was not found in the Catalog, it will be deleted`,
);
await this.store.deleteEntity(entityRef);
}
}
}
/** @internal */
async generateEntitiesLanguages(): Promise<void> {
const entitiesOverview = await this.getEntitiesOverview();
this.logger?.info(
`Entities overview: Entity: ${entitiesOverview.entityCount}, Processed: ${entitiesOverview.processedCount}, Pending: ${entitiesOverview.pendingCount}, Stale ${entitiesOverview.staleCount}`,
@@ -131,9 +152,10 @@ export class LinguistBackendApi {
0,
this.batchSize ?? 20,
);
entities.forEach(async entityRef => {
for (const entityRef of entities) {
const { token } = await this.tokenManager.getToken();
const entity = await this.catalogClient.getEntityByRef(entityRef, {
const entity = await this.catalogApi.getEntityByRef(entityRef, {
token,
});
const annotationKey = this.useSourceLocation
@@ -153,10 +175,11 @@ export class LinguistBackendApi {
`Unable to process "${entityRef}" using "${url}", message: ${error.message}, stack: ${error.stack}`,
);
}
});
}
}
private async getEntitiesOverview(): Promise<EntitiesOverview> {
/** @internal */
async getEntitiesOverview(): Promise<EntitiesOverview> {
this.logger?.debug('Getting pending entities');
const processedEntities = await this.store.getProcessedEntities();
@@ -169,10 +192,10 @@ export class LinguistBackendApi {
.map(pe => pe.entityRef);
const unprocessedEntities = await this.store.getUnprocessedEntities();
const filteredEntities = staleEntities.concat(unprocessedEntities);
const filteredEntities = unprocessedEntities.concat(staleEntities);
const entitiesOverview: EntitiesOverview = {
entityCount: unprocessedEntities.length,
entityCount: unprocessedEntities.length + processedEntities.length,
processedCount: processedEntities.length,
staleCount: staleEntities.length,
pendingCount: filteredEntities.length,
@@ -182,7 +205,8 @@ export class LinguistBackendApi {
return entitiesOverview;
}
private async generateEntityLanguages(
/** @internal */
async generateEntityLanguages(
entityRef: string,
url: string,
): Promise<string> {
@@ -193,7 +217,7 @@ export class LinguistBackendApi {
const readTreeResponse = await this.urlReader.readTree(url);
const dir = await readTreeResponse.dir();
const results = await linguist(dir, this.linguistJsOptions);
const results = await this.getLinguistResults(dir);
try {
const totalBytes = results.languages.bytes;
@@ -233,9 +257,15 @@ export class LinguistBackendApi {
await fs.remove(dir);
}
}
/** @internal */
async getLinguistResults(dir: string): Promise<Results> {
const results = await linguist(dir, this.linguistJsOptions);
return results;
}
}
export function kindOrDefault(kind?: string[]) {
export function kindOrDefault(kind?: string[]): string[] {
if (!kind || kind.length === 0) {
return ['API', 'Component', 'Template'];
}
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { LinguistBackendApi } from './LinguistBackendApi';
export type { LinguistBackendApi } from './LinguistBackendClient';
@@ -0,0 +1,200 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Knex as KnexType, Knex } from 'knex';
import { TestDatabases } from '@backstage/backend-test-utils';
import {
LinguistBackendDatabase,
LinguistBackendStore,
} from './LinguistBackendDatabase';
import { Languages, ProcessedEntity } from '@backstage/plugin-linguist-common';
function createDatabaseManager(
client: KnexType,
skipMigrations: boolean = false,
) {
return {
getClient: async () => client,
migrations: {
skip: skipMigrations,
},
};
}
const rawDbEntityResultRows = [
{
id: '14b32439-848a-49b2-92a4-98753f932606',
entity_ref: 'template:default/create-react-app-template',
languages: undefined,
processed_date: undefined,
},
{
id: '8c85d3ec-ccea-4b29-9de9-ccdd7e5ba452',
entity_ref: 'template:default/docs-template',
languages: undefined,
processed_date: undefined,
},
{
id: 'b922c87b-37bc-4505-b4af-70a1438decda',
entity_ref: 'template:default/pull-request',
languages:
'{"languageCount":1,"totalBytes":2205,"processedDate":"2023-02-15T20:10:21.378Z","breakdown":[{"name":"YAML","percentage":100,"bytes":2205,"type":"data","color":"#cb171e"}]}',
processed_date: new Date('2023-02-15 20:10:21.378Z'),
},
{
id: 'bd555e6d-a3d0-4b48-a930-194db8f80db7',
entity_ref: 'template:default/react-ssr-template',
languages:
'{"languageCount":8,"totalBytes":8988,"processedDate":"2023-02-15T20:10:21.388Z","breakdown":[{"name":"INI","percentage":2.26,"bytes":203,"type":"data","color":"#d1dbe0"},{"name":"JavaScript","percentage":5.94,"bytes":534,"type":"programming","color":"#f1e05a"},{"name":"YAML","percentage":31.09,"bytes":2794,"type":"data","color":"#cb171e"},{"name":"Markdown","percentage":11.79,"bytes":1060,"type":"prose","color":"#083fa1"},{"name":"JSON","percentage":21.09,"bytes":1896,"type":"data","color":"#292929"},{"name":"CSS","percentage":0,"bytes":0,"type":"markup","color":"#563d7c"},{"name":"TSX","percentage":26.01,"bytes":2338,"type":"programming","color":"#3178c6"},{"name":"TypeScript","percentage":1.81,"bytes":163,"type":"programming","color":"#3178c6"}]}',
processed_date: new Date('2023-02-15 20:10:21.388Z'),
},
{
id: '4145c0cf-44e9-4e95-9d57-781af4685b28',
entity_ref: 'template:default/springboot-template',
languages:
'{"languageCount":9,"totalBytes":80986,"processedDate":"2023-02-15T20:10:21.419Z","breakdown":[{"name":"Shell","percentage":0.16,"bytes":130,"type":"programming","color":"#89e051"},{"name":"INI","percentage":0.35,"bytes":286,"type":"data","color":"#d1dbe0"},{"name":"Dockerfile","percentage":0.3,"bytes":246,"type":"programming","color":"#384d54"},{"name":"YAML","percentage":3.92,"bytes":3171,"type":"data","color":"#cb171e"},{"name":"Markdown","percentage":1.31,"bytes":1059,"type":"prose","color":"#083fa1"},{"name":"XML","percentage":10.48,"bytes":8491,"type":"data","color":"#0060ac"},{"name":"Java","percentage":1.8,"bytes":1455,"type":"programming","color":"#b07219"},{"name":"Text","percentage":81.22,"bytes":65780,"type":"prose"},{"name":"Protocol Buffer","percentage":0.45,"bytes":368,"type":"data"}]}',
processed_date: new Date('2023-02-15 20:10:21.419Z'),
},
];
describe('Linguist database', () => {
const databases = TestDatabases.create();
let store: LinguistBackendStore;
let testDbClient: Knex<any, unknown[]>;
beforeAll(async () => {
testDbClient = await databases.init('SQLITE_3');
const database = createDatabaseManager(testDbClient);
store = await LinguistBackendDatabase.create(await database.getClient());
});
beforeEach(async () => {
await testDbClient.batchInsert('entity_result', rawDbEntityResultRows);
});
afterEach(async () => {
await testDbClient('entity_result').delete();
});
it('should be able to return entity results', async () => {
const validLanguagesResult: Languages = {
languageCount: 1,
totalBytes: 2205,
processedDate: '2023-02-15T20:10:21.378Z',
breakdown: [
{
name: 'YAML',
percentage: 100,
bytes: 2205,
type: 'data',
color: '#cb171e',
},
],
};
const entityResult = await store.getEntityResults(
'template:default/pull-request',
);
expect(entityResult).toMatchObject(validLanguagesResult);
});
it('should return empty entity results when not found', async () => {
const validEmptyLanguagesResult: Languages = {
languageCount: 0,
totalBytes: 0,
processedDate: 'undefined',
breakdown: [],
};
const entityResult = await store.getEntityResults(
'template:default/create-react-app-template',
);
expect(entityResult).toMatchObject(validEmptyLanguagesResult);
});
it('should be able to return unprocessed entities', async () => {
const validUnprocessedEntities: string[] = [
'template:default/create-react-app-template',
'template:default/docs-template',
];
const unprocessedEntities = await store.getUnprocessedEntities();
expect(unprocessedEntities).toMatchObject(validUnprocessedEntities);
});
it('should return string[] when there is no unprocessed entities', async () => {
await testDbClient('entity_result').delete();
const unprocessedEntities = await store.getUnprocessedEntities();
expect(unprocessedEntities).toMatchObject([]);
});
it('should be able to return processed entities', async () => {
const validProcessedEntities: ProcessedEntity[] = [
{
entityRef: 'template:default/pull-request',
processedDate: new Date('2023-02-15 20:10:21.378Z'),
},
{
entityRef: 'template:default/react-ssr-template',
processedDate: new Date('2023-02-15 20:10:21.388Z'),
},
{
entityRef: 'template:default/springboot-template',
processedDate: new Date('2023-02-15 20:10:21.419Z'),
},
];
const processedEntities = await store.getProcessedEntities();
expect(processedEntities).toMatchObject(validProcessedEntities);
});
it('should return string[] when there is no processed entities', async () => {
await testDbClient('entity_result').delete();
const unprocessedEntities = await store.getProcessedEntities();
expect(unprocessedEntities).toMatchObject([]);
});
it('should insert new entities and ignore duplicates', async () => {
const before = testDbClient.from('entity_result').count();
await store.insertNewEntity('component:/default/new-entity-one');
await store.insertNewEntity('component:/default/new-entity-two');
await store.insertNewEntity('template:default/pull-request');
const after = testDbClient.from('entity_result').count();
expect(before).toEqual(after);
});
it('should get all entities', async () => {
const allEntities = await store.getAllEntities();
expect(allEntities.length).toEqual(5);
});
it('should delete entity by its entityRef', async () => {
const before = await testDbClient.from('entity_result').count();
await store.deleteEntity('template:default/create-react-app-template');
const after = await testDbClient.from('entity_result').count();
expect(before).toEqual([{ 'count(*)': 5 }]);
expect(after).toEqual([{ 'count(*)': 4 }]);
});
});
@@ -26,8 +26,8 @@ import {
export type RawDbEntityResultRow = {
id: string;
entity_ref: string;
languages: string;
processed_date: Date;
languages?: string;
processed_date?: Date;
};
/** @public */
@@ -35,8 +35,10 @@ export interface LinguistBackendStore {
insertEntityResults(entityLanguages: EntityResults): Promise<string>;
insertNewEntity(entityRef: string): Promise<void>;
getEntityResults(entityRef: string): Promise<Languages>;
getProcessedEntities(): Promise<ProcessedEntity[] | []>;
getUnprocessedEntities(): Promise<string[] | []>;
getProcessedEntities(): Promise<ProcessedEntity[]>;
getUnprocessedEntities(): Promise<string[]>;
getAllEntities(): Promise<string[]>;
deleteEntity(entityRef: string): Promise<void>;
}
const migrationsDir = resolvePackagePath(
@@ -91,7 +93,7 @@ export class LinguistBackendDatabase implements LinguistBackendStore {
.where({ entity_ref: entityRef })
.first();
if (!entityResults) {
if (!entityResults || !entityResults.languages) {
const emptyResults: Languages = {
languageCount: 0,
totalBytes: 0,
@@ -108,7 +110,7 @@ export class LinguistBackendDatabase implements LinguistBackendStore {
}
}
async getProcessedEntities(): Promise<ProcessedEntity[] | []> {
async getProcessedEntities(): Promise<ProcessedEntity[]> {
const rawEntities = await this.db<RawDbEntityResultRow>('entity_result')
.whereNotNull('processed_date')
.whereNotNull('languages');
@@ -118,9 +120,20 @@ export class LinguistBackendDatabase implements LinguistBackendStore {
}
const processedEntities = rawEntities.map(rawEntity => {
// Note: processed_date should never be null, this is handled by the DB query above
let processedDate = new Date();
if (rawEntity.processed_date) {
// SQLite will return a Timestamp whereas Postgres will return a proper Date
// This tests to see if we are getting a timestamp and convert if needed
processedDate = new Date(+rawEntity.processed_date.toString());
if (isNaN(+rawEntity.processed_date.toString())) {
processedDate = rawEntity.processed_date;
}
}
const processEntity = {
entityRef: rawEntity.entity_ref,
processedDate: rawEntity.processed_date,
processedDate,
};
return processEntity;
@@ -129,8 +142,11 @@ export class LinguistBackendDatabase implements LinguistBackendStore {
return processedEntities;
}
async getUnprocessedEntities(): Promise<string[] | []> {
async getUnprocessedEntities(): Promise<string[]> {
const rawEntities = await this.db<RawDbEntityResultRow>('entity_result')
// TODO(ahhhndre) processed_date should always be null as well but it had a default to the current date
// once the default has been removed and released, we can then come back an enable this check
// .whereNull('processed_date')
.whereNull('languages')
.orderBy('created_at', 'asc');
@@ -144,4 +160,24 @@ export class LinguistBackendDatabase implements LinguistBackendStore {
return unprocessedEntities;
}
async getAllEntities(): Promise<string[]> {
const rawEntities = await this.db<RawDbEntityResultRow>('entity_result');
if (!rawEntities) {
return [];
}
const allEntities = rawEntities.map(rawEntity => {
return rawEntity.entity_ref;
});
return allEntities;
}
async deleteEntity(entityRef: string): Promise<void> {
await this.db<RawDbEntityResultRow>('entity_result')
.where('entity_ref', entityRef)
.delete();
}
}
+1 -3
View File
@@ -15,6 +15,4 @@
*/
export * from './service/router';
export { LinguistBackendApi } from './api';
export { LinguistBackendDatabase } from './db';
export type { LinguistBackendStore } from './db';
export type { LinguistBackendApi } from './api';
@@ -73,7 +73,7 @@ describe('createRouter', () => {
const router = await createRouter(
{ schedule: schedule, age: { days: 30 }, useSourceLocation: false },
{
linguistBackendApi,
linguistBackendApi: linguistBackendApi,
discovery: testDiscovery,
database: createDatabase(),
reader: mockUrlReader,
@@ -31,6 +31,8 @@ import {
TaskScheduleDefinition,
} from '@backstage/backend-tasks';
import { HumanDuration } from '@backstage/types';
import { CatalogClient } from '@backstage/catalog-client';
import { LinguistBackendClient } from '../api/LinguistBackendClient';
/** @public */
export interface PluginOptions {
@@ -74,14 +76,16 @@ export async function createRouter(
await database.getClient(),
);
const linguistBackendApi =
const catalogClient = new CatalogClient({ discoveryApi: discovery });
const linguistBackendClient =
routerOptions.linguistBackendApi ||
new LinguistBackendApi(
new LinguistBackendClient(
logger,
linguistBackendStore,
reader,
discovery,
tokenManager,
catalogClient,
age,
batchSize,
useSourceLocation,
@@ -100,7 +104,7 @@ export async function createRouter(
initialDelay: schedule.initialDelay,
scope: schedule.scope,
fn: async () => {
await linguistBackendApi.processEntities();
await linguistBackendClient.processEntities();
},
});
}
@@ -122,7 +126,7 @@ export async function createRouter(
throw new Error('No entityRef was provided');
}
const entityLanguages = await linguistBackendApi.getEntityLanguages(
const entityLanguages = await linguistBackendClient.getEntityLanguages(
entityRef as string,
);
res.status(200).json(entityLanguages);
@@ -169,67 +169,85 @@ export const useTaskEventStream = (taskId: string): TaskStream => {
let didCancel = false;
let subscription: Subscription | undefined;
let logPusher: NodeJS.Timeout | undefined;
scaffolderApi.getTask(taskId).then(
task => {
if (didCancel) {
return;
}
dispatch({ type: 'INIT', data: task });
// TODO(blam): Use a normal fetch to fetch the current log for the event stream
// and use that for an INIT_EVENTs dispatch event, and then
// use the last event ID to subscribe using after option to
// stream logs. Without this, if you have a lot of logs, it can look like the
// task is being rebuilt on load as it progresses through the steps at a slower
// rate whilst it builds the status from the event logs
const observable = scaffolderApi.streamLogs({ taskId });
const collectedLogEvents = new Array<LogEvent>();
function emitLogs() {
if (collectedLogEvents.length) {
const logs = collectedLogEvents.splice(
0,
collectedLogEvents.length,
);
dispatch({ type: 'LOGS', data: logs });
let retryCount = 1;
const startStreamLogProcess = () =>
scaffolderApi.getTask(taskId).then(
task => {
if (didCancel) {
return;
}
}
dispatch({ type: 'INIT', data: task });
logPusher = setInterval(emitLogs, 500);
// TODO(blam): Use a normal fetch to fetch the current log for the event stream
// and use that for an INIT_EVENTs dispatch event, and then
// use the last event ID to subscribe using after option to
// stream logs. Without this, if you have a lot of logs, it can look like the
// task is being rebuilt on load as it progresses through the steps at a slower
// rate whilst it builds the status from the event logs
const observable = scaffolderApi.streamLogs({ taskId });
subscription = observable.subscribe({
next: event => {
switch (event.type) {
case 'log':
return collectedLogEvents.push(event);
case 'cancelled':
dispatch({ type: 'CANCELLED' });
return undefined;
case 'completion':
emitLogs();
dispatch({ type: 'COMPLETED', data: event });
return undefined;
default:
throw new Error(
`Unhandled event type ${event.type} in observer`,
);
const collectedLogEvents = new Array<LogEvent>();
function emitLogs() {
if (collectedLogEvents.length) {
const logs = collectedLogEvents.splice(
0,
collectedLogEvents.length,
);
dispatch({ type: 'LOGS', data: logs });
}
},
error: error => {
emitLogs();
dispatch({ type: 'ERROR', data: error });
},
});
},
error => {
if (!didCancel) {
dispatch({ type: 'ERROR', data: error });
}
},
);
}
logPusher = setInterval(emitLogs, 500);
subscription = observable.subscribe({
next: event => {
switch (event.type) {
case 'log':
return collectedLogEvents.push(event);
case 'cancelled':
dispatch({ type: 'CANCELLED' });
return undefined;
case 'completion':
emitLogs();
dispatch({ type: 'COMPLETED', data: event });
return undefined;
default:
throw new Error(
`Unhandled event type ${event.type} in observer`,
);
}
},
error: error => {
emitLogs();
// in some cases the error is a refused connection from backend
// this can happen from internet issues or proxy problems
// so we try to reconnect again after some time
// just to restart the fetch process
// details here https://github.com/backstage/backstage/issues/15002
if (!error.message) {
error.message = `We cannot connect at the moment, trying again in some seconds... Retrying (${retryCount}/3 retries)`;
}
if (retryCount <= 3) {
setTimeout(() => {
retryCount += 1;
startStreamLogProcess();
}, 15000);
}
dispatch({ type: 'ERROR', data: error });
},
});
},
error => {
if (!didCancel) {
dispatch({ type: 'ERROR', data: error });
}
},
);
startStreamLogProcess();
return () => {
didCancel = true;
if (subscription) {
+1 -1
View File
@@ -74,6 +74,7 @@
"@types/react": "^16.13.1 || ^17.0.0",
"@uiw/react-codemirror": "^4.9.3",
"classnames": "^2.2.6",
"event-source-polyfill": "^1.0.31",
"git-url-parse": "^13.0.0",
"humanize-duration": "^3.25.1",
"immer": "^9.0.1",
@@ -108,7 +109,6 @@
"@types/json-schema": "^7.0.9",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"event-source-polyfill": "1.0.25",
"msw": "^1.0.0"
},
"files": [
+12 -3
View File
@@ -20,11 +20,14 @@ import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ScaffolderClient } from './api';
import { EventSourcePolyfill } from 'event-source-polyfill';
const MockedEventSource = global.EventSource as jest.MockedClass<
typeof EventSource
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
typeof EventSourcePolyfill
>;
jest.mock('event-source-polyfill');
const server = setupServer();
describe('api', () => {
@@ -102,6 +105,9 @@ describe('api', () => {
},
);
const token = 'fake-token';
identityApi.getCredentials.mockResolvedValue({ token: token });
const next = jest.fn();
await new Promise<void>(complete => {
@@ -112,7 +118,10 @@ describe('api', () => {
expect(MockedEventSource).toHaveBeenCalledWith(
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
{ withCredentials: true },
{
withCredentials: true,
headers: { Authorization: `Bearer ${token}` },
},
);
expect(MockedEventSource.prototype.close).toHaveBeenCalled();
+12 -3
View File
@@ -41,6 +41,7 @@ import {
} from '@backstage/plugin-scaffolder-react';
import queryString from 'qs';
import { EventSourcePolyfill } from 'event-source-polyfill';
/**
* An API to interact with the scaffolder backend.
@@ -224,8 +225,11 @@ export class ScaffolderClient implements ScaffolderApi {
params.set('after', String(Number(after)));
}
this.discoveryApi.getBaseUrl('scaffolder').then(
baseUrl => {
Promise.all([
this.discoveryApi.getBaseUrl('scaffolder'),
this.identityApi?.getCredentials(),
]).then(
([baseUrl, credentials]) => {
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
taskId,
)}/eventstream`;
@@ -240,7 +244,12 @@ export class ScaffolderClient implements ScaffolderApi {
}
};
const eventSource = new EventSource(url, { withCredentials: true });
const eventSource = new EventSourcePolyfill(url, {
withCredentials: true,
headers: credentials?.token
? { Authorization: `Bearer ${credentials.token}` }
: {},
});
eventSource.addEventListener('log', processEvent);
eventSource.addEventListener('cancelled', processEvent);
eventSource.addEventListener('completion', (event: any) => {
+6 -3
View File
@@ -20,11 +20,14 @@ import { ReactNode } from 'react';
import { ResultHighlight } from '@backstage/plugin-search-common';
import { RouteRef } from '@backstage/core-plugin-api';
import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react';
import { SyncResult as SyncResult_2 } from '@backstage/plugin-techdocs-react';
import { TableColumn } from '@backstage/core-components';
import { TableOptions } from '@backstage/core-components';
import { TableProps } from '@backstage/core-components';
import { TechDocsApi as TechDocsApi_2 } from '@backstage/plugin-techdocs-react';
import { TechDocsEntityMetadata as TechDocsEntityMetadata_2 } from '@backstage/plugin-techdocs-react';
import { TechDocsMetadata as TechDocsMetadata_2 } from '@backstage/plugin-techdocs-react';
import { TechDocsStorageApi as TechDocsStorageApi_2 } from '@backstage/plugin-techdocs-react';
import { ToolbarProps } from '@material-ui/core';
import { UserListFilterKind } from '@backstage/plugin-catalog-react';
@@ -237,7 +240,7 @@ export interface TechDocsApi {
export const techdocsApiRef: ApiRef<TechDocsApi>;
// @public
export class TechDocsClient implements TechDocsApi {
export class TechDocsClient implements TechDocsApi_2 {
constructor(options: {
configApi: Config;
discoveryApi: DiscoveryApi;
@@ -442,7 +445,7 @@ export interface TechDocsStorageApi {
export const techdocsStorageApiRef: ApiRef<TechDocsStorageApi>;
// @public
export class TechDocsStorageClient implements TechDocsStorageApi {
export class TechDocsStorageClient implements TechDocsStorageApi_2 {
constructor(options: {
configApi: Config;
discoveryApi: DiscoveryApi;
@@ -471,6 +474,6 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
syncEntityDocs(
entityId: CompoundEntityRef,
logHandler?: (line: string) => void,
): Promise<SyncResult>;
): Promise<SyncResult_2>;
}
```
+4 -2
View File
@@ -23,11 +23,13 @@ import {
} from '@backstage/core-plugin-api';
import { NotFoundError, ResponseError } from '@backstage/errors';
import {
SyncResult,
TechDocsApi,
TechDocsEntityMetadata,
TechDocsMetadata,
TechDocsStorageApi,
} from '@backstage/plugin-techdocs-react';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { SyncResult, TechDocsApi, TechDocsStorageApi } from './api';
/**
* API to talk to `techdocs-backend`.
@@ -189,7 +191,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
* @param entityId - Object containing entity data like name, namespace, etc.
* @param logHandler - Callback to receive log messages from the build process
* @returns Whether documents are currently synchronized to newest version
* @throws Throws error on error from sync endpoint in Techdocs Backend
* @throws Throws error on error from sync endpoint in TechDocs Backend
*/
async syncEntityDocs(
entityId: CompoundEntityRef,
@@ -29,7 +29,7 @@ export type TechDocsPageWrapperProps = {
};
/**
* Component wrapping a techdocs page with Page and Header components
* Component wrapping a TechDocs page with Page and Header components
*
* @public
*/
+1 -1
View File
@@ -2,7 +2,7 @@
The TechDocs reader is a component that fetches a remote page, runs transformers on it and renders it into a shadow dom.
Currently there's no easy way to customize which transformers to run or add new ones. If that is needed you would have to fork the techdocs plugin and make your changes in that fork.
Currently there's no easy way to customize which transformers to run or add new ones. If that is needed you would have to fork the TechDocs plugin and make your changes in that fork.
Transformers are functions that optionally takes in parameters from the Reader.tsx component and returns a function which gets passed the DOM of the fetched page. A very simple transformer can look like this.
+19 -2
View File
@@ -7416,6 +7416,7 @@ __metadata:
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
@@ -8376,7 +8377,7 @@ __metadata:
"@uiw/react-codemirror": ^4.9.3
classnames: ^2.2.6
cross-fetch: ^3.1.5
event-source-polyfill: 1.0.25
event-source-polyfill: ^1.0.31
git-url-parse: ^13.0.0
humanize-duration: ^3.25.1
immer: ^9.0.1
@@ -23573,6 +23574,13 @@ __metadata:
languageName: node
linkType: hard
"event-source-polyfill@npm:^1.0.31":
version: 1.0.31
resolution: "event-source-polyfill@npm:1.0.31"
checksum: 973f226404e2a1b14ed7ef15c718b89e213b41d7cfeeb1c10937fd09229f13904f3d7c3075ab28ccf858c213007559908eecdd577577330352f53a351383dd75
languageName: node
linkType: hard
"event-stream@npm:=3.3.4":
version: 3.3.4
resolution: "event-stream@npm:3.3.4"
@@ -34413,7 +34421,7 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:6.11.0, qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.9.1, qs@npm:^6.9.4, qs@npm:^6.9.6":
"qs@npm:6.11.0":
version: 6.11.0
resolution: "qs@npm:6.11.0"
dependencies:
@@ -34422,6 +34430,15 @@ __metadata:
languageName: node
linkType: hard
"qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.9.1, qs@npm:^6.9.4, qs@npm:^6.9.6":
version: 6.11.2
resolution: "qs@npm:6.11.2"
dependencies:
side-channel: ^1.0.4
checksum: e812f3c590b2262548647d62f1637b6989cc56656dc960b893fe2098d96e1bd633f36576f4cd7564dfbff9db42e17775884db96d846bebe4f37420d073ecdc0b
languageName: node
linkType: hard
"qs@npm:~6.5.2":
version: 6.5.3
resolution: "qs@npm:6.5.3"