Merge branch 'master' of github.com:backstage/backstage into RoadieHQ-tttentity-peek-ahead

This commit is contained in:
Brian Fletcher
2022-12-22 15:31:22 +00:00
147 changed files with 3477 additions and 888 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Fixed an issue where the CLI would fail to function when there was a mix of workspace and non-workspace versions of the same package in `yarn.lock` when using Yarn 3.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
rollback `@rjsf/validator-ajv8` to `@rjsf/validator-v6`
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-scaffolder-common': patch
'@backstage/plugin-search-backend-module-elasticsearch': patch
'@backstage/plugin-search-backend-node': patch
---
Fixed spelling mistakes in documentation.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': minor
---
Implemented support for the `order` directive on `getEntities`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The frontend serve task now filters out allowed package duplicates during its package check, just like `versions:bump` and `versions:check`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Fixed bug in `EntityTagPicker` that filtered on unavailable tags for the selected kind.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Applied fix from v1.9.1
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-stack-overflow-backend': minor
---
Enable configuration override for StackOverflow backend plugin when instantiating the search indexer. This makes it possible to set different configuration for frontend and backend of the plugin.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Updated dependency `@microsoft/tsdoc-config` to `0.16.2`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Fixed a bug that could cause an indexing process to silently fail, timeout, and accumulate stale indices.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Fixed a bug that could cause the backstage backend to unexpectedly terminate when client errors were encountered during the indexing process.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-module-elasticsearch': patch
---
Improved index throughput by optimizing when and how many documents were made available to the bulk client.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': minor
---
Implemented server side ordering in the entities endpoint
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The `repo test`, `repo lint`, and `repo build` commands will now analyze `yarn.lock` for dependency changes when searching for changed packages. This allows you to use the `--since <ref>` flag even if you have `yarn.lock` changes.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Workspace ranges are no longer considered invalid by version commands.
+3 -3
View File
@@ -8,11 +8,11 @@ x-uffizzi:
services:
backstage:
image: '${BACKSTAGE_IMAGE}'
environment:
POSTGRES_HOST: localhost
POSTGRES_HOST: db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
GITHUB_TOKEN: abc
NODE_ENV: production
deploy:
resources:
@@ -21,7 +21,7 @@ services:
entrypoint: '/bin/sh'
command:
- '-c'
- "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml --config app-config.production.yaml"
- "APP_CONFIG_app_baseUrl=$$UFFIZZI_URL APP_CONFIG_backend_baseUrl=$$UFFIZZI_URL APP_CONFIG_auth_environment='production' node packages/backend --config app-config.yaml"
db:
image: postgres
@@ -2,9 +2,258 @@ app:
title: Backstage Uffizzi Environment
baseUrl: ${UFFIZZI_URL}
organization:
name: My Company
backend:
baseUrl: ${UFFIZZI_URL}
auth:
keys:
# random mock key for uffizi deployments
- secret: 5TXvdjVZFxF7qf9K5RAYRDoGrLzJooqa
listen:
port: 7007
database:
client: better-sqlite3
connection: ':memory:'
cache:
store: memory
cors:
origin: ${UFFIZZI_URL}
methods: [GET, POST, PUT, DELETE]
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
# Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference
# Default Helmet Content-Security-Policy values can be removed by setting the key to false
auth:
environment: production
providers: {}
catalog:
locations:
- type: url
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
headers:
Circle-Token: ${CIRCLECI_AUTH_TOKEN}
'/jenkins/api':
target: http://localhost:8080
headers:
Authorization: ${JENKINS_BASIC_AUTH_HEADER}
'/travisci/api':
target: https://api.travis-ci.com
changeOrigin: true
headers:
Authorization: ${TRAVISCI_AUTH_TOKEN}
travis-api-version: '3'
'/newrelic/apm/api':
target: https://api.newrelic.com/v2
headers:
X-Api-Key: ${NEW_RELIC_REST_API_KEY}
'/newrelic/api':
target: https://api.newrelic.com
headers:
X-Api-Key: ${NEW_RELIC_USER_KEY}
'/pagerduty':
target: https://api.pagerduty.com
headers:
Authorization: Token token=${PAGERDUTY_TOKEN}
'/buildkite/api':
target: https://api.buildkite.com/v2/
headers:
Authorization: ${BUILDKITE_TOKEN}
'/sentry/api':
target: https://sentry.io/api/
allowedMethods: ['GET']
headers:
Authorization: ${SENTRY_TOKEN}
'/ilert':
target: https://api.ilert.com
allowedMethods: ['GET', 'POST', 'PUT']
allowedHeaders: ['Authorization']
headers:
Authorization: ${ILERT_AUTH_HEADER}
'/airflow':
target: https://your.airflow.instance.com/api/v1
headers:
Authorization: ${AIRFLOW_BASIC_AUTH_HEADER}
'/gocd':
target: https://your.gocd.instance.com/go/api
allowedMethods: ['GET']
allowedHeaders: ['Authorization']
headers:
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
'/dynatrace':
target: https://your.dynatrace.instance.com/api/v2
headers:
Authorization: 'Api-Token ${DYNATRACE_ACCESS_TOKEN}'
techdocs:
builder: 'local' # Alternatives - 'external'
generator:
runIn: 'docker'
# dockerImage: my-org/techdocs # use a custom docker image
# pullImage: true # or false to disable automatic pulling of image (e.g. if custom docker login is required)
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3' or 'azureBlobStorage' or 'openStackSwift'. Read documentation for using alternatives.
dynatrace:
baseUrl: https://your.dynatrace.instance.com
# Score-cards sample configuration.
scorecards:
jsonDataUrl: https://raw.githubusercontent.com/Oriflame/backstage-plugins/main/plugins/score-card/sample-data/
wikiLinkTemplate: https://link-to-wiki/{id}
sentry:
organization: my-company
rollbar:
organization: my-company
# NOTE: The rollbar-backend & accountToken key may be deprecated in the future (replaced by a proxy config)
accountToken: my-rollbar-account-token
lighthouse:
baseUrl: http://localhost:3003
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters: []
kafka:
clientId: backstage
clusters:
- name: cluster
dashboardUrl: https://akhq.io/
brokers:
- localhost:9092
allure:
baseUrl: http://localhost:5050/allure-docker-service
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token: ${GHE_TOKEN}
### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional):
# - host: ghe.example.net
# rawBaseUrl: https://ghe.example.net/raw
# token: ${GHE_TOKEN}
gitlab:
- host: gitlab.com
token: ${GITLAB_TOKEN}
### Example for how to add a bitbucket cloud integration
# bitbucketCloud:
# - username: ${BITBUCKET_USERNAME}
# appPassword: ${BITBUCKET_APP_PASSWORD}
### Example for how to add your bitbucket server instance using the API:
# - host: server.bitbucket.com
# apiBaseUrl: server.bitbucket.com
# username: ${BITBUCKET_SERVER_USERNAME}
# appPassword: ${BITBUCKET_SERVER_APP_PASSWORD}
azure:
- host: dev.azure.com
token: ${AZURE_TOKEN}
# googleGcs:
# clientEmail: 'example@example.com'
# privateKey: ${GCS_PRIVATE_KEY}
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
costInsights:
engineerCost: 200000
engineerThreshold: 0.5
products:
computeEngine:
name: Compute Engine
icon: compute
cloudDataflow:
name: Cloud Dataflow
icon: data
cloudStorage:
name: Cloud Storage
icon: storage
bigQuery:
name: BigQuery
icon: search
events:
name: Events
icon: data
metrics:
DAU:
name: Daily Active Users
default: true
MSC:
name: Monthly Subscribers
currencies:
engineers:
label: 'Engineers 🛠'
unit: 'engineer'
usd:
label: 'US Dollars 💵'
kind: 'USD'
unit: 'dollar'
prefix: '$'
rate: 1
carbonOffsetTons:
label: 'Carbon Offset Tons ♻️⚖️s'
kind: 'CARBON_OFFSET_TONS'
unit: 'carbon offset ton'
rate: 3.5
beers:
label: 'Beers 🍺'
kind: 'BEERS'
unit: 'beer'
rate: 4.5
pintsIceCream:
label: 'Pints of Ice Cream 🍦'
kind: 'PINTS_OF_ICE_CREAM'
unit: 'ice cream pint'
rate: 5.5
pagerduty:
eventsBaseUrl: 'https://events.pagerduty.com/v2'
jenkins:
instances:
- name: default
baseUrl: https://jenkins.example.com
username: backstage-bot
apiKey: 123456789abcdef0123456789abcedf012
azureDevOps:
host: dev.azure.com
token: my-token
organization: my-company
apacheAirflow:
baseUrl: https://your.airflow.instance.com
gocd:
baseUrl: https://your.gocd.instance.com
permission:
enabled: true
-22
View File
@@ -185,21 +185,10 @@ jobs:
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
- name: check for yarn.lock changes
id: yarn-lock
run: git diff --quiet origin/master HEAD -- yarn.lock
continue-on-error: true
- name: lint changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn backstage-cli repo lint --since origin/master
- name: lint all packages
if: ${{ steps.yarn-lock.outcome == 'failure' }}
run: yarn backstage-cli repo lint
- name: test changed packages
if: ${{ steps.yarn-lock.outcome == 'success' }}
run: yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --since origin/master
env:
BACKSTAGE_TEST_DISABLE_DOCKER: 1
@@ -207,17 +196,6 @@ jobs:
BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
- name: test all packages (and upload coverage)
if: ${{ steps.yarn-lock.outcome == 'failure' }}
run: |
yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=800M --coverage
bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD)
env:
BACKSTAGE_TEST_DISABLE_DOCKER: 1
BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }}
BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }}
BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored
- name: ensure clean working directory
run: |
if files=$(git ls-files --exclude-standard --others --modified) && [[ -z "$files" ]]; then
+18 -16
View File
@@ -8,6 +8,9 @@ on:
jobs:
build-backstage:
env:
NODE_OPTIONS: --max-old-space-size=4096
UFFIZZI_URL: https://uffizzi.com
name: Build PR image
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
@@ -16,8 +19,6 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
with:
path: backstage
- name: setup-node
uses: actions/setup-node@v3
@@ -25,21 +26,22 @@ jobs:
node-version: 16.x
registry-url: https://registry.npmjs.org/
- name: backstage create-app | delete if already exists
run: |
rm -rf ./example-app
npx @backstage/create-app
env:
BACKSTAGE_APP_NAME: example-app
- name: yarn build
run: |
yarn build:all
working-directory: ./example-app
- name: yarn install
uses: backstage/actions/yarn-install@v0.5.12
with:
cache-prefix: linux-v16
- name: Use Uffizzi's backstage app config
run: |
cp ./backstage/.github/uffizzi/uffizzi.production.app-config.yaml ./example-app/app-config.production.yaml;
cp -f ./.github/uffizzi/uffizzi.production.app-config.yaml ./app-config.yaml
- name: typescript build
run: |
yarn tsc
- name: backstage build
run: |
yarn workspace example-backend build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -58,8 +60,8 @@ jobs:
- name: Build Image
uses: docker/build-push-action@v2
with:
context: example-app
file: example-app/packages/backend/Dockerfile
context: .
file: packages/backend/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
+4
View File
@@ -37,6 +37,8 @@ auth:
audience: ${AUTH_AUTH0_AUDIENCE}
connection: ${AUTH_AUTH0_CONNECTION}
connectionScope: ${AUTH_AUTH0_CONNECTION_SCOPE}
session:
secret: ${AUTH_SESSION_SECRET}
```
The Auth0 provider is a structure with three configuration keys:
@@ -46,6 +48,8 @@ The Auth0 provider is a structure with three configuration keys:
page
- `domain`: The Application domain, found on the Auth0 Application page
Because Auth0 requires a session you need to give the session a secret key.
## Optional Configuration
- `audience`: The intended recipients of the token
+23
View File
@@ -154,6 +154,29 @@ Some more real world usable examples:
`/entities?fields=kind,metadata.namespace,metadata.name`
### Ordering
By default the entities are returned in an undefined, but stable order. You can
pass in one or more `order` query parameters to affect that ordering.
Each parameter starts either with `asc:` for ascending lexicographical order or
`desc:` for descending (reverse) lexicographical order, followed by a
dot-separated path into an entity's keys. The ordering is case insensitive. If
more than one order directive is given, later directives have lower precedence
(they are applied only when directives of higher precedence have equal values).
Example:
```text
/entities?order=asc:kind&order=desc:metadata.name
```
This will order the output first by kind ascending, and then within each kind
(if there's more than one of a given kind) by their name descending. When given
a field that does NOT exist on all entities in the result set, those entities
that do not have the field will always be sorted last in that particular order
step, no matter what the desired order was.
#### Pagination
You may pass the `offset` and `limit` query parameters to do classical
@@ -251,6 +251,23 @@ the entity belongs to the `"default"` namespace.
Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities,
i.e. not Backstage specific but the same as in Kubernetes.
### `uid` [output]
Each entity gets an automatically generated globally unique ID when it first
enters the database. This field is not meant to be specified as input data, but
is rater created by the database engine itself when producing the output entity.
Note that `uid` values are _not_ to be seen as stable, and should _not_ be used
as external references to an entity. The `uid` can change over time even when a
human observer might think that it wouldn't. As one of many examples,
unregistering and re-registering the exact same file will result in a different
`uid` value even though everything else is the same. Therefore there is very
little, if any, reason to read or use this field externally.
If you want to refer to an entity by some form of an identifier, you should
always use [string-form entity reference](references.md#string-references)
instead.
### `title` [optional]
A display name of the entity, to be presented in user interfaces instead of the
@@ -1309,3 +1326,7 @@ resolved relative to the location of this Location entity itself.
A list of targets as strings. They can all be either absolute paths/URLs
(depending on the type), or relative paths such as `./details/catalog-info.yaml`
which are resolved relative to the location of this Location entity itself.
### `spec.presence` [optional]
Describes whether the target of a location is required to exist or not. It defaults to `'required'` if not specified, can also be `'optional'`.
@@ -333,3 +333,85 @@ reading the search context.
If you produce something generic and reusable, consider contributing your
component upstream so that all users of the Backstage Search Platform can
benefit. Issues and pull requests welcome.
#### Custom search results
Search results throughout Backstage are rendered as lists so that list items can easily be customized; although a [default result list item](https://backstage.io/storybook/?path=/story/plugins-search-defaultresultlistitem--default) is available, plugins are in the best position to provide custom result list items that surface relevant information only known to the plugin.
The example below imagines `YourCustomSearchResult` as a type of search result that contains associated `tags` which could be rendered as chips below the title/text.
```tsx
import { Link } from '@backstage/core-components';
import { useAnalytics } from '@backstage/core-plugin-api';
import { ResultHighlight } from '@backstage/plugin-search-common';
import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
type CustomSearchResultListItemProps = {
result: YourCustomSearchResult;
rank?: number;
highlight?: ResultHighlight;
};
export const CustomSearchResultListItem = (
props: CustomSearchResultListItemProps,
) => {
const { title, text, location, tags } = props.result;
const analytics = useAnalytics();
const handleClick = () => {
analytics.captureEvent('discover', title, {
attributes: { to: location },
value: props.rank,
});
};
return (
<Link noTrack to={location} onClick={handleClick}>
<ListItem alignItems="center">
<Box flexWrap="wrap">
<ListItemText
primaryTypographyProps={{ variant: 'h6' }}
primary={
highlight?.fields?.title ? (
<HighlightedSearchResultText
text={highlight.fields.title}
preTag={highlight.preTag}
postTag={highlight.postTag}
/>
) : (
title
)
}
secondary={
highlight?.fields?.text ? (
<HighlightedSearchResultText
text={highlight.fields.text}
preTag={highlight.preTag}
postTag={highlight.postTag}
/>
) : (
text
)
}
/>
{tags &&
tags.map((tag: string) => (
<Chip key={tag} label={`Tag: ${tag}`} size="small" />
))}
</Box>
</ListItem>
<Divider />
</Link>
);
};
```
The optional use of the `<HighlightedSearchResultText>` component makes it possible to highlight relevant parts of the result based on the user's search query.
**Note on Analytics**: In order for app integrators to track and improve search experiences across Backstage, it's important for them to understand when and what users search for, as well as what they click on after searching. When providing a custom result component, it's your responsibility as a plugin developer to instrument it according to search analytics conventions. In particular:
- You must use the `analytics.captureEvent` method, from the `useAnalytics()` hook (detailed [plugin analytics docs are here](./analytics.md)).
- You must ensure that the action of the event, representing a click on a search result item, is `discover`, and the subject is the `title` of the clicked result. In addition, the `to` attribute should be set to the result's `location`, and the `value` of the event must be set to the `rank` (passed in as a prop).
- You must ensure that the aforementioned `captureEvent` method is called when a user clicks the link; you should further ensure that the `noTrack` prop is added to the link (which disables default link click tracking, in favor of this custom instrumentation).
For other examples and inspiration on custom result list items, check out the [`<StackOverflowSearchResultListItem>`](https://github.com/backstage/backstage/blob/c981e83/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx) or [`<CatalogSearchResultListItem>`](https://github.com/backstage/backstage/blob/c981e83/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx) components.
+14
View File
@@ -0,0 +1,14 @@
---
title: Hoop
author: hoop.dev
authorUrl: https://hoop.dev
category: Cloud-native ssh
description: hoop.dev is a cloud-native ssh implementation that lets you edit connections contents live.
documentation: https://github.com/hoophq/backstage-plugin
iconUrl: https://avatars.githubusercontent.com/u/113131551?s=200&v=4
npmPackageName: '@hoophq/backstage-plugin'
tags:
- cloud-native ssh
- monitoring
- redact
addedDate: '2022-12-20'
+10
View File
@@ -0,0 +1,10 @@
---
title: Q&A
author: drodil
authorUrl: https://github.com/drodil
category: Services
description: Ask and answer questions within Backstage
documentation: https://github.com/drodil/backstage-plugin-qeta
iconUrl: img/qeta-logo.png
npmPackageName: '@drodil/backstage-plugin-qeta'
addedDate: '2022-12-21'
+2 -2
View File
@@ -34,7 +34,7 @@ const Background = props => {
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.MediaFrame>
<iframe
src="https://www.youtube.com/embed/q9EQLR1l2SM"
src="https://www.youtube.com/embed/aEX90BlhQVs"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@@ -45,7 +45,7 @@ const Background = props => {
<iframe
width="300"
height="500"
src="https://www.youtube.com/live_chat?v=q9EQLR1l2SM&embed_domain=backstage.io&dark_theme=1"
src="https://www.youtube.com/live_chat?v=aEX90BlhQVs&embed_domain=backstage.io&dark_theme=1"
></iframe>
</Block.MediaFrame>
</Block.Container>
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
@@ -34,7 +34,7 @@
"techdocs-cli": "node scripts/techdocs-cli.js",
"techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js",
"prepare": "husky install",
"postinstall": "husky install"
"postinstall": "husky install || true"
},
"workspaces": {
"packages": [
+7
View File
@@ -1,5 +1,12 @@
# @backstage/app-defaults
## 1.0.10
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 1.0.9
### Patch Changes
+55
View File
@@ -1,5 +1,60 @@
# example-app
## 0.2.79
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/app-defaults@1.0.10
- @backstage/cli@0.22.0
- @backstage/integration-react@1.1.8
- @backstage/plugin-airbrake@0.3.13
- @backstage/plugin-apache-airflow@0.2.6
- @backstage/plugin-api-docs@0.8.13
- @backstage/plugin-azure-devops@0.2.4
- @backstage/plugin-azure-sites@0.1.2
- @backstage/plugin-badges@0.2.37
- @backstage/plugin-catalog-graph@0.2.25
- @backstage/plugin-catalog-import@0.9.3
- @backstage/plugin-catalog-react@1.2.3
- @backstage/plugin-circleci@0.3.13
- @backstage/plugin-cloudbuild@0.3.13
- @backstage/plugin-code-coverage@0.2.6
- @backstage/plugin-cost-insights@0.12.2
- @backstage/plugin-dynatrace@1.0.3
- @backstage/plugin-explore@0.3.44
- @backstage/plugin-gcalendar@0.3.9
- @backstage/plugin-gcp-projects@0.3.32
- @backstage/plugin-github-actions@0.5.13
- @backstage/plugin-gocd@0.1.19
- @backstage/plugin-graphiql@0.2.45
- @backstage/plugin-home@0.4.29
- @backstage/plugin-jenkins@0.7.12
- @backstage/plugin-kafka@0.3.13
- @backstage/plugin-kubernetes@0.7.6
- @backstage/plugin-lighthouse@0.3.13
- @backstage/plugin-newrelic@0.3.31
- @backstage/plugin-newrelic-dashboard@0.2.6
- @backstage/plugin-org@0.6.3
- @backstage/plugin-pagerduty@0.5.6
- @backstage/plugin-playlist@0.1.4
- @backstage/plugin-rollbar@0.4.13
- @backstage/plugin-scaffolder@1.9.1
- @backstage/plugin-search@1.0.6
- @backstage/plugin-search-react@1.3.1
- @backstage/plugin-sentry@0.4.6
- @backstage/plugin-shortcuts@0.3.5
- @backstage/plugin-stack-overflow@0.1.9
- @backstage/plugin-tech-insights@0.3.5
- @backstage/plugin-tech-radar@0.5.20
- @backstage/plugin-techdocs@1.4.2
- @backstage/plugin-techdocs-module-addons-contrib@1.0.8
- @backstage/plugin-techdocs-react@1.1.1
- @backstage/plugin-todo@0.2.15
- @backstage/plugin-user-settings@0.6.1
- @internal/plugin-catalog-customized@0.0.6
## 0.2.78
### Patch Changes
+12
View File
@@ -161,6 +161,17 @@ export type EntityFilterQuery =
| Record<string, string | symbol | (string | symbol)[]>[]
| Record<string, string | symbol | (string | symbol)[]>;
// @public
export type EntityOrderQuery =
| {
field: string;
order: 'asc' | 'desc';
}
| Array<{
field: string;
order: 'asc' | 'desc';
}>;
// @public
export interface GetEntitiesByRefsRequest {
entityRefs: string[];
@@ -179,6 +190,7 @@ export interface GetEntitiesRequest {
filter?: EntityFilterQuery;
limit?: number;
offset?: number;
order?: EntityOrderQuery;
}
// @public
@@ -193,6 +193,31 @@ describe('CatalogClient', () => {
expect(response.items).toEqual([]);
});
it('handles ordering properly', async () => {
expect.assertions(2);
server.use(
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
expect(req.url.search).toBe(
'?order=asc:kind&order=desc:metadata.name',
);
return res(ctx.json([]));
}),
);
const response = await client.getEntities(
{
order: [
{ field: 'kind', order: 'asc' },
{ field: 'metadata.name', order: 'desc' },
],
},
{ token },
);
expect(response.items).toEqual([]);
});
});
describe('getEntitiesByRefs', () => {
+20 -1
View File
@@ -99,7 +99,14 @@ export class CatalogClient implements CatalogApi {
request?: GetEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<GetEntitiesResponse> {
const { filter = [], fields = [], offset, limit, after } = request ?? {};
const {
filter = [],
fields = [],
order,
offset,
limit,
after,
} = request ?? {};
const params: string[] = [];
// filter param can occur multiple times, for example
@@ -129,6 +136,18 @@ export class CatalogClient implements CatalogApi {
params.push(`fields=${fields.map(encodeURIComponent).join(',')}`);
}
if (order) {
for (const directive of [order].flat()) {
if (directive) {
params.push(
`order=${encodeURIComponent(directive.order)}:${encodeURIComponent(
directive.field,
)}`,
);
}
}
}
if (offset !== undefined) {
params.push(`offset=${offset}`);
}
+46
View File
@@ -98,6 +98,48 @@ export type EntityFilterQuery =
*/
export type EntityFieldsQuery = string[];
/**
* Dot-separated field based ordering directives, controlling the sort order of
* the output entities.
*
* @remarks
*
* Each field is a dot-separated path into an entity's keys. The order is either
* ascending (`asc`, lexicographical order) or descending (`desc`, reverse
* lexicographical order). The ordering is case insensitive.
*
* If more than one order directive is given, later directives have lower
* precedence (they are applied only when directives of higher precedence have
* equal values).
*
* Example:
*
* ```
* [
* { field: 'kind', order: 'asc' },
* { field: 'metadata.name', order: 'desc' },
* ]
* ```
*
* This will order the output first by kind ascending, and then within each kind
* (if there's more than one of a given kind) by their name descending.
*
* When given a field that does NOT exist on all entities in the result set,
* those entities that do not have the field will always be sorted last in that
* particular order step, no matter what the desired order was.
*
* @public
*/
export type EntityOrderQuery =
| {
field: string;
order: 'asc' | 'desc';
}
| Array<{
field: string;
order: 'asc' | 'desc';
}>;
/**
* The request type for {@link CatalogClient.getEntities}.
*
@@ -113,6 +155,10 @@ export interface GetEntitiesRequest {
* declarations.
*/
fields?: EntityFieldsQuery;
/**
*If given, order the result set by those directives.
*/
order?: EntityOrderQuery;
/**
* If given, skips over the first N items in the result set.
*/
@@ -22,6 +22,7 @@ export type {
CatalogRequestOptions,
EntityFieldsQuery,
EntityFilterQuery,
EntityOrderQuery,
GetEntitiesByRefsRequest,
GetEntitiesByRefsResponse,
GetEntitiesRequest,
@@ -15,11 +15,11 @@
*/
import type { Entity } from '../entity/Entity';
import schema from '../schema/kinds/System.v1alpha1.schema.json';
import { ajvCompiledJsonSchemaValidator } from './util';
import schema from '../schema/kinds/System.v1alpha1.schema.json';
/**
* Backstage catalog System kind Entity. Systems group Comopnents, Resources and APIs together.
* Backstage catalog System kind Entity. Systems group Components, Resources and APIs together.
*
* @remarks
*
+1
View File
@@ -84,6 +84,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const graph = PackageGraph.fromPackages(packages);
const changedPackages = await graph.listChangedPackages({
ref: opts.since,
analyzeLockfile: true,
});
const withDevDependents = graph.collectPackageNames(
changedPackages.map(pkg => pkg.name),
+4 -1
View File
@@ -34,7 +34,10 @@ export async function command(opts: OptionValues): Promise<void> {
if (opts.since) {
const graph = PackageGraph.fromPackages(packages);
packages = await graph.listChangedPackages({ ref: opts.since });
packages = await graph.listChangedPackages({
ref: opts.since,
analyzeLockfile: true,
});
}
// Packages are ordered from most to least number of dependencies, as a
+1
View File
@@ -93,6 +93,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
const graph = PackageGraph.fromPackages(packages);
const changedPackages = await graph.listChangedPackages({
ref: opts.since,
analyzeLockfile: true,
});
const packageNames = Array.from(
@@ -21,7 +21,8 @@ import { serveBundle } from '../../lib/bundler';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
import { Lockfile } from '../../lib/versioning';
import { includedFilter } from '../versions/lint';
import { forbiddenDuplicatesFilter, includedFilter } from '../versions/lint';
import { PackageGraph } from '../../lib/monorepo';
interface StartAppOptions {
verifyVersions?: boolean;
@@ -36,10 +37,13 @@ export async function startFrontend(options: StartAppOptions) {
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
const result = lockfile.analyze({
filter: includedFilter,
localPackages: PackageGraph.fromPackages(
await PackageGraph.listTargetPackages(),
),
});
const problemPackages = [...result.newVersions, ...result.newRanges].map(
({ name }) => name,
);
const problemPackages = [...result.newVersions, ...result.newRanges]
.map(({ name }) => name)
.filter(forbiddenDuplicatesFilter);
if (problemPackages.length > 1) {
console.log(
+6 -1
View File
@@ -39,6 +39,7 @@ import {
ReleaseManifest,
} from '@backstage/release-manifests';
import 'global-agent/bootstrap';
import { PackageGraph } from '../../lib/monorepo';
const DEP_TYPES = [
'dependencies',
@@ -200,7 +201,7 @@ export default async (opts: OptionValues) => {
lockfile.remove(name, range);
}
}
await lockfile.save();
await lockfile.save(lockfilePath);
}
const breakingUpdates = new Map<string, { from: string; to: string }>();
@@ -305,8 +306,12 @@ export default async (opts: OptionValues) => {
// Finally we make sure the new lockfile doesn't have any duplicates
const dedupLockfile = await Lockfile.load(lockfilePath);
const result = dedupLockfile.analyze({
filter,
localPackages: PackageGraph.fromPackages(
await PackageGraph.listTargetPackages(),
),
});
if (result.newVersions.length > 0) {
+7 -2
View File
@@ -18,6 +18,7 @@ import { OptionValues } from 'commander';
import { Lockfile } from '../../lib/versioning';
import { paths } from '../../lib/paths';
import partition from 'lodash/partition';
import { PackageGraph } from '../../lib/monorepo';
// Packages that we try to avoid duplicates for
const INCLUDED = [/^@backstage\//];
@@ -52,9 +53,13 @@ export default async (cmd: OptionValues) => {
let success = true;
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const result = lockfile.analyze({
filter: includedFilter,
localPackages: PackageGraph.fromPackages(
await PackageGraph.listTargetPackages(),
),
});
logArray(
@@ -65,7 +70,7 @@ export default async (cmd: OptionValues) => {
if (fix) {
lockfile.replaceVersions(result.newVersions);
await lockfile.save();
await lockfile.save(lockfilePath);
} else {
const [newVersionsForbidden, newVersionsAllowed] = partition(
result.newVersions,
+20
View File
@@ -64,3 +64,23 @@ export async function listChangedFiles(ref: string) {
return Array.from(new Set([...tracked, ...untracked]));
}
/**
* Returns the contents of a file at a specific ref.
*/
export async function readFileAtRef(path: string, ref: string) {
let showRef = ref;
try {
const [base] = await runGit('merge-base', 'HEAD', ref);
showRef = base;
} catch {
// silently fall back to using the ref directly if merge base is not available
}
const { stdout } = await execFile('git', ['show', `${showRef}:${path}`], {
shell: true,
cwd: paths.targetRoot,
maxBuffer: 1024 * 1024 * 50,
});
return stdout;
}
@@ -14,19 +14,25 @@
* limitations under the License.
*/
import { resolve as resolvePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph } from './PackageGraph';
import { listChangedFiles } from '../git';
import { Lockfile } from '../versioning/Lockfile';
import { listChangedFiles, readFileAtRef } from '../git';
jest.mock('../git');
const mockListChangedFiles = listChangedFiles as jest.MockedFunction<
typeof listChangedFiles
>;
const mockReadFileAtRef = readFileAtRef as jest.MockedFunction<
typeof readFileAtRef
>;
jest.mock('../paths', () => ({
paths: {
targetRoot: '/',
resolveTargetRoot: (...paths: string[]) => resolvePath('/', ...paths),
},
}));
@@ -177,4 +183,54 @@ describe('PackageGraph', () => {
graph.listChangedPackages({ ref: 'origin/master' }),
).resolves.toEqual([graph.get('a'), graph.get('b')]);
});
it('lists changed packages with lockfile analysis', async () => {
const graph = PackageGraph.fromPackages(testPackages);
mockListChangedFiles.mockResolvedValueOnce(
['README.md', 'packages/a/src/foo.ts', 'yarn.lock'].sort(),
);
mockReadFileAtRef.mockResolvedValueOnce(`
a@^1:
version: "1.0.0"
c@^1:
version: "1.0.0"
dependencies:
c-dep: ^1
c-dep@^2:
version: "2.0.0"
integrity: sha512-xyz
`);
jest.spyOn(Lockfile, 'load').mockResolvedValueOnce(
Lockfile.parse(`
a@^1:
version: "1.0.0"
c@^1:
version: "1.0.0"
dependencies:
c-dep: ^1
c-dep@^2:
version: "2.0.0"
integrity: sha512-xyz-other
`),
);
await expect(
graph
.listChangedPackages({
ref: 'origin/master',
analyzeLockfile: true,
})
.then(pkgs => pkgs.map(pkg => pkg.name)),
).resolves.toEqual(['a', 'c']);
expect(mockReadFileAtRef).toHaveBeenCalledWith(
'yarn.lock',
'origin/master',
);
});
});
+77 -2
View File
@@ -18,7 +18,8 @@ import path from 'path';
import { getPackages, Package } from '@manypkg/get-packages';
import { paths } from '../paths';
import { PackageRole } from '../role';
import { listChangedFiles } from '../git';
import { listChangedFiles, readFileAtRef } from '../git';
import { Lockfile } from '../versioning';
type PackageJSON = Package['packageJson'];
@@ -191,7 +192,10 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
return targets;
}
async listChangedPackages(options: { ref: string }) {
async listChangedPackages(options: {
ref: string;
analyzeLockfile?: boolean;
}) {
const changedFiles = await listChangedFiles(options.ref);
const dirMap = new Map(
@@ -234,6 +238,77 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
}
}
if (changedFiles.includes('yarn.lock') && options.analyzeLockfile) {
// Load the lockfile in the working tree and the one at the ref and diff them
let thisLockfile: Lockfile;
let otherLockfile: Lockfile;
try {
thisLockfile = await Lockfile.load(
paths.resolveTargetRoot('yarn.lock'),
);
otherLockfile = Lockfile.parse(
await readFileAtRef('yarn.lock', options.ref),
);
} catch (error) {
console.warn(
`Failed to read lockfiles, assuming all packages have changed, ${error}`,
);
return Array.from(this.values());
}
const diff = thisLockfile.diff(otherLockfile);
// Create a simplified dependency graph only keeps track of package names
const graph = thisLockfile.createSimplifiedDependencyGraph();
// Merge the dependency graph from the other lockfile into this one in
// order to be able to detect removals accurately.
{
const otherGraph = thisLockfile.createSimplifiedDependencyGraph();
for (const [name, dependencies] of otherGraph) {
const node = graph.get(name);
if (node) {
dependencies.forEach(d => node.add(d));
} else {
graph.set(name, dependencies);
}
}
}
// The check is simplified by only considering the package names rather
// than the exact version range queries that were changed.
// TODO(Rugvip): Use a more exact check
const changedPackages = new Set(
[...diff.added, ...diff.changed, ...diff.removed].map(e => e.name),
);
// Starting with our set of changed packages from the diff, we loop through
// the full graph and add any package that has a dependency on a changed package.
// We keep looping until all transitive dependencies have been detected.
let changed = false;
do {
changed = false;
for (const [name, dependencies] of graph) {
if (changedPackages.has(name)) {
continue;
}
for (const dep of dependencies) {
if (changedPackages.has(dep)) {
changed = true;
changedPackages.add(name);
break;
}
}
}
} while (changed);
// Add all local packages that had a transitive dependency change to the result set
for (const node of this.values()) {
if (changedPackages.has(node.name) && !result.includes(node)) {
result.push(node);
}
}
}
return result;
}
}
+568 -27
View File
@@ -16,14 +16,23 @@
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { ExtendedPackage } from '../monorepo';
import { Lockfile } from './Lockfile';
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
const LEGACY_HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
`;
const mockA = `${HEADER}
const MODERN_HEADER = `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 6
cacheKey: 8
`;
const mockA = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
@@ -38,7 +47,7 @@ b@^2:
version "2.0.0"
`;
const mockADedup = `${HEADER}
const mockADedup = `${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
@@ -50,7 +59,7 @@ b@2.0.x, b@^2:
version "2.0.1"
`;
const mockB = `${HEADER}
const mockB = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^1":
version "1.0.1"
@@ -58,7 +67,7 @@ const mockB = `${HEADER}
version "2.0.0"
`;
const mockBDedup = `${HEADER}
const mockBDedup = `${LEGACY_HEADER}
"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x":
version "2.0.0"
@@ -77,10 +86,12 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('a')).toEqual([
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockA);
});
@@ -91,7 +102,7 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
@@ -110,7 +121,7 @@ describe('Lockfile', () => {
expect(lockfile.toString()).toBe(mockADedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockA);
await expect(lockfile.save()).resolves.toBeUndefined();
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockADedup);
});
@@ -120,7 +131,7 @@ describe('Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [
@@ -154,15 +165,7 @@ describe('Lockfile', () => {
});
});
const newHeader = `# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 6
cacheKey: 8
`;
const mockANew = `${newHeader}
const mockANew = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
@@ -177,7 +180,7 @@ b@^2:
version: 2.0.0
`;
const mockANewDedup = `${newHeader}
const mockANewDedup = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
@@ -192,6 +195,36 @@ b@^2:
version: 2.0.1
`;
const mockANewLocal = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
b: ^2
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
"b@2.0.x, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 2.0.0
`;
const mockANewLocalDedup = `${MODERN_HEADER}
a@^1:
version: 1.0.1
dependencies:
b: ^2
integrity: sha512-xyz
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
"b@2.0.x, b@^2.0.1":
version: 0.0.0-use.local
b@^2:
version: 0.0.0-use.local
`;
describe('New Lockfile', () => {
afterEach(() => {
mockFs.restore();
@@ -203,11 +236,13 @@ describe('New Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
expect(lockfile.get('a')).toEqual([{ range: '^1', version: '1.0.1' }]);
expect(lockfile.get('a')).toEqual([
{ range: '^1', version: '1.0.1', dataKey: 'a@^1' },
]);
expect(lockfile.get('b')).toEqual([
{ range: '2.0.x', version: '2.0.1' },
{ range: '^2.0.1', version: '2.0.1' },
{ range: '^2', version: '2.0.0' },
{ range: '2.0.x', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
{ range: '^2.0.1', version: '2.0.1', dataKey: 'b@2.0.x, b@^2.0.1' },
{ range: '^2', version: '2.0.0', dataKey: 'b@^2' },
]);
expect(lockfile.toString()).toBe(mockANew);
});
@@ -218,7 +253,7 @@ describe('New Lockfile', () => {
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze();
const result = lockfile.analyze({ localPackages: new Map() });
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
@@ -237,9 +272,515 @@ describe('New Lockfile', () => {
expect(lockfile.toString()).toBe(mockANewDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(mockANew);
await expect(lockfile.save()).resolves.toBeUndefined();
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewDedup,
);
});
it('should deduplicate and save mockANewLocal', async () => {
mockFs({
'/yarn.lock': mockANewLocal,
});
const lockfile = await Lockfile.load('/yarn.lock');
const result = lockfile.analyze({
localPackages: new Map([
[
'b',
{
packageJson: { version: '2.0.1' },
} as ExtendedPackage,
],
]),
});
expect(result).toEqual({
invalidRanges: [],
newRanges: [],
newVersions: [
{
name: 'b',
range: '^2',
oldVersion: '2.0.0',
newVersion: '0.0.0-use.local',
},
],
});
expect(lockfile.toString()).toBe(mockANewLocal);
lockfile.replaceVersions(result.newVersions);
expect(lockfile.toString()).toBe(mockANewLocalDedup);
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocal,
);
await expect(lockfile.save('/yarn.lock')).resolves.toBeUndefined();
await expect(fs.readFile('/yarn.lock', 'utf8')).resolves.toBe(
mockANewLocalDedup,
);
});
describe('diff', () => {
const lockfileLegacyA = Lockfile.parse(`${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
b "^2"
b@3:
version "3.0.1"
integrity sha512-abc1
b@2.0.x:
version "2.0.1"
integrity sha512-abc2
b@^2:
version "2.0.0"
integrity sha512-abc3
c@^1:
version "1.0.1"
integrity x
`);
const lockfileLegacyB = Lockfile.parse(`${LEGACY_HEADER}
a@^1:
version "1.0.1"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz-other
dependencies:
b "^2"
b@2.0.x, b@^2:
version "2.0.0"
integrity sha512-abc3
b@4:
version "4.0.0"
integrity sha512-abc
d@^1:
version "1.0.1"
integrity x
`);
const lockfileModernA = Lockfile.parse(`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
resolved: "https://my-registry/a-1.0.01.tgz#abc123"
checksum: sha512-xyz
dependencies:
b: "^2"
"b@npm:3":
version: "3.0.1"
checksum: sha512-abc1
"b@npm:2.0.x":
version: "2.0.1"
checksum: sha512-abc2
"b@npm:^2":
version: "2.0.0"
checksum: sha512-abc3
"c@npm:^1":
version: "1.0.1"
checksum: x
`);
const lockfileModernB = Lockfile.parse(`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
resolution: "a@npm:1.0.1"
checksum: sha512-xyz-other
dependencies:
b: "^2"
"b@npm:2.0.x, b@npm:^2":
version: "2.0.0"
checksum: sha512-abc3
"b@npm:4":
version: "4.0.0"
checksum: sha512-abc
"d@npm:^1":
version: "1.0.1"
checksum: x
`);
it('should diff two legacy lockfiles', async () => {
expect(lockfileLegacyA.diff(lockfileLegacyB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileLegacyB.diff(lockfileLegacyA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should diff two modern lockfiles', async () => {
expect(lockfileModernA.diff(lockfileModernB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileModernB.diff(lockfileModernA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should diff legacy and modern lockfiles', async () => {
expect(lockfileLegacyA.diff(lockfileModernB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileLegacyB.diff(lockfileModernA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should diff modern and legacy lockfiles', async () => {
expect(lockfileModernA.diff(lockfileLegacyB)).toEqual({
added: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
});
expect(lockfileModernB.diff(lockfileLegacyA)).toEqual({
added: [
{ name: 'b', range: '4' },
{ name: 'd', range: '^1' },
],
changed: [
{ name: 'a', range: '^1' },
{ name: 'b', range: '2.0.x' },
],
removed: [
{ name: 'b', range: '3' },
{ name: 'c', range: '^1' },
],
});
});
it('should handle workspace ranges', async () => {
const lockfile = `${MODERN_HEADER}
"@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults":
version: 0.0.0-use.local
resolution: "@backstage/app-defaults@workspace:packages/app-defaults"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@types/node": ^16.11.26
"@types/react": ^16.13.1 || ^17.0.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-dom: ^16.13.1 || ^17.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/backend-app-api@workspace:^, @backstage/backend-app-api@workspace:packages/backend-app-api":
version: 0.0.0-use.local
resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
express: ^4.17.1
express-promise-router: ^4.1.0
winston: ^3.2.1
languageName: unknown
linkType: soft
`;
expect(Lockfile.parse(lockfile).diff(Lockfile.parse(lockfile))).toEqual({
added: [],
changed: [],
removed: [],
});
});
});
describe('createSimplifiedDependencyGraph', () => {
it('for modern lockfile', () => {
expect(
Lockfile.parse(
`${MODERN_HEADER}
"@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults":
version: 0.0.0-use.local
resolution: "@backstage/app-defaults@workspace:packages/app-defaults"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2
"@material-ui/icons": ^4.9.1
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@types/node": ^16.11.26
"@types/react": ^16.13.1 || ^17.0.0
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-dom: ^16.13.1 || ^17.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/backend-app-api@workspace:^, @backstage/backend-app-api@workspace:packages/backend-app-api":
version: 0.0.0-use.local
resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
express: ^4.17.1
express-promise-router: ^4.1.0
winston: ^3.2.1
languageName: unknown
linkType: soft
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
[
'@backstage/app-defaults',
new Set([
'@backstage/cli',
'@backstage/core-app-api',
'@backstage/core-components',
'@backstage/core-plugin-api',
'@backstage/plugin-permission-react',
'@backstage/test-utils',
'@backstage/theme',
'@material-ui/core',
'@material-ui/icons',
'@testing-library/jest-dom',
'@testing-library/react',
'@types/node',
'@types/react',
'react',
'react-dom',
'react-router-dom',
]),
],
[
'@backstage/backend-app-api',
new Set([
'@backstage/backend-common',
'@backstage/backend-plugin-api',
'@backstage/backend-tasks',
'@backstage/cli',
'@backstage/errors',
'@backstage/plugin-permission-node',
'express',
'express-promise-router',
'winston',
]),
],
]),
);
});
it('for simple lockfile without dependencies', () => {
expect(
Lockfile.parse(
`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
"b@npm:3":
version: "3.0.1"
"b@npm:2.0.x":
version: "2.0.1"
checksum: sha512-abc2
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
['a', new Set()],
['b', new Set()],
]),
);
});
it('for lockfile with dependencies', () => {
expect(
Lockfile.parse(
`${MODERN_HEADER}
"a@npm:^1":
version: "1.0.1"
dependencies:
b: "^2"
"b@npm:3":
version: "3.0.1"
checksum: sha512-abc1
"b@npm:2.0.x":
version: "2.0.1"
checksum: sha512-abc2
dependencies:
c: "^1"
"b@npm:^2":
version: "2.0.0"
checksum: sha512-abc3
peerDependencies:
d: "^1"
"c@npm:^1":
version: "1.0.1"
"d@npm:^1":
version: "1.0.2"
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
['a', new Set(['b'])],
['b', new Set(['c', 'd'])],
['c', new Set()],
['d', new Set()],
]),
);
});
it('for legacy lockfile', () => {
expect(
Lockfile.parse(
`${LEGACY_HEADER}
a@^1:
version "1.0.1"
dependencies:
b "^2"
b@3:
version "3.0.1"
integrity sha512-abc1
b@2.0.x:
version "2.0.1"
integrity sha512-abc2
dependencies:
c "^1"
b@^2:
version "2.0.0"
integrity sha512-abc3
dependencies:
d "^1"
c@^1:
version "1.0.1"
integrity x
d@^1:
version "1.0.1"
integrity x
`,
).createSimplifiedDependencyGraph(),
).toEqual(
new Map([
['a', new Set(['b'])],
['b', new Set(['c', 'd'])],
['c', new Set()],
['d', new Set()],
]),
);
});
});
});
+146 -41
View File
@@ -18,6 +18,7 @@ import fs from 'fs-extra';
import semver from 'semver';
import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
import { ExtendedPackage } from '../monorepo';
const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
@@ -25,14 +26,28 @@ type LockfileData = {
[entry: string]: {
version: string;
resolved?: string;
integrity?: string;
integrity?: string /* old */;
checksum?: string /* new */;
dependencies?: { [name: string]: string };
peerDependencies?: { [name: string]: string };
};
};
type LockfileQueryEntry = {
range: string;
version: string;
dataKey: string;
};
type LockfileDiffEntry = {
name: string;
range: string;
};
type LockfileDiff = {
added: LockfileDiffEntry[];
changed: LockfileDiffEntry[];
removed: LockfileDiffEntry[];
};
/** Entries that have an invalid version range, for example an npm tag */
@@ -64,20 +79,6 @@ type AnalyzeResult = {
newRanges: AnalyzeResultNewRange[];
};
function parseLockfile(lockfileContents: string) {
try {
return {
object: parseSyml(lockfileContents),
type: 'success',
};
} catch (err) {
return {
object: null,
type: err,
};
}
}
// the new yarn header is handled out of band of the parsing
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
const NEW_HEADER = `${[
@@ -85,11 +86,6 @@ const NEW_HEADER = `${[
`# Manual changes might be lost - proceed with caution!\n`,
].join(``)}\n`;
function stringifyLockfile(data: LockfileData, legacy: boolean) {
return legacy
? legacyStringifyLockfile(data)
: NEW_HEADER + stringifySyml(data);
}
// taken from yarn parser package
// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
@@ -110,13 +106,19 @@ const SPECIAL_OBJECT_KEYS = [
export class Lockfile {
static async load(path: string) {
const lockfileContents = await fs.readFile(path, 'utf8');
const legacy = LEGACY_REGEX.test(lockfileContents);
const lockfile = parseLockfile(lockfileContents);
if (lockfile.type !== 'success') {
throw new Error(`Failed yarn.lock parse with ${lockfile.type}`);
return Lockfile.parse(lockfileContents);
}
static parse(content: string) {
const legacy = LEGACY_REGEX.test(content);
let data: LockfileData;
try {
data = parseSyml(content);
} catch (err) {
throw new Error(`Failed yarn.lock parse, ${err}`);
}
const data = lockfile.object as LockfileData;
const packages = new Map<string, LockfileQueryEntry[]>();
for (const [key, value] of Object.entries(data)) {
@@ -139,15 +141,14 @@ export class Lockfile {
if (range.startsWith('npm:')) {
range = range.slice('npm:'.length);
}
queries.push({ range, version: value.version });
queries.push({ range, version: value.version, dataKey: key });
}
}
return new Lockfile(path, packages, data, legacy);
return new Lockfile(packages, data, legacy);
}
private constructor(
private readonly path: string,
private readonly packages: Map<string, LockfileQueryEntry[]>,
private readonly data: LockfileData,
private readonly legacy: boolean = false,
@@ -164,8 +165,11 @@ export class Lockfile {
}
/** Analyzes the lockfile to identify possible actions and warnings for the entries */
analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult {
const { filter } = options ?? {};
analyze(options: {
filter?: (name: string) => boolean;
localPackages: Map<string, ExtendedPackage>;
}): AnalyzeResult {
const { filter, localPackages } = options;
const result: AnalyzeResult = {
invalidRanges: [],
newVersions: [],
@@ -178,7 +182,9 @@ export class Lockfile {
}
// Get rid of and signal any invalid ranges upfront
const invalid = allEntries.filter(e => !semver.validRange(e.range));
const invalid = allEntries.filter(
e => !semver.validRange(e.range) && !e.range.startsWith('workspace:'),
);
result.invalidRanges.push(
...invalid.map(({ range }) => ({ name, range })),
);
@@ -190,36 +196,55 @@ export class Lockfile {
}
// Find all versions currently in use
const versions = Array.from(new Set(entries.map(e => e.version))).sort(
(v1, v2) => semver.rcompare(v1, v2),
);
const versions = Array.from(new Set(entries.map(e => e.version)))
.map(v => {
// Translate workspace:^ references to the actual version
if (v === '0.0.0-use.local') {
const local = localPackages.get(name);
if (!local) {
throw new Error(`No local package found for ${name}`);
}
if (!local.packageJson.version) {
throw new Error(`No version found for local package ${name}`);
}
return {
entryVersion: v,
actualVersion: local.packageJson.version,
};
}
return { entryVersion: v, actualVersion: v };
})
.sort((v1, v2) => semver.rcompare(v1.actualVersion, v2.actualVersion));
// If we're not using at least 2 different versions we're done
if (versions.length < 2) {
continue;
}
// TODO(Rugvip): Support bumping into workspace ranges too
const acceptedVersions = new Set<string>();
for (const { version, range } of entries) {
// Finds the highest matching version from the the known versions
// TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one
const acceptedVersion = versions.find(v => semver.satisfies(v, range));
const acceptedVersion = versions.find(v =>
semver.satisfies(v.actualVersion, range),
);
if (!acceptedVersion) {
throw new Error(
`No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
);
}
if (acceptedVersion !== version) {
if (acceptedVersion.entryVersion !== version) {
result.newVersions.push({
name,
range,
newVersion: acceptedVersion,
newVersion: acceptedVersion.entryVersion,
oldVersion: version,
});
}
acceptedVersions.add(acceptedVersion);
acceptedVersions.add(acceptedVersion.actualVersion);
}
// If all ranges were able to accept the same version, we're done
@@ -315,11 +340,91 @@ export class Lockfile {
}
}
async save() {
await fs.writeFile(this.path, this.toString(), 'utf8');
createSimplifiedDependencyGraph(): Map<string, Set<string>> {
const graph = new Map<string, Set<string>>();
for (const [name, entries] of this.packages) {
const dependencies = new Set(
entries.flatMap(e => {
const data = this.data[e.dataKey];
return [
...Object.keys(data?.dependencies ?? {}),
...Object.keys(data?.peerDependencies ?? {}),
];
}),
);
graph.set(name, dependencies);
}
return graph;
}
/**
* Diff with another lockfile, returning entries that have been
* added, changed, and removed compared to the other lockfile.
*/
diff(otherLockfile: Lockfile): LockfileDiff {
const diff = {
added: new Array<{ name: string; range: string }>(),
changed: new Array<{ name: string; range: string }>(),
removed: new Array<{ name: string; range: string }>(),
};
// Keeps track of packages that only exist in this lockfile
const remainingOldNames = new Set(this.packages.keys());
for (const [name, otherQueries] of otherLockfile.packages) {
remainingOldNames.delete(name);
const thisQueries = this.packages.get(name);
// If the packages doesn't exist in this lockfile, add all entries
if (!thisQueries) {
diff.removed.push(...otherQueries.map(q => ({ name, range: q.range })));
continue;
}
const remainingOldRanges = new Set(thisQueries.map(q => q.range));
for (const otherQuery of otherQueries) {
remainingOldRanges.delete(otherQuery.range);
const thisQuery = thisQueries.find(q => q.range === otherQuery.range);
if (!thisQuery) {
diff.removed.push({ name, range: otherQuery.range });
continue;
}
const otherPkg = otherLockfile.data[otherQuery.dataKey];
const thisPkg = this.data[thisQuery.dataKey];
if (otherPkg && thisPkg) {
const thisCheck = thisPkg.integrity || thisPkg.checksum;
const otherCheck = otherPkg.integrity || otherPkg.checksum;
if (thisCheck !== otherCheck) {
diff.changed.push({ name, range: otherQuery.range });
}
}
}
for (const thisRange of remainingOldRanges) {
diff.added.push({ name, range: thisRange });
}
}
for (const name of remainingOldNames) {
const queries = this.packages.get(name) ?? [];
diff.added.push(...queries.map(q => ({ name, range: q.range })));
}
return diff;
}
async save(path: string) {
await fs.writeFile(path, this.toString(), 'utf8');
}
toString() {
return stringifyLockfile(this.data, this.legacy);
return this.legacy
? legacyStringifyLockfile(this.data)
: NEW_HEADER + stringifySyml(this.data);
}
}
+6
View File
@@ -1,5 +1,11 @@
# @backstage/core-components
## 0.12.2
### Patch Changes
- Fixing the UPPERCASED links in the sidebar
## 0.12.1
### Patch Changes
@@ -63,6 +63,9 @@ const useStyles = makeStyles<BackstageTheme, { sidebarConfig: SidebarConfig }>(
'&::-webkit-scrollbar': {
display: 'none',
},
'& .MuiButtonBase-root': {
textTransform: 'none',
},
}),
drawerOpen: props => ({
width: props.sidebarConfig.drawerWidthOpen,
+10
View File
@@ -1,5 +1,15 @@
# @backstage/dev-utils
## 1.0.10
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/app-defaults@1.0.10
- @backstage/integration-react@1.1.8
- @backstage/plugin-catalog-react@1.2.3
## 1.0.9
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/integration-react
## 1.1.8
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 1.1.7
### Patch Changes
+1 -1
View File
@@ -37,7 +37,7 @@
"@microsoft/api-extractor": "^7.23.0",
"@microsoft/api-extractor-model": "^7.17.2",
"@microsoft/tsdoc": "0.14.1",
"@microsoft/tsdoc-config": "0.16.1",
"@microsoft/tsdoc-config": "0.16.2",
"chalk": "^4.0.0",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
@@ -1,5 +1,18 @@
# techdocs-cli-embedded-app
## 0.2.78
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/app-defaults@1.0.10
- @backstage/cli@0.22.0
- @backstage/integration-react@1.1.8
- @backstage/plugin-catalog@1.7.1
- @backstage/plugin-techdocs@1.4.2
- @backstage/plugin-techdocs-react@1.1.1
## 0.2.77
### Patch Changes
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-adr
## 0.2.5
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/integration-react@1.1.8
- @backstage/plugin-catalog-react@1.2.3
- @backstage/plugin-search-react@1.3.1
## 0.2.4
### Patch Changes
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-airbrake
## 0.3.13
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/dev-utils@1.0.10
- @backstage/plugin-catalog-react@1.2.3
## 0.3.12
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-allure
## 0.1.29
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.1.28
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-analytics-module-ga
## 0.1.24
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.1.23
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-apache-airflow
## 0.2.6
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.2.5
### Patch Changes
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-api-docs
## 0.8.13
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog@1.7.1
- @backstage/plugin-catalog-react@1.2.3
## 0.8.12
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-apollo-explorer
## 0.1.6
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.1.5
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-azure-devops
## 0.2.4
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.2.3
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-azure-sites
## 0.1.2
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.1.1
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-badges
## 0.2.37
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.2.36
### Patch Changes
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-bazaar
## 0.2.2
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/cli@0.22.0
- @backstage/plugin-catalog@1.7.1
- @backstage/plugin-catalog-react@1.2.3
## 0.2.1
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-bitrise
## 0.1.40
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.1.39
### Patch Changes
@@ -38,6 +38,14 @@ export type EntityPagination = {
after?: string;
};
/**
* A sorting rule for entities.
*/
export type EntityOrder = {
field: string;
order: 'asc' | 'desc';
};
/**
* Matches rows in the search table.
* @public
@@ -71,6 +79,7 @@ export type PageInfo =
export type EntitiesRequest = {
filter?: EntityFilter;
fields?: (entity: Entity) => Entity;
order?: EntityOrder[];
pagination?: EntityPagination;
authorizationToken?: string;
};
@@ -28,6 +28,7 @@ import {
import { Stitcher } from '../stitching/Stitcher';
import { buildEntitySearch } from '../stitching/buildEntitySearch';
import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog';
import { EntitiesRequest } from '../catalog/types';
describe('DefaultEntitiesCatalog', () => {
const databases = TestDatabases.create({
@@ -254,7 +255,7 @@ describe('DefaultEntitiesCatalog', () => {
describe('entities', () => {
it.each(databases.eachSupportedId())(
'should return correct entity for simple filter',
'should return correct entity for simple filter, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
@@ -284,10 +285,11 @@ describe('DefaultEntitiesCatalog', () => {
expect(entities.length).toBe(1);
expect(entities[0]).toEqual(entity2);
},
60_000,
);
it.each(databases.eachSupportedId())(
'should return correct entity for negation filter',
'should return correct entity for negation filter, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
@@ -319,10 +321,11 @@ describe('DefaultEntitiesCatalog', () => {
expect(entities.length).toBe(1);
expect(entities[0]).toEqual(entity1);
},
60_000,
);
it.each(databases.eachSupportedId())(
'should return correct entities for nested filter',
'should return correct entities for nested filter, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
@@ -388,10 +391,11 @@ describe('DefaultEntitiesCatalog', () => {
expect(entities).toContainEqual(entity2);
expect(entities).toContainEqual(entity4);
},
60_000,
);
it.each(databases.eachSupportedId())(
'should return correct entities for complex negation filter',
'should return correct entities for complex negation filter, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
@@ -429,10 +433,11 @@ describe('DefaultEntitiesCatalog', () => {
expect(entities.length).toBe(1);
expect(entities).toContainEqual(entity1);
},
60_000,
);
it.each(databases.eachSupportedId())(
'should return no matches for an empty values array',
'should return no matches for an empty values array, %p',
// NOTE: An empty values array is not a sensible input in a realistic scenario.
async databaseId => {
const { knex } = await createDatabase(databaseId);
@@ -461,6 +466,7 @@ describe('DefaultEntitiesCatalog', () => {
expect(entities.length).toBe(0);
},
60_000,
);
it.each(databases.eachSupportedId())(
@@ -517,12 +523,105 @@ describe('DefaultEntitiesCatalog', () => {
},
]);
},
60_000,
);
it.each(databases.eachSupportedId())(
'can order and combine with filtering, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
const entity1: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n1' },
spec: { a: 'foo' },
};
const entity2: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n2' },
spec: { a: 'bar' },
};
const entity3: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n3' },
spec: { a: 'bar', b: 'lonely' },
};
const entity4: Entity = {
apiVersion: 'a',
kind: 'k',
metadata: { name: 'n4' },
spec: { a: 'baz', b: 'only' },
};
await addEntityToSearch(knex, entity1);
await addEntityToSearch(knex, entity2);
await addEntityToSearch(knex, entity3);
await addEntityToSearch(knex, entity4);
const catalog = new DefaultEntitiesCatalog(knex, stitcher);
function f(request: EntitiesRequest): Promise<string[]> {
return catalog
.entities(request)
.then(response => response.entities.map(e => e.metadata.name));
}
await expect(
f({ order: [{ field: 'metadata.name', order: 'asc' }] }),
).resolves.toEqual(['n1', 'n2', 'n3', 'n4']);
await expect(
f({ order: [{ field: 'metadata.name', order: 'desc' }] }),
).resolves.toEqual(['n4', 'n3', 'n2', 'n1']);
await expect(
f({
order: [
{ field: 'spec.a', order: 'asc' },
{ field: 'metadata.name', order: 'desc' },
],
}),
).resolves.toEqual(['n3', 'n2', 'n4', 'n1']);
await expect(
f({
filter: { not: { key: 'spec.b', values: ['lonely'] } },
order: [
{ field: 'spec.a', order: 'asc' },
{ field: 'metadata.name', order: 'desc' },
],
}),
).resolves.toEqual(['n2', 'n4', 'n1']);
// only n3 and n4 has spec.b, nulls (no match) always goes last no matter the order
await expect(
f({
order: [
{ field: 'spec.b', order: 'asc' },
{ field: 'metadata.name', order: 'asc' },
],
}),
).resolves.toEqual(['n3', 'n4', 'n1', 'n2']);
// only n3 and n4 has spec.b, nulls (no match) always goes last no matter the order
await expect(
f({
order: [
{ field: 'spec.b', order: 'desc' },
{ field: 'metadata.name', order: 'asc' },
],
}),
).resolves.toEqual(['n4', 'n3', 'n1', 'n2']);
},
60_000,
);
});
describe('entitiesBatch', () => {
it.each(databases.eachSupportedId())(
'queries for entities by ref, including duplicates, and gracefully returns null for missing entities',
'queries for entities by ref, including duplicates, and gracefully returns null for missing entities, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
@@ -571,12 +670,13 @@ describe('DefaultEntitiesCatalog', () => {
'k:default/two',
]);
},
60_000,
);
});
describe('removeEntityByUid', () => {
it.each(databases.eachSupportedId())(
'also clears parent hashes',
'also clears parent hashes, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
@@ -659,12 +759,13 @@ describe('DefaultEntitiesCatalog', () => {
new Set(['k:default/unrelated1', 'k:default/unrelated2']),
);
},
60_000,
);
});
describe('facets', () => {
it.each(databases.eachSupportedId())(
'can filter and collect properly',
'can filter and collect properly, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
@@ -697,10 +798,11 @@ describe('DefaultEntitiesCatalog', () => {
},
});
},
60_000,
);
it.each(databases.eachSupportedId())(
'can match on annotations and labels with dots in them',
'can match on annotations and labels with dots in them, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
@@ -743,10 +845,11 @@ describe('DefaultEntitiesCatalog', () => {
},
});
},
60_000,
);
it.each(databases.eachSupportedId())(
'can match on strings in arrays',
'can match on strings in arrays, %p',
async databaseId => {
const { knex } = await createDatabase(databaseId);
@@ -784,6 +887,7 @@ describe('DefaultEntitiesCatalog', () => {
},
});
},
60_000,
);
});
});
@@ -97,7 +97,7 @@ function addCondition(
// make a lot of sense. However, it had abysmal performance on sqlite
// when datasets grew large, so we're using IN instead.
const matchQuery = db<DbSearchRow>('search')
.select(entityIdField)
.select('search.entity_id')
.where({ key: filter.key.toLowerCase() })
.andWhere(function keyFilter() {
if (filter.values) {
@@ -178,14 +178,48 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
let entitiesQuery =
db<DbFinalEntitiesRow>('final_entities').select('final_entities.*');
request?.order?.forEach(({ field }, index) => {
const alias = `order_${index}`;
entitiesQuery = entitiesQuery.leftOuterJoin(
{ [alias]: 'search' },
function search(inner) {
inner
.on(`${alias}.entity_id`, 'final_entities.entity_id')
.andOn(`${alias}.key`, db.raw('?', [field]));
},
);
});
entitiesQuery = entitiesQuery.whereNotNull('final_entities.final_entity');
if (request?.filter) {
entitiesQuery = parseFilter(request.filter, entitiesQuery, db);
entitiesQuery = parseFilter(
request.filter,
entitiesQuery,
db,
false,
'final_entities.entity_id',
);
}
// TODO: move final_entities to use entity_ref
entitiesQuery = entitiesQuery
.whereNotNull('final_entities.final_entity')
.orderBy('entity_id', 'asc');
request?.order?.forEach(({ order }, index) => {
if (db.client.config.client === 'pg') {
// pg correctly orders by the column value and handling nulls in one go
entitiesQuery = entitiesQuery.orderBy([
{ column: `order_${index}.value`, order, nulls: 'last' },
]);
} else {
// sqlite and mysql translate the above statement ONLY into "order by (value is null) asc"
// no matter what the order is, for some reason, so we have to manually add back the statement
// that translates to "order by value <order>" while avoiding to give an order
entitiesQuery = entitiesQuery.orderBy([
{ column: `order_${index}.value`, order: undefined, nulls: 'last' },
{ column: `order_${index}.value`, order },
]);
}
});
entitiesQuery = entitiesQuery.orderBy('final_entities.entity_id', 'asc'); // stable sort
const { limit, offset } = parsePagination(request?.pagination);
if (limit !== undefined) {
@@ -41,6 +41,7 @@ import {
parseEntityTransformParams,
} from './request';
import { parseEntityFacetParams } from './request/parseEntityFacetParams';
import { parseEntityOrderParams } from './request/parseEntityOrderParams';
import { LocationService, RefreshOptions, RefreshService } from './types';
import {
disallowReadonlyMode,
@@ -113,6 +114,7 @@ export async function createRouter(
const { entities, pageInfo } = await entitiesCatalog.entities({
filter: parseEntityFilterParams(req.query),
fields: parseEntityTransformParams(req.query),
order: parseEntityOrderParams(req.query),
pagination: parseEntityPaginationParams(req.query),
authorizationToken: getBearerToken(req.header('authorization')),
});
@@ -0,0 +1,42 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseEntityOrderParams } from './parseEntityOrderParams';
describe('parseEntityOrderParams', () => {
it('handles missing parameter', () => {
expect(parseEntityOrderParams({})).toBeUndefined();
});
it('handles parameters with various orders', () => {
expect(
parseEntityOrderParams({ order: ['asc:a', 'desc:b', 'asc:c:d'] }),
).toEqual([
{ field: 'a', order: 'asc' },
{ field: 'b', order: 'desc' },
{ field: 'c:d', order: 'asc' },
]);
});
it.each(['', ':', 'ascii:', 'ascii:ebcdic', ':colon'])(
'rejects missing/bad order or key, %p',
order => {
expect(() => parseEntityOrderParams({ order: [order] })).toThrow(
`Invalid order parameter "${order}", expected "<asc or desc>:<field name>"`,
);
},
);
});
@@ -0,0 +1,37 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InputError } from '@backstage/errors';
import { EntityOrder } from '../../catalog/types';
import { parseStringsParam } from './common';
export function parseEntityOrderParams(
params: Record<string, unknown>,
): EntityOrder[] | undefined {
return parseStringsParam(params.order, 'order')?.map(item => {
const match = item.match(/^(asc|desc):(.+)$/);
if (!match) {
throw new InputError(
`Invalid order parameter "${item}", expected "<asc or desc>:<field name>"`,
);
}
return {
order: match[1] as 'asc' | 'desc',
field: match[2],
};
});
}
+8
View File
@@ -1,5 +1,13 @@
# @internal/plugin-catalog-customized
## 0.0.6
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog@1.7.1
- @backstage/plugin-catalog-react@1.2.3
## 0.0.5
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-catalog-graph
## 0.2.25
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.2.24
### Patch Changes
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-catalog-import
## 0.9.3
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/integration-react@1.1.8
- @backstage/plugin-catalog-react@1.2.3
## 0.9.2
### Patch Changes
+8
View File
@@ -1,9 +1,17 @@
# @backstage/plugin-catalog-react
## 1.2.3
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 1.2.2
### Patch Changes
- 2cb9998: Fixed bug in `EntityOwnerPicker` and `EntityLifecyclePicker` that filtered on unavailable tags for the selected kind.
- 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`.
- 6ffa47bb0a: Cleanup and small fixes for the kind selector
- 19356df560: Updated dependency `zen-observable` to `^0.9.0`.
@@ -202,4 +202,33 @@ describe('<EntityTagPicker/>', () => {
tags: new EntityTagFilter(['tag2']),
});
});
it('removes tags from filters if there are none available', async () => {
const updateFilters = jest.fn();
const mockCatalogApiRefNoTags = {
getEntityFacets: async () => ({
facets: {
'metadata.tags': {},
},
}),
} as unknown as CatalogApi;
render(
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRefNoTags]]}>
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters: { tags: ['tag1'] },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
,
</TestApiProvider>,
);
await waitFor(() =>
expect(updateFilters).toHaveBeenLastCalledWith({
tags: undefined,
}),
);
});
});
@@ -93,10 +93,14 @@ export const EntityTagPicker = (props: EntityTagPickerProps) => {
}, [queryParamTags]);
useEffect(() => {
const tags = Object.keys(availableTags ?? {});
updateFilters({
tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined,
tags:
selectedTags.length && tags.length
? new EntityTagFilter(selectedTags)
: undefined,
});
}, [selectedTags, updateFilters]);
}, [selectedTags, updateFilters, availableTags]);
if (!Object.keys(availableTags ?? {}).length) return null;
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-catalog
## 1.7.1
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/integration-react@1.1.8
- @backstage/plugin-catalog-react@1.2.3
- @backstage/plugin-search-react@1.3.1
## 1.7.0
### Minor Changes
@@ -1,5 +1,12 @@
# @backstage/plugin-cicd-statistics-module-gitlab
## 0.1.9
### Patch Changes
- Updated dependencies
- @backstage/plugin-cicd-statistics@0.1.15
## 0.1.8
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-cicd-statistics
## 0.1.15
### Patch Changes
- Updated dependencies
- @backstage/plugin-catalog-react@1.2.3
## 0.1.14
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-circleci
## 0.3.13
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.3.12
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-cloudbuild
## 0.3.13
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.3.12
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-code-climate
## 0.1.13
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.1.12
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-code-coverage
## 0.2.6
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.2.5
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-codescene
## 0.1.8
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.1.7
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-config-schema
## 0.1.36
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.1.35
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-cost-insights
## 0.12.2
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.12.1
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-dynatrace
## 1.0.3
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 1.0.2
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @internal/plugin-todo-list
## 1.0.9
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 1.0.8
### Patch Changes
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-explore
## 0.3.44
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
- @backstage/plugin-search-react@1.3.1
- @backstage/plugin-explore-react@0.0.24
## 0.3.43
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-firehydrant
## 0.1.30
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.1.29
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-fossa
## 0.2.45
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.2.44
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-gcalendar
## 0.3.9
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.3.8
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-gcp-projects
## 0.3.32
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.3.31
### Patch Changes
+7
View File
@@ -1,5 +1,12 @@
# @backstage/plugin-git-release-manager
## 0.3.26
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
## 0.3.25
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-github-actions
## 0.5.13
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.5.12
### Patch Changes
@@ -3,6 +3,7 @@
"name": "Build",
"node_id": "MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ==",
"check_suite_id": 42,
"display_title": "foo",
"check_suite_node_id": "MDEwOkNoZWNrU3VpdGU0Mg==",
"head_branch": "main",
"head_sha": "acb5820ced9479c074f688cc328bf03f341a511d",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/plugin-github-deployments
## 0.1.44
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/integration-react@1.1.8
- @backstage/plugin-catalog-react@1.2.3
## 0.1.43
### Patch Changes
+8
View File
@@ -1,5 +1,13 @@
# @backstage/plugin-github-issues
## 0.2.2
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.2.1
### Patch Changes
@@ -1,5 +1,13 @@
# @backstage/plugin-github-pull-requests-board
## 0.1.7
### Patch Changes
- Updated dependencies
- @backstage/core-components@0.12.2
- @backstage/plugin-catalog-react@1.2.3
## 0.1.6
### Patch Changes

Some files were not shown because too many files have changed in this diff Show More