Merge branch 'backstage:master' into master
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Fix the public path configuration of the frontend app build so that a trailing `/` is always appended when needed.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-azure': patch
|
||||
---
|
||||
|
||||
Add a new provider `AzureDevOpsEntityProvider` as replacement for `AzureDevOpsDiscoveryProcessor`.
|
||||
|
||||
In order to migrate from the `AzureDevOpsDiscoveryProcessor` you need to apply
|
||||
the following changes:
|
||||
|
||||
**Before:**
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
catalog:
|
||||
locations:
|
||||
- type: azure-discovery
|
||||
target: https://dev.azure.com/myorg/myproject/_git/service-*?path=/catalog-info.yaml
|
||||
```
|
||||
|
||||
```ts
|
||||
/* packages/backend/src/plugins/catalog.ts */
|
||||
|
||||
import { AzureDevOpsDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/** ... other processors ... */
|
||||
builder.addProcessor(new AzureDevOpsDiscoveryProcessor(env.reader));
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
|
||||
catalog:
|
||||
providers:
|
||||
azureDevOps:
|
||||
anyProviderId:
|
||||
host: selfhostedazure.yourcompany.com # This is only really needed for on-premise user, defaults to dev.azure.com
|
||||
organization: myorg # For on-premise this would be your Collection
|
||||
project: myproject
|
||||
repository: service-*
|
||||
path: /catalog-info.yaml
|
||||
```
|
||||
|
||||
```ts
|
||||
/* packages/backend/src/plugins/catalog.ts */
|
||||
|
||||
import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/** ... other processors and/or providers ... */
|
||||
builder.addEntityProvider(
|
||||
AzureDevOpsEntityProvider.fromConfig(env.config, {
|
||||
logger: env.logger,
|
||||
schedule: env.scheduler.createScheduledTaskRunner({
|
||||
frequency: { minutes: 30 },
|
||||
timeout: { minutes: 3 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
Visit [https://backstage.io/docs/integrations/azure/discovery](https://backstage.io/docs/integrations/azure/discovery) for more details and options on configuration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-pagerduty': minor
|
||||
---
|
||||
|
||||
**Breaking**: Use identityApi to provide auth token for pagerduty API calls.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-common': patch
|
||||
---
|
||||
|
||||
`@beta` exports now replaced with `@public` exports
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
Fix broken app-config in the example in the README
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-common': patch
|
||||
---
|
||||
|
||||
Replaced all usages of `@backstage/search-common` with `@backstage/plugin-search-common`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
---
|
||||
|
||||
ability to configure refresh interval on Kubernetes tab
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Server-to-server tokens that are authenticated by the `ServerTokenManager` now must have an `exp` claim that has not expired. Tokens where the `exp` claim is in the past or missing are considered invalid and will throw an error. This is a followup to the deprecation from the `1.2` release of Backstage where perpetual tokens were deprecated. Be sure to update any usage of the `getToken()` method to have it be called every time a token is needed. Do not store tokens for later use.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend': patch
|
||||
---
|
||||
|
||||
`RouterOptions` and `createRouter` now marked as public exports
|
||||
@@ -25,7 +25,6 @@
|
||||
"@backstage/integration": "1.2.0",
|
||||
"@backstage/integration-react": "1.1.0",
|
||||
"@backstage/release-manifests": "0.0.3",
|
||||
"@backstage/search-common": "0.3.4",
|
||||
"@techdocs/cli": "1.1.1",
|
||||
"techdocs-cli-embedded-app": "0.2.70",
|
||||
"@backstage/techdocs-common": "0.11.15",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@techdocs/cli': patch
|
||||
---
|
||||
|
||||
Updated dependency `cypress` to `^10.0.0`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Updated dependency `passport` to `^0.6.0`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Updated dependency `minimatch` to `5.1.0`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-codescene': patch
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
---
|
||||
|
||||
Updated dependency `rc-progress` to `3.3.3`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Fixed a bug that could cause analytics events in other parts of Backstage to capture nonsensical values resembling search modal state under some circumstances.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-elasticsearch': patch
|
||||
---
|
||||
|
||||
Additional types now exported publicly:
|
||||
|
||||
- ElasticSearchAgentOptions
|
||||
- ElasticSearchConcreteQuery
|
||||
- ElasticSearchQueryTranslator
|
||||
- ElasticSearchConnectionConstructor,
|
||||
- ElasticSearchTransportConstructor,
|
||||
- ElasticSearchNodeOptions,
|
||||
- ElasticSearchOptions,
|
||||
- ElasticSearchAuth,
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Added the ability to use an additional `filter` when fetching groups in `MyGroupsSidebarItem` component. Example:
|
||||
|
||||
```diff
|
||||
// app/src/components/Root/Root.tsx
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
//...
|
||||
<SidebarGroup label="Menu" icon={<MenuIcon />}>
|
||||
{/* Global nav, not org-specific */}
|
||||
//...
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
<MyGroupsSidebarItem
|
||||
singularTitle="My Squad"
|
||||
pluralTitle="My Squads"
|
||||
icon={GroupIcon}
|
||||
+ filter={{ 'spec.type': 'team' }}
|
||||
/>
|
||||
//...
|
||||
</SidebarGroup>
|
||||
</ Sidebar>
|
||||
</SidebarPage>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
In order to ensure a good, stable TechDocs user experience when running TechDocs with `techdocs.builder` set to `local`, the number of concurrent builds has been limited to 10. Any additional builds requested concurrently will be queued and handled as prior builds complete. In the unlikely event that you need to handle more concurrent builds, consider scaling out your TechDocs backend deployment or using the `external` option for `techdocs.builder`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
fix Sidebar Contexts deprecation message
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-node': patch
|
||||
---
|
||||
|
||||
Replaced all `@beta` exports with `@public` exports
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Handle binary files and files that are too large during dry-run content upload.
|
||||
@@ -154,3 +154,4 @@ _If you're using Backstage in your organization, please try to add your company
|
||||
| [Zego](https://www.zego.com) | [Sean Kenny](mailto:sean.kenny@zego.com) | Single pane of glass for organisational and operational information for all services across our systems. |
|
||||
| [Absa Group Limited](https://www.absa.africa/absaafrica/) | [Chris Kieser](mailto:chris.kieser@absa.africa) | Developer portal for all development needs - security, AWS, k8s, build and deployment pipelines and more |
|
||||
| [Nutrien Ag Solutions](https://www.nutrienagsolutions.com.au) | [Jan Quijano](mailto:jan.quijano@nutrien.com.au) | Software Project Catalog |
|
||||
| [Lendingkart](https://www.lendingkart.com/) | [Dinesh Rajpoot](mailto:dinesh.rajpoot@lendingkart.com) | Service catalog, Software templates to enforce best practices and tech insights to track mandates & migrations.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.15
|
||||
FROM alpine:3.16
|
||||
|
||||
RUN apk add --update \
|
||||
git \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
ConfluenceCollator.ts reference
|
||||
|
||||
```ts
|
||||
import { DocumentCollator } from '@backstage/search-common';
|
||||
import { DocumentCollator } from '@backstage/plugin-search-common';
|
||||
import fetch from 'cross-fetch';
|
||||
|
||||
export class ConfluenceCollator implements DocumentCollator {
|
||||
|
||||
@@ -3,7 +3,7 @@ ConfluenceResultListItem.tsx reference
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Link } from '@backstage/core-components';
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import {
|
||||
Divider,
|
||||
ListItem,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"cypress": "^7.3.0",
|
||||
"cypress": "^10.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
+63
-43
@@ -7,7 +7,7 @@
|
||||
resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@cypress/request@^2.88.5":
|
||||
"@cypress/request@^2.88.10":
|
||||
version "2.88.10"
|
||||
resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
|
||||
integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==
|
||||
@@ -49,10 +49,10 @@
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277"
|
||||
integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA==
|
||||
|
||||
"@types/sinonjs__fake-timers@^6.0.2":
|
||||
version "6.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
|
||||
integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
|
||||
"@types/sinonjs__fake-timers@8.1.1":
|
||||
version "8.1.1"
|
||||
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
|
||||
integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
|
||||
|
||||
"@types/sizzle@^2.3.2":
|
||||
version "2.3.3"
|
||||
@@ -150,6 +150,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base64-js@^1.3.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||
@@ -180,6 +185,14 @@ buffer-crc32@~0.2.3:
|
||||
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
|
||||
|
||||
buffer@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
cachedir@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
|
||||
@@ -220,7 +233,7 @@ cli-cursor@^3.1.0:
|
||||
dependencies:
|
||||
restore-cursor "^3.1.0"
|
||||
|
||||
cli-table3@~0.6.0:
|
||||
cli-table3@~0.6.1:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a"
|
||||
integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==
|
||||
@@ -290,24 +303,25 @@ cross-spawn@^7.0.0:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cypress@^7.3.0:
|
||||
version "7.7.0"
|
||||
resolved "https://registry.npmjs.org/cypress/-/cypress-7.7.0.tgz#0839ae28e5520536f9667d6c9ae81496b3836e64"
|
||||
integrity sha512-uYBYXNoI5ym0UxROwhQXWTi8JbUEjpC6l/bzoGZNxoKGsLrC1SDPgIDJMgLX/MeEdPL0UInXLDUWN/rSyZUCjQ==
|
||||
cypress@^10.0.0:
|
||||
version "10.0.2"
|
||||
resolved "https://registry.npmjs.org/cypress/-/cypress-10.0.2.tgz#6aeac1923d534f9850d57dad9496f9ea74034a23"
|
||||
integrity sha512-7+C4KHYBcfZrawss+Gt5PlS35rfc6ySc59JcHDVsIMm1E/J35dqE41UEXpdtwIq3549umCerNWnFADzqib4kcA==
|
||||
dependencies:
|
||||
"@cypress/request" "^2.88.5"
|
||||
"@cypress/request" "^2.88.10"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/node" "^14.14.31"
|
||||
"@types/sinonjs__fake-timers" "^6.0.2"
|
||||
"@types/sinonjs__fake-timers" "8.1.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
arch "^2.2.0"
|
||||
blob-util "^2.0.2"
|
||||
bluebird "^3.7.2"
|
||||
buffer "^5.6.0"
|
||||
cachedir "^2.3.0"
|
||||
chalk "^4.1.0"
|
||||
check-more-types "^2.24.0"
|
||||
cli-cursor "^3.1.0"
|
||||
cli-table3 "~0.6.0"
|
||||
cli-table3 "~0.6.1"
|
||||
commander "^5.1.0"
|
||||
common-tags "^1.8.0"
|
||||
dayjs "^1.10.4"
|
||||
@@ -326,15 +340,15 @@ cypress@^7.3.0:
|
||||
listr2 "^3.8.3"
|
||||
lodash "^4.17.21"
|
||||
log-symbols "^4.0.0"
|
||||
minimist "^1.2.5"
|
||||
minimist "^1.2.6"
|
||||
ospath "^1.2.2"
|
||||
pretty-bytes "^5.6.0"
|
||||
ramda "~0.27.1"
|
||||
proxy-from-env "1.0.0"
|
||||
request-progress "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
supports-color "^8.1.1"
|
||||
tmp "~0.2.1"
|
||||
untildify "^4.0.0"
|
||||
url "^0.11.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
dashdash@^1.12.0:
|
||||
@@ -560,6 +574,11 @@ human-signals@^1.1.1:
|
||||
resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
||||
|
||||
ieee754@^1.1.13:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
indent-string@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
|
||||
@@ -714,6 +733,13 @@ log-update@^4.0.0:
|
||||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
@@ -743,7 +769,7 @@ minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.5:
|
||||
minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
@@ -821,6 +847,11 @@ pretty-bytes@^5.6.0:
|
||||
resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||
|
||||
proxy-from-env@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
|
||||
integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
|
||||
|
||||
psl@^1.1.28:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||
@@ -834,11 +865,6 @@ pump@^3.0.0:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
punycode@1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
|
||||
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
|
||||
|
||||
punycode@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
@@ -849,16 +875,6 @@ qs@~6.5.2:
|
||||
resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||
|
||||
querystring@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
|
||||
|
||||
ramda@~0.27.1:
|
||||
version "0.27.2"
|
||||
resolved "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1"
|
||||
integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==
|
||||
|
||||
request-progress@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
|
||||
@@ -903,6 +919,13 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
semver@^7.3.2:
|
||||
version "7.3.7"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
|
||||
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
@@ -1036,9 +1059,9 @@ type-fest@^0.21.3:
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
typescript@^4.1.3:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
|
||||
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
|
||||
version "4.7.2"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4"
|
||||
integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -1050,14 +1073,6 @@ untildify@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
|
||||
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
|
||||
|
||||
url@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
|
||||
dependencies:
|
||||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
@@ -1102,6 +1117,11 @@ wrappy@1:
|
||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||
|
||||
@@ -33,10 +33,14 @@ const serviceEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
|
||||
<EntityKubernetesContent />
|
||||
<EntityKubernetesContent refreshIntervalMs={30000} />
|
||||
</EntityLayout.Route>
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- The optional `refreshIntervalMs` property on the `EntityKubernetesContent` defines the interval in which the content automatically refreshes, if not set this will default to 10 seconds.
|
||||
|
||||
That's it! But now, we need the Kubernetes Backend plugin for the frontend to
|
||||
work.
|
||||
|
||||
|
||||
@@ -6,25 +6,95 @@ sidebar_label: Discovery
|
||||
description: Automatically discovering catalog entities from repositories in an Azure DevOps organization
|
||||
---
|
||||
|
||||
The Azure DevOps integration has a special discovery processor for discovering
|
||||
catalog entities within an Azure DevOps. The processor will crawl the Azure
|
||||
The Azure DevOps integration has a special entity provider for discovering
|
||||
catalog entities within an Azure DevOps. The provider will crawl your Azure
|
||||
DevOps organization and register entities matching the configured path. This can
|
||||
be useful as an alternative to static locations or manually adding things to the
|
||||
catalog.
|
||||
|
||||
This guide explains how to install and configure the Azure DevOps Entity Provider (recommended) or the Azure DevOps Processor.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Code Search Feature
|
||||
|
||||
Azure discovery is driven by the Code Search feature in Azure DevOps, this may not be enabled by default. For Azure
|
||||
DevOps Services you can confirm this by looking at the installed extensions in your Organization Settings. For Azure
|
||||
DevOps Server you'll find this information in your Collection Settings.
|
||||
|
||||
If the Code Search extension is not listed then you can install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ms.vss-code-search&targetId=f9352dac-ba6e-434e-9241-a848a510ce3f&utm_source=vstsproduct&utm_medium=SearchExtStatus).
|
||||
|
||||
### Azure Integration
|
||||
|
||||
Setup [Azure integration](locations.md) with `host` and `token`. Host must be `dev.azure.com` for Cloud users, otherwise set this to your on-premise hostname.
|
||||
|
||||
## Installation
|
||||
|
||||
You will have to add the processors in the catalog initialization code of your
|
||||
backend. They are not installed by default, therefore you have to add a
|
||||
dependency to `@backstage/plugin-catalog-backend-module-azure` to your backend
|
||||
package.
|
||||
At your configuration, you add one or more provider configs:
|
||||
|
||||
```yaml
|
||||
# app-config.yaml
|
||||
catalog:
|
||||
providers:
|
||||
azureDevOps:
|
||||
yourProviderId: # identifies your dataset / provider independent of config changes
|
||||
organization: myorg
|
||||
project: myproject
|
||||
repository: service-* # this will match all repos starting with service-*
|
||||
path: /catalog-info.yaml
|
||||
anotherProviderId: # another identifier
|
||||
organization: myorg
|
||||
project: myproject
|
||||
repository: '*' # this will match all repos starting with service-*
|
||||
path: /src/*/catalog-info.yaml # this will search for files deep inside the /src folder
|
||||
yetAotherProviderId: # guess, what? Another one :)
|
||||
host: selfhostedazure.yourcompany.com
|
||||
organization: myorg
|
||||
project: myproject
|
||||
```
|
||||
|
||||
The parameters available are:
|
||||
|
||||
- `host:` Leave empty for Cloud hosted, otherwise set to your self-hosted instance host.
|
||||
- `organization:` Your Organization slug (or Collection for on-premise users). Required.
|
||||
- `project:` Your project slug. Required.
|
||||
- `repository:` The repository name. Wildcards are supported as show on the examples above. If not set, all repositories will be searched.
|
||||
- `path:` Where to find catalog-info.yaml files. Defaults to /catalog-info.yaml.
|
||||
|
||||
_Note:_ the path parameter follows the same rules as the search on Azure DevOps
|
||||
web interface. For more details visit the
|
||||
[official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops).
|
||||
|
||||
As this provider is not one of the default providers, you will first need to install
|
||||
the Azure catalog plugin:
|
||||
|
||||
```bash
|
||||
# From your Backstage root directory
|
||||
yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-azure
|
||||
```
|
||||
|
||||
And then add the processors to your catalog builder:
|
||||
Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`:
|
||||
|
||||
```diff
|
||||
/* packages/backend/src/plugins/catalog.ts */
|
||||
+import { AzureDevOpsEntityProvider } from '@backstage/plugin-catalog-backend-module-azure';
|
||||
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
/** ... other processors and/or providers ... */
|
||||
+builder.addEntityProvider(
|
||||
+ AzureDevOpsEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: Duration.fromObject({ minutes: 30 }),
|
||||
+ timeout: Duration.fromObject({ minutes: 3 }),
|
||||
+ }),
|
||||
+ }),
|
||||
+);
|
||||
```
|
||||
|
||||
## Alternative Processor
|
||||
|
||||
As an alternative to the entity provider `AzureDevOpsEntityProvider`, you can still use the `AzureDevopsDiscoveryProcessor`.
|
||||
|
||||
```diff
|
||||
// In packages/backend/src/plugins/catalog.ts
|
||||
@@ -37,12 +107,6 @@ And then add the processors to your catalog builder:
|
||||
+ builder.addProcessor(AzureDevOpsDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }));
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
To use the discovery processor, you'll need a Azure integration
|
||||
[set up](locations.md) with a `AZURE_TOKEN`. Then you can add a location target
|
||||
to the catalog configuration:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
@@ -70,13 +134,3 @@ When using a custom pattern, the target is composed of five parts:
|
||||
- The path within each repository to find the catalog YAML file. This will
|
||||
usually be `/catalog-info.yaml`, `/src/*/catalog-info.yaml` or a similar
|
||||
variation for catalog files stored in the root directory of each repository.
|
||||
|
||||
_Note:_ the path parameter follows the same rules as the search on Azure DevOps
|
||||
web interface. For more details visit the
|
||||
[official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops).
|
||||
|
||||
Azure discovery is driven by the Code Search feature in Azure DevOps, this may not be enabled by default. For Azure
|
||||
DevOps Services you can confirm this by looking at the installed extensions in your Organization Settings. For Azure
|
||||
DevOps Server you'll find this information in your Collection Settings.
|
||||
|
||||
If the Code Search extension is not listed then you can install it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ms.vss-code-search&targetId=f9352dac-ba6e-434e-9241-a848a510ce3f&utm_source=vstsproduct&utm_medium=SearchExtStatus).
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@
|
||||
"fs-extra": "10.1.0",
|
||||
"husky": "^8.0.0",
|
||||
"lerna": "^4.0.0",
|
||||
"lint-staged": "^12.2.0",
|
||||
"lint-staged": "^13.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"prettier": "^2.2.1",
|
||||
"semver": "^7.3.2",
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"@types/react-dom": "*",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"cross-env": "^7.0.0",
|
||||
"cypress": "^9.5.0",
|
||||
"cypress": "^10.0.0",
|
||||
"eslint-plugin-cypress": "^2.10.3",
|
||||
"start-server-and-test": "^1.10.11"
|
||||
},
|
||||
|
||||
@@ -171,7 +171,6 @@ describe('ServerTokenManager', () => {
|
||||
|
||||
it('should throw for expired tokens, and re-issue new ones', async () => {
|
||||
jest.useFakeTimers();
|
||||
const warn = jest.spyOn(logger, 'warn');
|
||||
|
||||
const tokenManager = ServerTokenManager.fromConfig(configWithSecret, {
|
||||
logger,
|
||||
@@ -179,14 +178,12 @@ describe('ServerTokenManager', () => {
|
||||
|
||||
const { token: token1 } = await tokenManager.getToken();
|
||||
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
|
||||
expect(warn.mock.calls.length).toBe(0);
|
||||
|
||||
// Right before the reissue timeout, it still returns the same token
|
||||
jest.advanceTimersByTime(9 * 60 * 1000);
|
||||
const { token: token1Again } = await tokenManager.getToken();
|
||||
expect(token1).toEqual(token1Again);
|
||||
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
|
||||
expect(warn.mock.calls.length).toBe(0);
|
||||
|
||||
// Right after the reissue timeout, the old ones are still valid but returning a new token
|
||||
jest.advanceTimersByTime(2 * 60 * 1000);
|
||||
@@ -194,14 +191,47 @@ describe('ServerTokenManager', () => {
|
||||
expect(token1).not.toEqual(token2);
|
||||
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
|
||||
await expect(tokenManager.authenticate(token2)).resolves.not.toThrow();
|
||||
expect(warn.mock.calls.length).toBe(0);
|
||||
|
||||
// After expiry of the first one, it gets warnings but the newest one is still valid
|
||||
jest.advanceTimersByTime(52 * 60 * 1000);
|
||||
await expect(tokenManager.authenticate(token1)).resolves.not.toThrow();
|
||||
await expect(tokenManager.authenticate(token1)).rejects.toThrow(
|
||||
'Invalid server token; caused by JWTExpired: "exp" claim timestamp check failed',
|
||||
);
|
||||
await expect(tokenManager.authenticate(token2)).resolves.not.toThrow();
|
||||
expect(warn.mock.calls[0][0]).toMatchInlineSnapshot(
|
||||
'"#### DEPRECATION WARNING: #### Server-to-server token had an expired exp claim, support for this has been deprecated and will result in errors in a future release"',
|
||||
});
|
||||
|
||||
it('should work with a manually crafted JWT', async () => {
|
||||
const secret = 'a1b2c3';
|
||||
const token = await new jose.SignJWT({})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setSubject('backstage-server')
|
||||
.setExpirationTime(Date.now() + 1000 * 60 * 60)
|
||||
.sign(jose.base64url.decode(secret));
|
||||
|
||||
const tokenManager = ServerTokenManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: { auth: { keys: [{ secret }] } },
|
||||
}),
|
||||
{ logger },
|
||||
);
|
||||
await expect(tokenManager.authenticate(token)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reject tokens without exp claim', async () => {
|
||||
const secret = 'a1b2c3';
|
||||
const token = await new jose.SignJWT({})
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setSubject('backstage-server')
|
||||
.sign(jose.base64url.decode(secret));
|
||||
|
||||
const tokenManager = ServerTokenManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: { auth: { keys: [{ secret }] } },
|
||||
}),
|
||||
{ logger },
|
||||
);
|
||||
await expect(tokenManager.authenticate(token)).rejects.toThrow(
|
||||
'Invalid server token; caused by AuthenticationError: Server-to-server token had no exp claim',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { AuthenticationError, NotAllowedError } from '@backstage/errors';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { base64url, exportJWK, generateSecret, jwtVerify, SignJWT } from 'jose';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Logger } from 'winston';
|
||||
@@ -64,8 +64,6 @@ export class ServerTokenManager implements TokenManager {
|
||||
private signingKey: Uint8Array;
|
||||
private privateKeyPromise: Promise<void> | undefined;
|
||||
private currentTokenPromise: Promise<{ token: string }> | undefined;
|
||||
private warnedForMissingExpClaim = false;
|
||||
private warnedForExpiredExpClaim = false;
|
||||
|
||||
/**
|
||||
* Creates a token manager that issues static dummy tokens and never fails
|
||||
@@ -184,36 +182,20 @@ export class ServerTokenManager implements TokenManager {
|
||||
const {
|
||||
protectedHeader: { alg },
|
||||
payload: { sub, exp },
|
||||
} = await jwtVerify(token, key, {
|
||||
// TODO(freben): Holding on to tokens and reusing them is deprecated; remove this tolerance in a future release
|
||||
clockTolerance: 3e9,
|
||||
});
|
||||
} = await jwtVerify(token, key);
|
||||
|
||||
if (alg !== TOKEN_ALG) {
|
||||
throw new NotAllowedError(`Illegal alg "${alg}"`);
|
||||
throw new AuthenticationError(`Illegal alg "${alg}"`);
|
||||
}
|
||||
|
||||
if (sub !== TOKEN_SUB) {
|
||||
throw new NotAllowedError(`Illegal sub "${sub}"`);
|
||||
throw new AuthenticationError(`Illegal sub "${sub}"`);
|
||||
}
|
||||
|
||||
// TODO(freben): Passing in tokens without an exp is deprecated; change this warning to an error in a future release
|
||||
if (typeof exp !== 'number') {
|
||||
if (!this.warnedForMissingExpClaim) {
|
||||
this.warnedForMissingExpClaim = true;
|
||||
this.options.logger.warn(
|
||||
`#### DEPRECATION WARNING: #### Server-to-server token had no exp claim, support for this has been deprecated and will result in errors in a future release`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO(freben): Holding on to tokens and reusing them is deprecated; remove this tolerance in a future release
|
||||
else if (exp * 1000 < Date.now()) {
|
||||
if (!this.warnedForExpiredExpClaim) {
|
||||
this.warnedForExpiredExpClaim = true;
|
||||
this.options.logger.warn(
|
||||
`#### DEPRECATION WARNING: #### Server-to-server token had an expired exp claim, support for this has been deprecated and will result in errors in a future release`,
|
||||
);
|
||||
}
|
||||
throw new AuthenticationError(
|
||||
'Server-to-server token had no exp claim',
|
||||
);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
@@ -222,6 +204,6 @@ export class ServerTokenManager implements TokenManager {
|
||||
}
|
||||
}
|
||||
|
||||
throw new AuthenticationError(`Invalid server token: ${verifyError}`);
|
||||
throw new AuthenticationError('Invalid server token', verifyError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.4.2",
|
||||
"minimatch": "5.0.1",
|
||||
"minimatch": "5.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-libs-browser": "^2.2.1",
|
||||
"npm-packlist": "^5.0.0",
|
||||
|
||||
@@ -89,6 +89,7 @@ export async function createConfig(
|
||||
|
||||
const baseUrl = frontendConfig.getString('app.baseUrl');
|
||||
const validBaseUrl = new URL(baseUrl);
|
||||
const publicPath = validBaseUrl.pathname.replace(/\/$/, '');
|
||||
if (checksEnabled) {
|
||||
plugins.push(
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
@@ -121,7 +122,7 @@ export async function createConfig(
|
||||
new HtmlWebpackPlugin({
|
||||
template: paths.targetHtml,
|
||||
templateParameters: {
|
||||
publicPath: validBaseUrl.pathname.replace(/\/$/, ''),
|
||||
publicPath,
|
||||
config: frontendConfig,
|
||||
},
|
||||
}),
|
||||
@@ -194,7 +195,7 @@ export async function createConfig(
|
||||
},
|
||||
output: {
|
||||
path: paths.targetDist,
|
||||
publicPath: validBaseUrl.pathname,
|
||||
publicPath: `${publicPath}/`,
|
||||
filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js',
|
||||
chunkFilename: isDev
|
||||
? '[name].chunk.js'
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"pluralize": "^8.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.9.4",
|
||||
"rc-progress": "3.3.2",
|
||||
"rc-progress": "3.3.3",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-markdown": "^8.0.0",
|
||||
|
||||
@@ -59,7 +59,7 @@ const defaultSidebarOpenStateContext = {
|
||||
* Context whether the `Sidebar` is open
|
||||
*
|
||||
* @public @deprecated
|
||||
* Use `<SidebarContextProvider>` + `useSidebar()` instead.
|
||||
* Use `<SidebarOpenStateProvider>` + `useSidebarOpenState()` instead.
|
||||
*/
|
||||
export const LegacySidebarContext = createContext<SidebarContextType>(
|
||||
defaultSidebarOpenStateContext,
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react-dom": "*",
|
||||
"cross-env": "^7.0.0",
|
||||
"cypress": "^9.5.0",
|
||||
"cypress": "^10.0.0",
|
||||
"eslint-plugin-cypress": "^2.10.3",
|
||||
"start-server-and-test": "^1.10.11"
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/serve-handler": "^6.1.0",
|
||||
"@types/webpack-env": "^1.15.3",
|
||||
"cypress": "^9.5.0",
|
||||
"cypress": "^10.0.0",
|
||||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"find-process": "^1.4.5",
|
||||
"nodemon": "^2.0.2",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-cache": "^5.1.2",
|
||||
"openid-client": "^5.1.3",
|
||||
"passport": "^0.5.2",
|
||||
"passport": "^0.6.0",
|
||||
"passport-bitbucket-oauth2": "^0.1.2",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-gitlab2": "^5.0.0",
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
|
||||
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
|
||||
import { Config } from '@backstage/config';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-backend';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
|
||||
import { LocationSpec } from '@backstage/plugin-catalog-backend';
|
||||
import { Logger } from 'winston';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
|
||||
// @public
|
||||
export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor {
|
||||
@@ -32,4 +35,22 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor {
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class AzureDevOpsEntityProvider implements EntityProvider {
|
||||
// (undocumented)
|
||||
connect(connection: EntityProviderConnection): Promise<void>;
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
configRoot: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
schedule: TaskRunner;
|
||||
},
|
||||
): AzureDevOpsEntityProvider[];
|
||||
// (undocumented)
|
||||
getProviderName(): string;
|
||||
// (undocumented)
|
||||
refresh(logger: Logger): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface AzureDevOpsConfig {
|
||||
/**
|
||||
* (Optional) The DevOps host; leave empty for `dev.azure.com`, otherwise set to your self-hosted instance host.
|
||||
* @visibility backend
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* (Required) Your organization slug.
|
||||
* @visibility backend
|
||||
*/
|
||||
organization: string;
|
||||
/**
|
||||
* (Required) Your project slug.
|
||||
* @visibility backend
|
||||
*/
|
||||
project: string;
|
||||
/**
|
||||
* (Optional) The repository name. Wildcards are supported as show on the examples above.
|
||||
* If not set, all repositories will be searched.
|
||||
* @visibility backend
|
||||
*/
|
||||
repository?: string;
|
||||
/**
|
||||
* (Optional) Where to find catalog-info.yaml files. Wildcards are supported.
|
||||
* If not set, defaults to /catalog-info.yaml.
|
||||
* @visibility backend
|
||||
*/
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
catalog?: {
|
||||
/**
|
||||
* List of provider-specific options and attributes
|
||||
*/
|
||||
providers?: {
|
||||
/**
|
||||
* AzureDevopsEntityProvider configuration
|
||||
*/
|
||||
azureDevOps?: Record<string, AzureDevOpsConfig>;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.13.6-next.1",
|
||||
"@backstage/catalog-model": "^1.0.3-next.0",
|
||||
"@backstage/backend-tasks": "^0.3.2-next.0",
|
||||
"@backstage/config": "^1.0.1",
|
||||
"@backstage/errors": "^1.0.0",
|
||||
"@backstage/integration": "^1.2.1-next.1",
|
||||
@@ -42,6 +43,7 @@
|
||||
"@backstage/types": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"msw": "^0.42.0",
|
||||
"uuid": "^8.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
@@ -51,6 +53,8 @@
|
||||
"@types/lodash": "^4.14.151"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor';
|
||||
export { AzureDevOpsDiscoveryProcessor } from './processors';
|
||||
export { AzureDevOpsEntityProvider } from './providers';
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ import {
|
||||
AzureDevOpsDiscoveryProcessor,
|
||||
parseUrl,
|
||||
} from './AzureDevOpsDiscoveryProcessor';
|
||||
import { codeSearch } from './lib';
|
||||
import { codeSearch } from '../lib';
|
||||
|
||||
jest.mock('./lib');
|
||||
jest.mock('../lib');
|
||||
const mockCodeSearch = codeSearch as jest.MockedFunction<typeof codeSearch>;
|
||||
|
||||
describe('AzureDevOpsDiscoveryProcessor', () => {
|
||||
+1
-1
@@ -26,7 +26,7 @@ import {
|
||||
processingResult,
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { Logger } from 'winston';
|
||||
import { codeSearch } from './lib';
|
||||
import { codeSearch } from '../lib';
|
||||
|
||||
/**
|
||||
* Extracts repositories out of an Azure DevOps org.
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor';
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2022 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 } from '@backstage/backend-common';
|
||||
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
|
||||
import { CodeSearchResultItem } from '../lib';
|
||||
import { AzureDevOpsEntityProvider } from './AzureDevOpsEntityProvider';
|
||||
import { codeSearch } from '../lib';
|
||||
|
||||
jest.mock('../lib');
|
||||
const mockCodeSearch = codeSearch as jest.MockedFunction<typeof codeSearch>;
|
||||
|
||||
class PersistingTaskRunner implements TaskRunner {
|
||||
private tasks: TaskInvocationDefinition[] = [];
|
||||
|
||||
getTasks() {
|
||||
return this.tasks;
|
||||
}
|
||||
|
||||
run(task: TaskInvocationDefinition): Promise<void> {
|
||||
this.tasks.push(task);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
describe('AzureDevOpsEntityProvider', () => {
|
||||
afterEach(() => {
|
||||
mockCodeSearch.mockClear();
|
||||
});
|
||||
|
||||
const expectMutation = async (
|
||||
providerId: string,
|
||||
providerConfig: object,
|
||||
codeSearchResults: CodeSearchResultItem[],
|
||||
expectedBaseUrl: string,
|
||||
names: Record<string, string>,
|
||||
integrationConfig?: object,
|
||||
) => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
azure: integrationConfig ? [integrationConfig] : [],
|
||||
},
|
||||
catalog: {
|
||||
providers: {
|
||||
azureDevOps: {
|
||||
[providerId]: providerConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mockCodeSearch.mockResolvedValueOnce(codeSearchResults);
|
||||
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const entityProviderConnection: EntityProviderConnection = {
|
||||
applyMutation: jest.fn(),
|
||||
};
|
||||
|
||||
const provider = AzureDevOpsEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
schedule,
|
||||
})[0];
|
||||
expect(provider.getProviderName()).toEqual(
|
||||
`AzureDevOpsEntityProvider:${providerId}`,
|
||||
);
|
||||
|
||||
await provider.connect(entityProviderConnection);
|
||||
|
||||
const taskDef = schedule.getTasks()[0];
|
||||
expect(taskDef.id).toEqual(
|
||||
`AzureDevOpsEntityProvider:${providerId}:refresh`,
|
||||
);
|
||||
await (taskDef.fn as () => Promise<void>)();
|
||||
|
||||
const expectedEntities = codeSearchResults.map(item => {
|
||||
const url = encodeURI(
|
||||
`${expectedBaseUrl}/_git/${item.repository.name}?path=${item.path}`,
|
||||
);
|
||||
return {
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location': `url:${url}`,
|
||||
'backstage.io/managed-by-origin-location': `url:${url}`,
|
||||
},
|
||||
name: names[`${item.repository.name}?path=${item.path}`],
|
||||
},
|
||||
spec: {
|
||||
presence: 'required',
|
||||
target: `${url}`,
|
||||
type: 'url',
|
||||
},
|
||||
},
|
||||
locationKey: `AzureDevOpsEntityProvider:${providerId}`,
|
||||
};
|
||||
});
|
||||
|
||||
expect(entityProviderConnection.applyMutation).toBeCalledWith({
|
||||
type: 'full',
|
||||
entities: expectedEntities,
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('no mutation when repos are empty', async () => {
|
||||
return expectMutation(
|
||||
'allRepos',
|
||||
{
|
||||
organization: 'myorganization',
|
||||
project: 'myproject',
|
||||
},
|
||||
[],
|
||||
'https://dev.azure.com/myorganization/myproject',
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('single mutation when repos have 1 file found', async () => {
|
||||
return expectMutation(
|
||||
'allReposSingleFile',
|
||||
{
|
||||
organization: 'myorganization',
|
||||
project: 'myproject',
|
||||
},
|
||||
[
|
||||
{
|
||||
fileName: 'catalog-info.yaml',
|
||||
path: '/catalog-info.yaml',
|
||||
repository: {
|
||||
name: 'myrepo',
|
||||
},
|
||||
},
|
||||
],
|
||||
'https://dev.azure.com/myorganization/myproject',
|
||||
{
|
||||
'myrepo?path=/catalog-info.yaml':
|
||||
'generated-87865246726bb12a8c4fb4f914443f1fbb91648c',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('single mutation when multiple repos have multiple files', async () => {
|
||||
return expectMutation(
|
||||
'allReposMultipleFiles',
|
||||
{
|
||||
organization: 'myorganization',
|
||||
project: 'myproject',
|
||||
},
|
||||
[
|
||||
{
|
||||
fileName: 'catalog-info.yaml',
|
||||
path: '/catalog-info.yaml',
|
||||
repository: {
|
||||
name: 'myrepo',
|
||||
},
|
||||
},
|
||||
{
|
||||
fileName: 'catalog-info.yaml',
|
||||
path: '/catalog-info.yaml',
|
||||
repository: {
|
||||
name: 'myotherrepo',
|
||||
},
|
||||
},
|
||||
],
|
||||
'https://dev.azure.com/myorganization/myproject',
|
||||
{
|
||||
'myrepo?path=/catalog-info.yaml':
|
||||
'generated-87865246726bb12a8c4fb4f914443f1fbb91648c',
|
||||
'myotherrepo?path=/catalog-info.yaml':
|
||||
'generated-2deccac384c34d0dca37be0ebb4b1c8cf6913fe1',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2022 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 { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
import { AzureIntegration, ScmIntegrations } from '@backstage/integration';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
LocationSpec,
|
||||
locationSpecToLocationEntity,
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { readAzureDevOpsConfigs } from './config';
|
||||
import { Logger } from 'winston';
|
||||
import { AzureDevOpsConfig } from './types';
|
||||
import * as uuid from 'uuid';
|
||||
import { codeSearch, CodeSearchResultItem } from '../lib';
|
||||
|
||||
/**
|
||||
* Provider which discovers catalog files within an Azure DevOps repositories.
|
||||
*
|
||||
* Use `AzureDevOpsEntityProvider.fromConfig(...)` to create instances.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class AzureDevOpsEntityProvider implements EntityProvider {
|
||||
private readonly logger: Logger;
|
||||
private readonly scheduleFn: () => Promise<void>;
|
||||
private connection?: EntityProviderConnection;
|
||||
|
||||
static fromConfig(
|
||||
configRoot: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
schedule: TaskRunner;
|
||||
},
|
||||
): AzureDevOpsEntityProvider[] {
|
||||
const providerConfigs = readAzureDevOpsConfigs(configRoot);
|
||||
|
||||
return providerConfigs.map(providerConfig => {
|
||||
const integration = ScmIntegrations.fromConfig(configRoot).azure.byHost(
|
||||
providerConfig.host,
|
||||
);
|
||||
|
||||
if (!integration) {
|
||||
throw new Error(
|
||||
`There is no Azure integration for host ${providerConfig.host}. Please add a configuration entry for it under integrations.azure`,
|
||||
);
|
||||
}
|
||||
|
||||
return new AzureDevOpsEntityProvider(
|
||||
providerConfig,
|
||||
integration,
|
||||
options.logger,
|
||||
options.schedule,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly config: AzureDevOpsConfig,
|
||||
private readonly integration: AzureIntegration,
|
||||
logger: Logger,
|
||||
schedule: TaskRunner,
|
||||
) {
|
||||
this.logger = logger.child({
|
||||
target: this.getProviderName(),
|
||||
});
|
||||
|
||||
this.scheduleFn = this.createScheduleFn(schedule);
|
||||
}
|
||||
|
||||
private createScheduleFn(schedule: TaskRunner): () => Promise<void> {
|
||||
return async () => {
|
||||
const taskId = `${this.getProviderName()}:refresh`;
|
||||
return schedule.run({
|
||||
id: taskId,
|
||||
fn: async () => {
|
||||
const logger = this.logger.child({
|
||||
class: AzureDevOpsEntityProvider.prototype.constructor.name,
|
||||
taskId,
|
||||
taskInstanceId: uuid.v4(),
|
||||
});
|
||||
|
||||
try {
|
||||
await this.refresh(logger);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
|
||||
getProviderName(): string {
|
||||
return `AzureDevOpsEntityProvider:${this.config.id}`;
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
|
||||
async connect(connection: EntityProviderConnection): Promise<void> {
|
||||
this.connection = connection;
|
||||
await this.scheduleFn();
|
||||
}
|
||||
|
||||
async refresh(logger: Logger) {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
logger.info('Discovering Azure DevOps catalog files');
|
||||
|
||||
const files = await codeSearch(
|
||||
this.integration.config,
|
||||
this.config.organization,
|
||||
this.config.project,
|
||||
this.config.repository,
|
||||
this.config.path,
|
||||
);
|
||||
|
||||
logger.info(`Discovered ${files.length} catalog files`);
|
||||
|
||||
const locations = files.map(key => this.createLocationSpec(key));
|
||||
|
||||
await this.connection.applyMutation({
|
||||
type: 'full',
|
||||
entities: locations.map(location => {
|
||||
return {
|
||||
locationKey: this.getProviderName(),
|
||||
entity: locationSpecToLocationEntity({ location }),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Committed ${locations.length} locations for AzureDevOps catalog files`,
|
||||
);
|
||||
}
|
||||
|
||||
private createLocationSpec(file: CodeSearchResultItem): LocationSpec {
|
||||
return {
|
||||
type: 'url',
|
||||
target: this.createObjectUrl(file),
|
||||
presence: 'required',
|
||||
};
|
||||
}
|
||||
|
||||
private createObjectUrl(file: CodeSearchResultItem): string {
|
||||
const baseUrl = `https://${this.config.host}/${this.config.organization}/${this.config.project}`;
|
||||
return encodeURI(
|
||||
`${baseUrl}/_git/${file.repository.name}?path=${file.path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { readAzureDevOpsConfigs } from './config';
|
||||
|
||||
describe('readAzureDevOpsConfigs', () => {
|
||||
it('reads all provider configs and set default values', () => {
|
||||
const provider1 = {
|
||||
host: 'azure.mycompany.com',
|
||||
organization: 'mycompany',
|
||||
project: 'myproject',
|
||||
};
|
||||
const provider2 = {
|
||||
organization: 'mycompany',
|
||||
project: 'myproject',
|
||||
};
|
||||
const provider3 = {
|
||||
organization: 'mycompany',
|
||||
project: 'myproject',
|
||||
repository: 'service-*',
|
||||
};
|
||||
const config = {
|
||||
catalog: {
|
||||
providers: {
|
||||
azureDevOps: { provider1, provider2, provider3 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actual = readAzureDevOpsConfigs(new ConfigReader(config));
|
||||
|
||||
expect(actual).toHaveLength(3);
|
||||
expect(actual[0]).toEqual({
|
||||
...provider1,
|
||||
path: '/catalog-info.yaml',
|
||||
repository: '*',
|
||||
id: 'provider1',
|
||||
});
|
||||
expect(actual[1]).toEqual({
|
||||
...provider2,
|
||||
host: 'dev.azure.com',
|
||||
path: '/catalog-info.yaml',
|
||||
repository: '*',
|
||||
id: 'provider2',
|
||||
});
|
||||
expect(actual[2]).toEqual({
|
||||
...provider3,
|
||||
host: 'dev.azure.com',
|
||||
path: '/catalog-info.yaml',
|
||||
id: 'provider3',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Config } from '@backstage/config';
|
||||
import { AzureDevOpsConfig } from './types';
|
||||
|
||||
export function readAzureDevOpsConfigs(config: Config): AzureDevOpsConfig[] {
|
||||
const configs: AzureDevOpsConfig[] = [];
|
||||
|
||||
const providerConfigs = config.getOptionalConfig(
|
||||
'catalog.providers.azureDevOps',
|
||||
);
|
||||
|
||||
if (!providerConfigs) {
|
||||
return configs;
|
||||
}
|
||||
|
||||
for (const id of providerConfigs.keys()) {
|
||||
configs.push(readAzureDevOpsConfig(id, providerConfigs.getConfig(id)));
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
function readAzureDevOpsConfig(id: string, config: Config): AzureDevOpsConfig {
|
||||
const organization = config.getString('organization');
|
||||
const project = config.getString('project');
|
||||
const host = config.getOptionalString('host') || 'dev.azure.com';
|
||||
const repository = config.getOptionalString('repository') || '*';
|
||||
const path = config.getOptionalString('path') || '/catalog-info.yaml';
|
||||
|
||||
return {
|
||||
id,
|
||||
host,
|
||||
organization,
|
||||
project,
|
||||
repository,
|
||||
path,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { AzureDevOpsEntityProvider } from './AzureDevOpsEntityProvider';
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type AzureDevOpsConfig = {
|
||||
id: string;
|
||||
host: string;
|
||||
organization: string;
|
||||
project: string;
|
||||
repository: string;
|
||||
path: string;
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```ts
|
||||
import { BasicPermission } from '@backstage/plugin-permission-common';
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { ResourcePermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
// @alpha
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/plugin-permission-common": "^0.6.2-next.0",
|
||||
"@backstage/search-common": "^0.3.5-next.0"
|
||||
"@backstage/plugin-search-common": "^0.3.5-next.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.17.2-next.1"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { IndexableDocument } from '@backstage/search-common';
|
||||
import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
|
||||
/**
|
||||
* The Document format for an Entity in the Catalog for search
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@material-ui/core": "^4.9.10",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"rc-progress": "3.3.2",
|
||||
"rc-progress": "3.3.3",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ yarn add --cwd packages/app @backstage/plugin-cost-insights
|
||||
|
||||
2. Create a CostInsights client. Clients must implement the [CostInsightsApi](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts) interface. Create your own or [use a template](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/example/templates/CostInsightsClient.ts) to get started.
|
||||
|
||||
Tip: You can also use the `ExampleCostInsightsClient` from `@backstage/plugin-cost-insights` to see how the plugin looks with some mock data.
|
||||
|
||||
```ts
|
||||
// path/to/CostInsightsClient.ts
|
||||
import { CostInsightsApi } from '@backstage/plugin-cost-insights';
|
||||
@@ -171,16 +173,15 @@ costInsights:
|
||||
name: Some Other Cloud Product
|
||||
icon: data
|
||||
currencies:
|
||||
metricA:
|
||||
currencyA:
|
||||
label: Currency A
|
||||
unit: Unit A
|
||||
currencyB:
|
||||
label: Currency B
|
||||
kind: CURRENCY_B
|
||||
unit: Unit B
|
||||
prefix: B
|
||||
rate: 3.5
|
||||
currencyA:
|
||||
label: Currency A
|
||||
unit: Unit A
|
||||
currencyB:
|
||||
label: Currency B
|
||||
kind: CURRENCY_B
|
||||
unit: Unit B
|
||||
prefix: B
|
||||
rate: 3.5
|
||||
```
|
||||
|
||||
## Alerts
|
||||
|
||||
@@ -112,7 +112,14 @@ export interface DeploymentResources {
|
||||
// Warning: (ae-missing-release-tag) "EntityKubernetesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityKubernetesContent: (_props: {}) => JSX.Element;
|
||||
export const EntityKubernetesContent: (
|
||||
props: EntityKubernetesContentProps,
|
||||
) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type EntityKubernetesContentProps = {
|
||||
refreshIntervalMs?: number;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ErrorPanelProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ErrorPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -303,6 +310,7 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
// @public (undocumented)
|
||||
export const KubernetesContent: ({
|
||||
entity,
|
||||
refreshIntervalMs,
|
||||
}: KubernetesContentProps) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "KubernetesDrawerable" needs to be exported by the entry point index.d.ts
|
||||
@@ -373,11 +381,10 @@ export const PodsTable: ({
|
||||
extraColumns,
|
||||
}: PodsTablesProps) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const Router: (_props: Props) => JSX.Element;
|
||||
export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ServiceAccountKubernetesAuthProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
||||
@@ -32,9 +32,7 @@ export const isKubernetesAvailable = (entity: Entity) =>
|
||||
entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION],
|
||||
);
|
||||
|
||||
type Props = {};
|
||||
|
||||
export const Router = (_props: Props) => {
|
||||
export const Router = (props: { refreshIntervalMs?: number }) => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
const kubernetesAnnotationValue =
|
||||
@@ -49,7 +47,15 @@ export const Router = (_props: Props) => {
|
||||
) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<KubernetesContent entity={entity} />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<KubernetesContent
|
||||
entity={entity}
|
||||
refreshIntervalMs={props.refreshIntervalMs}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,10 +25,20 @@ import EmptyStateImage from '../assets/emptystate.svg';
|
||||
import { useKubernetesObjects } from '../hooks';
|
||||
import { Content, Page, Progress } from '@backstage/core-components';
|
||||
|
||||
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
|
||||
type KubernetesContentProps = {
|
||||
entity: Entity;
|
||||
refreshIntervalMs?: number;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
const { kubernetesObjects, error } = useKubernetesObjects(entity);
|
||||
export const KubernetesContent = ({
|
||||
entity,
|
||||
refreshIntervalMs,
|
||||
}: KubernetesContentProps) => {
|
||||
const { kubernetesObjects, error } = useKubernetesObjects(
|
||||
entity,
|
||||
refreshIntervalMs,
|
||||
);
|
||||
|
||||
const clustersWithErrors =
|
||||
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
|
||||
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
kubernetesPlugin as plugin,
|
||||
EntityKubernetesContent,
|
||||
} from './plugin';
|
||||
export type { EntityKubernetesContentProps } from './plugin';
|
||||
export { Router, isKubernetesAvailable } from './Router';
|
||||
export * from './api';
|
||||
export * from './kubernetes-auth-provider';
|
||||
|
||||
@@ -76,7 +76,21 @@ export const kubernetesPlugin = createPlugin({
|
||||
},
|
||||
});
|
||||
|
||||
export const EntityKubernetesContent = kubernetesPlugin.provide(
|
||||
/**
|
||||
* Props of EntityKubernetesContent
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityKubernetesContentProps = {
|
||||
/**
|
||||
* Sets the refresh interval in milliseconds. The default value is 10000 (10 seconds)
|
||||
*/
|
||||
refreshIntervalMs?: number;
|
||||
};
|
||||
|
||||
export const EntityKubernetesContent: (
|
||||
props: EntityKubernetesContentProps,
|
||||
) => JSX.Element = kubernetesPlugin.provide(
|
||||
createRoutableExtension({
|
||||
name: 'EntityKubernetesContent',
|
||||
component: () => import('./Router').then(m => m.Router),
|
||||
|
||||
@@ -50,6 +50,7 @@ export const MyGroupsSidebarItem: (props: {
|
||||
singularTitle: string;
|
||||
pluralTitle: string;
|
||||
icon: IconComponent;
|
||||
filter?: Record<string, string | symbol | (string | symbol)[]>;
|
||||
}) => JSX.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -196,4 +196,92 @@ describe('MyGroupsSidebarItem Test', () => {
|
||||
expect(rendered.getByLabelText('My Squads')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When an additional filter is not provided', () => {
|
||||
it('catalogApi.getEntities() should be called with the default filter', async () => {
|
||||
const identityApi: Partial<IdentityApi> = {
|
||||
getBackstageIdentity: async () => ({
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/guest',
|
||||
ownershipEntityRefs: ['user:default/guest'],
|
||||
}),
|
||||
};
|
||||
const mockCatalogApi: Partial<CatalogApi> = {
|
||||
getEntities: jest.fn(),
|
||||
};
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[identityApiRef, identityApi],
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
]}
|
||||
>
|
||||
<MyGroupsSidebarItem
|
||||
singularTitle="My Squad"
|
||||
pluralTitle="My Squads"
|
||||
icon={GroupIcon}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(mockCatalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: 'group',
|
||||
'relations.hasMember': 'user:default/guest',
|
||||
},
|
||||
],
|
||||
fields: ['metadata', 'kind'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When an additional filter is provided', () => {
|
||||
it('catalogApi.getEntities() should be called with an additional filter item', async () => {
|
||||
const identityApi: Partial<IdentityApi> = {
|
||||
getBackstageIdentity: async () => ({
|
||||
type: 'user',
|
||||
userEntityRef: 'user:default/guest',
|
||||
ownershipEntityRefs: ['user:default/guest'],
|
||||
}),
|
||||
};
|
||||
const mockCatalogApi: Partial<CatalogApi> = {
|
||||
getEntities: jest.fn(),
|
||||
};
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[identityApiRef, identityApi],
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
]}
|
||||
>
|
||||
<MyGroupsSidebarItem
|
||||
singularTitle="My Squad"
|
||||
pluralTitle="My Squads"
|
||||
icon={GroupIcon}
|
||||
filter={{ 'spec.type': 'team' }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(mockCatalogApi.getEntities).toHaveBeenCalledWith({
|
||||
filter: [
|
||||
{
|
||||
kind: 'group',
|
||||
'relations.hasMember': 'user:default/guest',
|
||||
'spec.type': 'team',
|
||||
},
|
||||
],
|
||||
fields: ['metadata', 'kind'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,8 +45,9 @@ export const MyGroupsSidebarItem = (props: {
|
||||
singularTitle: string;
|
||||
pluralTitle: string;
|
||||
icon: IconComponent;
|
||||
filter?: Record<string, string | symbol | (string | symbol)[]>;
|
||||
}) => {
|
||||
const { singularTitle, pluralTitle, icon } = props;
|
||||
const { singularTitle, pluralTitle, icon, filter } = props;
|
||||
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const catalogApi: CatalogApi = useApi(catalogApiRef);
|
||||
@@ -56,7 +57,13 @@ export const MyGroupsSidebarItem = (props: {
|
||||
const profile = await identityApi.getBackstageIdentity();
|
||||
|
||||
const response = await catalogApi.getEntities({
|
||||
filter: [{ kind: 'group', 'relations.hasMember': profile.userEntityRef }],
|
||||
filter: [
|
||||
{
|
||||
kind: 'group',
|
||||
'relations.hasMember': profile.userEntityRef,
|
||||
...(filter ?? {}),
|
||||
},
|
||||
],
|
||||
fields: ['metadata', 'kind'],
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -45,6 +46,7 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
static fromConfig(
|
||||
configApi: ConfigApi,
|
||||
discoveryApi: DiscoveryApi,
|
||||
identityApi: IdentityApi,
|
||||
): PagerDutyClient;
|
||||
// Warning: (ae-forgotten-export) The symbol "ChangeEvent" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
createApiRef,
|
||||
DiscoveryApi,
|
||||
ConfigApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export class UnauthorizedError extends Error {}
|
||||
@@ -38,13 +39,18 @@ export const pagerDutyApiRef = createApiRef<PagerDutyApi>({
|
||||
});
|
||||
|
||||
export class PagerDutyClient implements PagerDutyApi {
|
||||
static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) {
|
||||
static fromConfig(
|
||||
configApi: ConfigApi,
|
||||
discoveryApi: DiscoveryApi,
|
||||
identityApi: IdentityApi,
|
||||
) {
|
||||
const eventsBaseUrl: string =
|
||||
configApi.getOptionalString('pagerDuty.eventsBaseUrl') ??
|
||||
'https://events.pagerduty.com/v2';
|
||||
return new PagerDutyClient({
|
||||
eventsBaseUrl,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
});
|
||||
}
|
||||
constructor(private readonly config: ClientApiConfig) {}
|
||||
@@ -124,11 +130,13 @@ export class PagerDutyClient implements PagerDutyApi {
|
||||
}
|
||||
|
||||
private async getByUrl<T>(url: string): Promise<T> {
|
||||
const { token: idToken } = await this.config.identityApi.getCredentials();
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.pagerduty+json;version=2',
|
||||
'Content-Type': 'application/json',
|
||||
...(idToken && { Authorization: `Bearer ${idToken}` }),
|
||||
},
|
||||
};
|
||||
const response = await this.request(url, options);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Incident, ChangeEvent, OnCall, Service } from '../components/types';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export type TriggerAlarmRequest = {
|
||||
integrationKey: string;
|
||||
@@ -74,6 +74,7 @@ export type OnCallsResponse = {
|
||||
export type ClientApiConfig = {
|
||||
eventsBaseUrl?: string;
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
};
|
||||
|
||||
export type RequestOptions = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
discoveryApiRef,
|
||||
configApiRef,
|
||||
createComponentExtension,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
@@ -32,9 +33,13 @@ export const pagerDutyPlugin = createPlugin({
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: pagerDutyApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, configApi: configApiRef },
|
||||
factory: ({ configApi, discoveryApi }) =>
|
||||
PagerDutyClient.fromConfig(configApi, discoveryApi),
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
configApi: configApiRef,
|
||||
identityApi: identityApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi }) =>
|
||||
PagerDutyClient.fromConfig(configApi, discoveryApi, identityApi),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2022 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 { base64EncodeContent } from './DryRunContext';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { TextEncoder } from 'util';
|
||||
|
||||
window.TextEncoder = TextEncoder;
|
||||
|
||||
describe('base64EncodeContent', () => {
|
||||
it('encodes text files', () => {
|
||||
expect(base64EncodeContent('abc')).toBe('YWJj');
|
||||
expect(base64EncodeContent('abc'.repeat(1000000))).toBe(
|
||||
btoa('<file too large>'),
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes binary files', () => {
|
||||
expect(base64EncodeContent('\x00\x01\x02')).toBe('AAEC');
|
||||
expect(base64EncodeContent('😅')).toBe('8J+YhQ==');
|
||||
// Triggers chunking
|
||||
expect(base64EncodeContent('😅'.repeat(18000))).toBe(
|
||||
'8J+YhfCfmIXwn5iF8J+YhfCfmIXwn5iF'.repeat(3000),
|
||||
);
|
||||
// Triggers size check
|
||||
expect(base64EncodeContent('😅'.repeat(1000000))).toBe(
|
||||
btoa('<file too large>'),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,9 @@ import React, {
|
||||
import { scaffolderApiRef } from '../../api';
|
||||
import { ScaffolderDryRunResponse } from '../../types';
|
||||
|
||||
const MAX_CONTENT_SIZE = 256 * 1024;
|
||||
const CHUNK_SIZE = 0x8000;
|
||||
|
||||
interface DryRunOptions {
|
||||
templateContent: string;
|
||||
values: JsonObject;
|
||||
@@ -54,6 +57,27 @@ interface DryRunProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function base64EncodeContent(content: string): string {
|
||||
if (content.length > MAX_CONTENT_SIZE) {
|
||||
return btoa('<file too large>');
|
||||
}
|
||||
|
||||
try {
|
||||
return btoa(content);
|
||||
} catch {
|
||||
const decoder = new TextEncoder();
|
||||
const buffer = decoder.encode(content);
|
||||
|
||||
const chunks = new Array<string>();
|
||||
for (let offset = 0; offset < buffer.length; offset += CHUNK_SIZE) {
|
||||
chunks.push(
|
||||
String.fromCharCode(...buffer.slice(offset, offset + CHUNK_SIZE)),
|
||||
);
|
||||
}
|
||||
return btoa(chunks.join(''));
|
||||
}
|
||||
}
|
||||
|
||||
export function DryRunProvider(props: DryRunProviderProps) {
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
|
||||
@@ -110,7 +134,7 @@ export function DryRunProvider(props: DryRunProviderProps) {
|
||||
secrets: {},
|
||||
directoryContents: options.files.map(file => ({
|
||||
path: file.path,
|
||||
base64Content: btoa(file.content),
|
||||
base64Content: base64EncodeContent(file.content),
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
@@ -15,17 +15,37 @@ import { Logger } from 'winston';
|
||||
import { SearchEngine } from '@backstage/plugin-search-common';
|
||||
import { SearchQuery } from '@backstage/plugin-search-common';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ElasticSearchClientOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-search-backend-module-elasticsearch" does not have an export "ElasticSearchEngine"
|
||||
//
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchAgentOptions {
|
||||
// (undocumented)
|
||||
keepAlive?: boolean;
|
||||
// (undocumented)
|
||||
keepAliveMsecs?: number;
|
||||
// (undocumented)
|
||||
maxFreeSockets?: number;
|
||||
// (undocumented)
|
||||
maxSockets?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchAuth =
|
||||
| {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
| {
|
||||
apiKey:
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ElasticSearchClientOptions {
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchAgentOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchAuth" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
auth?: ElasticSearchAuth;
|
||||
// (undocumented)
|
||||
@@ -36,8 +56,6 @@ export interface ElasticSearchClientOptions {
|
||||
};
|
||||
// (undocumented)
|
||||
compression?: 'gzip';
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchConnectionConstructor" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
Connection?: ElasticSearchConnectionConstructor;
|
||||
// (undocumented)
|
||||
@@ -50,8 +68,6 @@ export interface ElasticSearchClientOptions {
|
||||
maxRetries?: number;
|
||||
// (undocumented)
|
||||
name?: string | symbol;
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchNodeOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
node?:
|
||||
| string
|
||||
@@ -92,12 +108,35 @@ export interface ElasticSearchClientOptions {
|
||||
ssl?: ConnectionOptions;
|
||||
// (undocumented)
|
||||
suggestCompression?: boolean;
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchTransportConstructor" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
Transport?: ElasticSearchTransportConstructor;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ElasticSearchConcreteQuery = {
|
||||
documentTypes?: string[];
|
||||
elasticSearchQuery: Object;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchConnectionConstructor {
|
||||
// (undocumented)
|
||||
new (opts?: any): any;
|
||||
// (undocumented)
|
||||
roles: {
|
||||
MASTER: string;
|
||||
DATA: string;
|
||||
INGEST: string;
|
||||
ML: string;
|
||||
};
|
||||
// (undocumented)
|
||||
statuses: {
|
||||
ALIVE: string;
|
||||
DEAD: string;
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ElasticSearchHighlightConfig = {
|
||||
fragmentDelimiter: string;
|
||||
@@ -115,6 +154,41 @@ export type ElasticSearchHighlightOptions = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchNodeOptions {
|
||||
// (undocumented)
|
||||
agent?: ElasticSearchAgentOptions;
|
||||
// (undocumented)
|
||||
headers?: Record<string, any>;
|
||||
// (undocumented)
|
||||
id?: string;
|
||||
// (undocumented)
|
||||
roles?: {
|
||||
master: boolean;
|
||||
data: boolean;
|
||||
ingest: boolean;
|
||||
ml: boolean;
|
||||
};
|
||||
// (undocumented)
|
||||
ssl?: ConnectionOptions;
|
||||
// (undocumented)
|
||||
url: URL;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ElasticSearchOptions = {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
aliasPostfix?: string;
|
||||
indexPrefix?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ElasticSearchQueryTranslator = (
|
||||
query: SearchQuery,
|
||||
options?: ElasticSearchQueryTranslatorOptions,
|
||||
) => ElasticSearchConcreteQuery;
|
||||
|
||||
// @public
|
||||
export type ElasticSearchQueryTranslatorOptions = {
|
||||
highlightOptions?: ElasticSearchHighlightConfig;
|
||||
};
|
||||
@@ -128,8 +202,6 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
logger: Logger,
|
||||
highlightOptions?: ElasticSearchHighlightOptions,
|
||||
);
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchOptions" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
static fromConfig({
|
||||
logger,
|
||||
@@ -142,22 +214,16 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
newClient<T>(create: (options: ElasticSearchClientOptions) => T): T;
|
||||
// (undocumented)
|
||||
query(query: SearchQuery): Promise<IndexableResultSet>;
|
||||
// Warning: (ae-forgotten-export) The symbol "ElasticSearchQueryTranslator" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
setTranslator(translator: ElasticSearchQueryTranslator): void;
|
||||
// Warning: (ae-forgotten-export) The symbol "ConcreteElasticSearchQuery" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
protected translator(
|
||||
query: SearchQuery,
|
||||
options?: ElasticSearchQueryTranslatorOptions,
|
||||
): ConcreteElasticSearchQuery;
|
||||
): ElasticSearchConcreteQuery;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
constructor(options: ElasticSearchSearchEngineIndexerOptions);
|
||||
// (undocumented)
|
||||
@@ -170,9 +236,7 @@ export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
initialize(): Promise<void>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
type: string;
|
||||
indexPrefix: string;
|
||||
@@ -181,4 +245,17 @@ export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
logger: Logger;
|
||||
elasticSearchClient: Client;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ElasticSearchTransportConstructor {
|
||||
// (undocumented)
|
||||
new (opts?: any): any;
|
||||
// (undocumented)
|
||||
sniffReasons: {
|
||||
SNIFF_ON_START: string;
|
||||
SNIFF_INTERVAL: string;
|
||||
SNIFF_ON_CONNECTION_FAULT: string;
|
||||
DEFAULT: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
+18
-2
@@ -18,10 +18,12 @@ import type { ConnectionOptions as TLSConnectionOptions } from 'tls';
|
||||
/**
|
||||
* Options used to configure the `@elastic/elasticsearch` client and
|
||||
* are what will be passed as an argument to the
|
||||
* {@link ElasticSearchEngine.newClient} method
|
||||
* {@link ElasticSearchSearchEngine.newClient} method
|
||||
*
|
||||
* They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
|
||||
* but are maintained separately so that this interface is not coupled to
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchClientOptions {
|
||||
provider?: 'aws' | 'elastic';
|
||||
@@ -65,6 +67,9 @@ export interface ElasticSearchClientOptions {
|
||||
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchAuth =
|
||||
| {
|
||||
username: string;
|
||||
@@ -79,6 +84,9 @@ export type ElasticSearchAuth =
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchNodeOptions {
|
||||
url: URL;
|
||||
id?: string;
|
||||
@@ -93,6 +101,9 @@ export interface ElasticSearchNodeOptions {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchAgentOptions {
|
||||
keepAlive?: boolean;
|
||||
keepAliveMsecs?: number;
|
||||
@@ -100,6 +111,9 @@ export interface ElasticSearchAgentOptions {
|
||||
maxFreeSockets?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchConnectionConstructor {
|
||||
new (opts?: any): any;
|
||||
statuses: {
|
||||
@@ -113,7 +127,9 @@ export interface ElasticSearchConnectionConstructor {
|
||||
ML: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ElasticSearchTransportConstructor {
|
||||
new (opts?: any): any;
|
||||
sniffReasons: {
|
||||
|
||||
+7
-7
@@ -19,7 +19,7 @@ import { ConfigReader } from '@backstage/config';
|
||||
import { Client, errors } from '@elastic/elasticsearch';
|
||||
import Mock from '@elastic/elasticsearch-mock';
|
||||
import {
|
||||
ConcreteElasticSearchQuery,
|
||||
ElasticSearchConcreteQuery,
|
||||
decodePageCursor,
|
||||
ElasticSearchSearchEngine,
|
||||
encodePageCursor,
|
||||
@@ -131,7 +131,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
types: ['indexName'],
|
||||
term: 'testTerm',
|
||||
filters: { kind: 'testKind' },
|
||||
}) as ConcreteElasticSearchQuery;
|
||||
}) as ElasticSearchConcreteQuery;
|
||||
|
||||
expect(actualTranslatedQuery).toMatchObject({
|
||||
documentTypes: ['indexName'],
|
||||
@@ -170,7 +170,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
types: ['indexName'],
|
||||
term: 'testTerm',
|
||||
pageCursor: 'MQ==',
|
||||
}) as ConcreteElasticSearchQuery;
|
||||
}) as ElasticSearchConcreteQuery;
|
||||
|
||||
expect(actualTranslatedQuery).toMatchObject({
|
||||
documentTypes: ['indexName'],
|
||||
@@ -210,7 +210,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
foo: 123,
|
||||
bar: true,
|
||||
},
|
||||
}) as ConcreteElasticSearchQuery;
|
||||
}) as ElasticSearchConcreteQuery;
|
||||
|
||||
expect(actualTranslatedQuery).toMatchObject({
|
||||
documentTypes: ['indexName'],
|
||||
@@ -266,7 +266,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
types: ['indexName'],
|
||||
term: 'testTerm',
|
||||
filters: { kind: ['testKind', 'kastTeint'] },
|
||||
}) as ConcreteElasticSearchQuery;
|
||||
}) as ElasticSearchConcreteQuery;
|
||||
|
||||
expect(actualTranslatedQuery).toMatchObject({
|
||||
documentTypes: ['indexName'],
|
||||
@@ -327,7 +327,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
fragmentDelimiter: ' ... ',
|
||||
},
|
||||
},
|
||||
) as ConcreteElasticSearchQuery;
|
||||
) as ElasticSearchConcreteQuery;
|
||||
|
||||
expect(actualTranslatedQuery).toMatchObject({
|
||||
documentTypes: ['indexName'],
|
||||
@@ -369,7 +369,7 @@ describe('ElasticSearchSearchEngine', () => {
|
||||
types: ['indexName'],
|
||||
term: 'testTerm',
|
||||
filters: { kind: { a: 'b' } },
|
||||
}) as ConcreteElasticSearchQuery;
|
||||
}) as ElasticSearchConcreteQuery;
|
||||
expect(actualTranslatedQuery).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
+18
-5
@@ -36,25 +36,38 @@ import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineInd
|
||||
|
||||
export type { ElasticSearchClientOptions };
|
||||
|
||||
export type ConcreteElasticSearchQuery = {
|
||||
/**
|
||||
* Search query that the elasticsearch engine understands.
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchConcreteQuery = {
|
||||
documentTypes?: string[];
|
||||
elasticSearchQuery: Object;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options available for the Elasticsearch specific query translator.
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchQueryTranslatorOptions = {
|
||||
highlightOptions?: ElasticSearchHighlightConfig;
|
||||
};
|
||||
|
||||
type ElasticSearchQueryTranslator = (
|
||||
/**
|
||||
* Elasticsearch specific query translator.
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchQueryTranslator = (
|
||||
query: SearchQuery,
|
||||
options?: ElasticSearchQueryTranslatorOptions,
|
||||
) => ConcreteElasticSearchQuery;
|
||||
) => ElasticSearchConcreteQuery;
|
||||
|
||||
type ElasticSearchOptions = {
|
||||
/**
|
||||
* Options for instansiate ElasticSearchSearchEngine
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchOptions = {
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
aliasPostfix?: string;
|
||||
@@ -161,7 +174,7 @@ export class ElasticSearchSearchEngine implements SearchEngine {
|
||||
protected translator(
|
||||
query: SearchQuery,
|
||||
options?: ElasticSearchQueryTranslatorOptions,
|
||||
): ConcreteElasticSearchQuery {
|
||||
): ElasticSearchConcreteQuery {
|
||||
const { term, filters = {}, types, pageCursor } = query;
|
||||
|
||||
const filter = Object.entries(filters)
|
||||
|
||||
+8
@@ -20,6 +20,10 @@ import { Client } from '@elastic/elasticsearch';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/**
|
||||
* Options for instansiate ElasticSearchSearchEngineIndexer
|
||||
* @public
|
||||
*/
|
||||
export type ElasticSearchSearchEngineIndexerOptions = {
|
||||
type: string;
|
||||
indexPrefix: string;
|
||||
@@ -35,6 +39,10 @@ function duration(startTimestamp: [number, number]): string {
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch specific search engine indexer.
|
||||
* @public
|
||||
*/
|
||||
export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
private received: number = 0;
|
||||
private processed: number = 0;
|
||||
|
||||
@@ -16,11 +16,20 @@
|
||||
|
||||
export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine';
|
||||
export type {
|
||||
ConcreteElasticSearchQuery,
|
||||
ElasticSearchAgentOptions,
|
||||
ElasticSearchConnectionConstructor,
|
||||
ElasticSearchTransportConstructor,
|
||||
ElasticSearchNodeOptions,
|
||||
ElasticSearchAuth,
|
||||
} from './ElasticSearchClientOptions';
|
||||
export type {
|
||||
ElasticSearchConcreteQuery,
|
||||
ElasticSearchClientOptions,
|
||||
ElasticSearchHighlightConfig,
|
||||
ElasticSearchHighlightOptions,
|
||||
ElasticSearchQueryTranslator,
|
||||
ElasticSearchQueryTranslatorOptions,
|
||||
ElasticSearchOptions,
|
||||
} from './ElasticSearchSearchEngine';
|
||||
export type {
|
||||
ElasticSearchSearchEngineIndexer,
|
||||
|
||||
@@ -22,10 +22,18 @@
|
||||
|
||||
export { ElasticSearchSearchEngine } from './engines';
|
||||
export type {
|
||||
ElasticSearchAgentOptions,
|
||||
ElasticSearchConcreteQuery,
|
||||
ElasticSearchClientOptions,
|
||||
ElasticSearchHighlightConfig,
|
||||
ElasticSearchHighlightOptions,
|
||||
ElasticSearchQueryTranslator,
|
||||
ElasticSearchQueryTranslatorOptions,
|
||||
ElasticSearchConnectionConstructor,
|
||||
ElasticSearchTransportConstructor,
|
||||
ElasticSearchNodeOptions,
|
||||
ElasticSearchOptions,
|
||||
ElasticSearchAuth,
|
||||
ElasticSearchSearchEngineIndexer,
|
||||
ElasticSearchSearchEngineIndexerOptions,
|
||||
} from './engines';
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Transform } from 'stream';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export abstract class BatchSearchEngineIndexer extends Writable {
|
||||
constructor(options: BatchSearchEngineOptions);
|
||||
abstract finalize(): Promise<void>;
|
||||
@@ -32,19 +32,19 @@ export abstract class BatchSearchEngineIndexer extends Writable {
|
||||
abstract initialize(): Promise<void>;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export type BatchSearchEngineOptions = {
|
||||
batchSize: number;
|
||||
};
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export type ConcreteLunrQuery = {
|
||||
lunrQueryBuilder: lunr_2.Index.QueryBuilder;
|
||||
documentTypes?: string[];
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export abstract class DecoratorBase extends Transform {
|
||||
constructor();
|
||||
abstract decorate(
|
||||
@@ -54,7 +54,7 @@ export abstract class DecoratorBase extends Transform {
|
||||
abstract initialize(): Promise<void>;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export class IndexBuilder {
|
||||
constructor({ logger, searchEngine }: IndexBuilderOptions);
|
||||
addCollator({ factory, schedule }: RegisterCollatorParameters): void;
|
||||
@@ -62,22 +62,20 @@ export class IndexBuilder {
|
||||
build(): Promise<{
|
||||
scheduler: Scheduler;
|
||||
}>;
|
||||
// (undocumented)
|
||||
getDocumentTypes(): Record<string, DocumentTypeInfo>;
|
||||
// (undocumented)
|
||||
getSearchEngine(): SearchEngine;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export type IndexBuilderOptions = {
|
||||
searchEngine: SearchEngine;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export class LunrSearchEngine implements SearchEngine {
|
||||
constructor({ logger }: { logger: Logger });
|
||||
// (undocumented)
|
||||
@@ -100,7 +98,7 @@ export class LunrSearchEngine implements SearchEngine {
|
||||
protected translator: QueryTranslator;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
constructor();
|
||||
// (undocumented)
|
||||
@@ -115,7 +113,7 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
initialize(): Promise<void>;
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export class NewlineDelimitedJsonCollatorFactory
|
||||
implements DocumentCollatorFactory
|
||||
{
|
||||
@@ -131,7 +129,7 @@ export class NewlineDelimitedJsonCollatorFactory
|
||||
readonly visibilityPermission: Permission | undefined;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export type NewlineDelimitedJsonCollatorFactoryOptions = {
|
||||
type: string;
|
||||
searchPattern: string;
|
||||
@@ -140,18 +138,18 @@ export type NewlineDelimitedJsonCollatorFactoryOptions = {
|
||||
visibilityPermission?: Permission;
|
||||
};
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface RegisterCollatorParameters {
|
||||
factory: DocumentCollatorFactory;
|
||||
schedule: TaskRunner;
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface RegisterDecoratorParameters {
|
||||
factory: DocumentDecoratorFactory;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public
|
||||
export class Scheduler {
|
||||
constructor({ logger }: { logger: Logger });
|
||||
addToSchedule({ id, task, scheduledRunner }: ScheduleTaskParameters): void;
|
||||
@@ -160,25 +158,22 @@ export class Scheduler {
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ScheduleTaskParameters {
|
||||
// (undocumented)
|
||||
export type ScheduleTaskParameters = {
|
||||
id: string;
|
||||
// (undocumented)
|
||||
scheduledRunner: TaskRunner;
|
||||
// (undocumented)
|
||||
task: TaskFunction;
|
||||
}
|
||||
scheduledRunner: TaskRunner;
|
||||
};
|
||||
|
||||
export { SearchEngine };
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export class TestPipeline {
|
||||
execute(): Promise<TestPipelineResult>;
|
||||
withDocuments(documents: IndexableDocument[]): TestPipeline;
|
||||
static withSubject(subject: Readable | Transform | Writable): TestPipeline;
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export type TestPipelineResult = {
|
||||
error: unknown;
|
||||
documents: IndexableDocument[];
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Used for adding collators, decorators and compile them into tasks which are added to a scheduler returned to the caller.
|
||||
* @public
|
||||
*/
|
||||
export class IndexBuilder {
|
||||
private collators: Record<string, RegisterCollatorParameters>;
|
||||
@@ -45,10 +46,16 @@ export class IndexBuilder {
|
||||
this.searchEngine = searchEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for returning the registered search engine.
|
||||
*/
|
||||
getSearchEngine(): SearchEngine {
|
||||
return this.searchEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for returning the registered document types.
|
||||
*/
|
||||
getDocumentTypes(): Record<string, DocumentTypeInfo> {
|
||||
return this.documentTypes;
|
||||
}
|
||||
|
||||
@@ -24,16 +24,18 @@ type TaskEnvelope = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @public ScheduleTaskParameters
|
||||
* ScheduleTaskParameters
|
||||
* @public
|
||||
*/
|
||||
export interface ScheduleTaskParameters {
|
||||
export type ScheduleTaskParameters = {
|
||||
id: string;
|
||||
task: TaskFunction;
|
||||
scheduledRunner: TaskRunner;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Scheduler responsible for all search tasks.
|
||||
* @public
|
||||
*/
|
||||
export class Scheduler {
|
||||
private logger: Logger;
|
||||
|
||||
@@ -23,7 +23,8 @@ import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Options for instansiate NewlineDelimitedJsonCollatorFactory
|
||||
* @public
|
||||
*/
|
||||
export type NewlineDelimitedJsonCollatorFactoryOptions = {
|
||||
type: string;
|
||||
@@ -60,15 +61,13 @@ export type NewlineDelimitedJsonCollatorFactoryOptions = {
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export class NewlineDelimitedJsonCollatorFactory
|
||||
implements DocumentCollatorFactory
|
||||
{
|
||||
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory."type"} */
|
||||
readonly type: string;
|
||||
|
||||
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.visibilityPermission} */
|
||||
public readonly visibilityPermission: Permission | undefined;
|
||||
|
||||
private constructor(
|
||||
@@ -123,7 +122,6 @@ export class NewlineDelimitedJsonCollatorFactory
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc @backstage/plugin-search-common#DocumentCollatorFactory.getCollator} */
|
||||
async getCollator(): Promise<Readable> {
|
||||
// Search for files matching the given pattern.
|
||||
const lastUrl = await this.lastUrl();
|
||||
|
||||
@@ -27,7 +27,8 @@ import { Logger } from 'winston';
|
||||
import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Type of translated query for the Lunr Search Engine.
|
||||
* @public
|
||||
*/
|
||||
export type ConcreteLunrQuery = {
|
||||
lunrQueryBuilder: lunr.Index.QueryBuilder;
|
||||
@@ -41,12 +42,14 @@ type LunrResultEnvelope = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Translator repsonsible for translating search term and filters to a query that the Lunr Search Engine understands.
|
||||
* @public
|
||||
*/
|
||||
export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Lunr specific search engine implementation.
|
||||
* @public
|
||||
*/
|
||||
export class LunrSearchEngine implements SearchEngine {
|
||||
protected lunrIndices: Record<string, lunr.Index> = {};
|
||||
|
||||
@@ -19,7 +19,8 @@ import lunr from 'lunr';
|
||||
import { BatchSearchEngineIndexer } from '../indexing';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Lunr specific search engine indexer
|
||||
* @public
|
||||
*/
|
||||
export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer {
|
||||
private schemaInitialized = false;
|
||||
|
||||
@@ -19,7 +19,8 @@ import { IndexableDocument } from '@backstage/plugin-search-common';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Options for {@link BatchSearchEngineIndexer}
|
||||
* @public
|
||||
*/
|
||||
export type BatchSearchEngineOptions = {
|
||||
batchSize: number;
|
||||
@@ -28,7 +29,7 @@ export type BatchSearchEngineOptions = {
|
||||
/**
|
||||
* Base class encapsulating batch-based stream processing. Useful as a base
|
||||
* class for search engine indexers.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export abstract class BatchSearchEngineIndexer extends Writable {
|
||||
private batchSize: number;
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Transform } from 'stream';
|
||||
/**
|
||||
* Base class encapsulating simple async transformations. Useful as a base
|
||||
* class for Backstage search decorators.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export abstract class DecoratorBase extends Transform {
|
||||
private initialized: Promise<undefined | Error>;
|
||||
|
||||
@@ -19,7 +19,7 @@ import { pipeline, Readable, Transform, Writable } from 'stream';
|
||||
|
||||
/**
|
||||
* Object resolved after a test pipeline is executed.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type TestPipelineResult = {
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ export type TestPipelineResult = {
|
||||
|
||||
/**
|
||||
* Test utility for Backstage Search collators, decorators, and indexers.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export class TestPipeline {
|
||||
private collator?: Readable;
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
import { Logger } from 'winston';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Options required to instantiate the index builder.
|
||||
* @public
|
||||
*/
|
||||
export type IndexBuilderOptions = {
|
||||
searchEngine: SearchEngine;
|
||||
@@ -32,7 +33,7 @@ export type IndexBuilderOptions = {
|
||||
|
||||
/**
|
||||
* Parameters required to register a collator.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface RegisterCollatorParameters {
|
||||
/**
|
||||
@@ -48,7 +49,7 @@ export interface RegisterCollatorParameters {
|
||||
|
||||
/**
|
||||
* Parameters required to register a decorator
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface RegisterDecoratorParameters {
|
||||
/**
|
||||
|
||||
@@ -11,13 +11,9 @@ import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import { SearchEngine } from '@backstage/plugin-search-backend-node';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function createRouter(options: RouterOptions): Promise<express.Router>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type RouterOptions = {
|
||||
engine: SearchEngine;
|
||||
|
||||
@@ -51,6 +51,9 @@ const jsonObjectSchema: z.ZodSchema<JsonObject> = z.lazy(() => {
|
||||
return z.record(jsonValueSchema);
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type RouterOptions = {
|
||||
engine: SearchEngine;
|
||||
types: Record<string, DocumentTypeInfo>;
|
||||
@@ -61,6 +64,9 @@ export type RouterOptions = {
|
||||
|
||||
const allowedLocationProtocols = ['http:', 'https:'];
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
|
||||
@@ -11,46 +11,46 @@ import { Readable } from 'stream';
|
||||
import { Transform } from 'stream';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface DocumentCollatorFactory {
|
||||
getCollator(): Promise<Readable>;
|
||||
readonly type: string;
|
||||
readonly visibilityPermission?: Permission;
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface DocumentDecoratorFactory {
|
||||
getDecorator(): Promise<Transform>;
|
||||
readonly types?: string[];
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export type DocumentTypeInfo = {
|
||||
visibilityPermission?: Permission;
|
||||
};
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export type IndexableDocument = SearchDocument & {
|
||||
authorization?: {
|
||||
resourceRef: string;
|
||||
};
|
||||
};
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export type IndexableResult = Result<IndexableDocument>;
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export type IndexableResultSet = ResultSet<IndexableDocument>;
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export type QueryRequestOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export type QueryTranslator = (query: SearchQuery) => unknown;
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export interface Result<TDocument extends SearchDocument> {
|
||||
// (undocumented)
|
||||
document: TDocument;
|
||||
@@ -60,7 +60,7 @@ export interface Result<TDocument extends SearchDocument> {
|
||||
type: string;
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface ResultHighlight {
|
||||
// (undocumented)
|
||||
fields: {
|
||||
@@ -70,7 +70,7 @@ export interface ResultHighlight {
|
||||
preTag: string;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export interface ResultSet<TDocument extends SearchDocument> {
|
||||
// (undocumented)
|
||||
nextPageCursor?: string;
|
||||
@@ -80,14 +80,14 @@ export interface ResultSet<TDocument extends SearchDocument> {
|
||||
results: Result<TDocument>[];
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface SearchDocument {
|
||||
location: string;
|
||||
text: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// @beta
|
||||
// @public
|
||||
export interface SearchEngine {
|
||||
getIndexer(type: string): Promise<Writable>;
|
||||
query(
|
||||
@@ -97,7 +97,7 @@ export interface SearchEngine {
|
||||
setTranslator(translator: QueryTranslator): void;
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export interface SearchQuery {
|
||||
// (undocumented)
|
||||
filters?: JsonObject;
|
||||
@@ -109,9 +109,9 @@ export interface SearchQuery {
|
||||
types?: string[];
|
||||
}
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export type SearchResult = Result<SearchDocument>;
|
||||
|
||||
// @beta (undocumented)
|
||||
// @public (undocumented)
|
||||
export type SearchResultSet = ResultSet<SearchDocument>;
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ import { JsonObject } from '@backstage/types';
|
||||
import { Readable, Transform, Writable } from 'stream';
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface SearchQuery {
|
||||
term: string;
|
||||
@@ -29,10 +29,10 @@ export interface SearchQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* Metadata for result relevant document fields with matched terms highlighted
|
||||
* via wrapping in associated pre/post tags. The UI is expected to parse these
|
||||
* field excerpts by replacing wrapping tags with applicable UI elements for rendering.
|
||||
* @public
|
||||
*/
|
||||
export interface ResultHighlight {
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ export interface ResultHighlight {
|
||||
}
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface Result<TDocument extends SearchDocument> {
|
||||
type: string;
|
||||
@@ -62,7 +62,7 @@ export interface Result<TDocument extends SearchDocument> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface ResultSet<TDocument extends SearchDocument> {
|
||||
results: Result<TDocument>[];
|
||||
@@ -71,28 +71,28 @@ export interface ResultSet<TDocument extends SearchDocument> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type SearchResult = Result<SearchDocument>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type SearchResultSet = ResultSet<SearchDocument>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type IndexableResult = Result<IndexableDocument>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type IndexableResultSet = ResultSet<IndexableDocument>;
|
||||
|
||||
/**
|
||||
* Base properties that all search documents must include.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface SearchDocument {
|
||||
/**
|
||||
@@ -117,7 +117,7 @@ export interface SearchDocument {
|
||||
* backends working directly with documents being inserted or retrieved from
|
||||
* search indexes. When dealing with documents in the frontend, use
|
||||
* {@link SearchDocument}.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type IndexableDocument = SearchDocument & {
|
||||
/**
|
||||
@@ -136,7 +136,7 @@ export type IndexableDocument = SearchDocument & {
|
||||
* Information about a specific document type. Intended to be used in the
|
||||
* {@link @backstage/plugin-search-backend-node#IndexBuilder} to collect information
|
||||
* about the types stored in the index.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type DocumentTypeInfo = {
|
||||
/**
|
||||
@@ -148,7 +148,7 @@ export type DocumentTypeInfo = {
|
||||
|
||||
/**
|
||||
* Factory class for instantiating collators.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface DocumentCollatorFactory {
|
||||
/**
|
||||
@@ -171,7 +171,7 @@ export interface DocumentCollatorFactory {
|
||||
|
||||
/**
|
||||
* Factory class for instantiating decorators.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface DocumentDecoratorFactory {
|
||||
/**
|
||||
@@ -190,13 +190,13 @@ export interface DocumentDecoratorFactory {
|
||||
/**
|
||||
* A type of function responsible for translating an abstract search query into
|
||||
* a concrete query relevant to a particular search engine.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type QueryTranslator = (query: SearchQuery) => unknown;
|
||||
|
||||
/**
|
||||
* Options when querying a search engine.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export type QueryRequestOptions = {
|
||||
token?: string;
|
||||
@@ -206,7 +206,7 @@ export type QueryRequestOptions = {
|
||||
* Interface that must be implemented by specific search engines, responsible
|
||||
* for performing indexing and querying and translating abstract queries into
|
||||
* concrete, search engine-specific queries.
|
||||
* @beta
|
||||
* @public
|
||||
*/
|
||||
export interface SearchEngine {
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,7 @@ export type SearchModalValue = {
|
||||
|
||||
const SearchModalContext = createVersionedContext<{
|
||||
1: SearchModalValue | undefined;
|
||||
}>('analytics-context');
|
||||
}>('search-modal-context');
|
||||
|
||||
/**
|
||||
* Props for the SearchModalProvider.
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@material-ui/styles": "^4.10.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"rc-progress": "3.3.2",
|
||||
"rc-progress": "3.3.3",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -159,6 +159,37 @@ describe('DocsSynchronizer', () => {
|
||||
expect(DocsBuilder.prototype.build).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should limit concurrent updates', async () => {
|
||||
// Given a build implementation that runs long...
|
||||
MockedDocsBuilder.prototype.build.mockImplementation(
|
||||
() => new Promise(() => {}),
|
||||
);
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(true);
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
uid: '0',
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
// When more than 10 syncs are attempted...
|
||||
for (let i = 0; i < 12; i++) {
|
||||
docsSynchronizer.doSync({
|
||||
responseHandler: mockResponseHandler,
|
||||
entity,
|
||||
preparers,
|
||||
generators,
|
||||
});
|
||||
}
|
||||
|
||||
// Then still only 10 builds should have been triggered.
|
||||
await new Promise<void>(resolve => resolve());
|
||||
expect(DocsBuilder.prototype.build).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it('should not check for an update too often', async () => {
|
||||
(shouldCheckForUpdate as jest.Mock).mockReturnValue(false);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
PublisherBase,
|
||||
} from '@backstage/plugin-techdocs-node';
|
||||
import fetch from 'node-fetch';
|
||||
import pLimit, { Limit } from 'p-limit';
|
||||
import { PassThrough } from 'stream';
|
||||
import * as winston from 'winston';
|
||||
import { TechDocsCache } from '../cache';
|
||||
@@ -47,6 +48,7 @@ export class DocsSynchronizer {
|
||||
private readonly config: Config;
|
||||
private readonly scmIntegrations: ScmIntegrationRegistry;
|
||||
private readonly cache: TechDocsCache | undefined;
|
||||
private readonly buildLimiter: Limit;
|
||||
|
||||
constructor({
|
||||
publisher,
|
||||
@@ -69,6 +71,9 @@ export class DocsSynchronizer {
|
||||
this.publisher = publisher;
|
||||
this.scmIntegrations = scmIntegrations;
|
||||
this.cache = cache;
|
||||
|
||||
// Single host/process: limit concurrent builds up to 10 at a time.
|
||||
this.buildLimiter = pLimit(10);
|
||||
}
|
||||
|
||||
async doSync({
|
||||
@@ -123,7 +128,7 @@ export class DocsSynchronizer {
|
||||
cache: this.cache,
|
||||
});
|
||||
|
||||
const updated = await docsBuilder.build();
|
||||
const updated = await this.buildLimiter(() => docsBuilder.build());
|
||||
|
||||
if (!updated) {
|
||||
finish({ updated: false });
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
TSDocTagSyntaxKind,
|
||||
} from '@microsoft/tsdoc';
|
||||
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
|
||||
import { ApiPackage, ApiModel } from '@microsoft/api-extractor-model';
|
||||
import { ApiPackage, ApiModel, ApiItem } from '@microsoft/api-extractor-model';
|
||||
import {
|
||||
IMarkdownDocumenterOptions,
|
||||
MarkdownDocumenter,
|
||||
@@ -256,7 +256,9 @@ const NO_WARNING_PACKAGES = [
|
||||
'plugins/scaffolder-backend-module-rails',
|
||||
'plugins/scaffolder-backend-module-yeoman',
|
||||
'plugins/scaffolder-common',
|
||||
'plugins/search-backend',
|
||||
'plugins/search-backend-node',
|
||||
'plugins/search-backend-module-elasticsearch',
|
||||
'plugins/search-common',
|
||||
'plugins/search-react',
|
||||
'plugins/techdocs',
|
||||
@@ -939,6 +941,16 @@ async function buildDocs({
|
||||
this._markdownEmitter = new CustomCustomMarkdownEmitter(newModel);
|
||||
}
|
||||
|
||||
private _getFilenameForApiItem(apiItem: ApiItem): string {
|
||||
const filename: string = super._getFilenameForApiItem(apiItem);
|
||||
|
||||
if (filename.includes('.html.')) {
|
||||
return filename.replace(/\.html\./g, '._html.');
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// We don't really get many chances to modify the generated AST
|
||||
// so we hook in wherever we can. In this case we add the front matter
|
||||
// just before writing the breadcrumbs at the top.
|
||||
|
||||
+1928
-1567
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user