Merge branch 'master' of github.com:backstage/backstage into RoadieHQ-tttentity-peek-ahead
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
rollback `@rjsf/validator-ajv8` to `@rjsf/validator-v6`
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
---
|
||||
|
||||
Implemented support for the `order` directive on `getEntities`
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Fixed bug in `EntityTagPicker` that filtered on unavailable tags for the selected kind.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Applied fix from v1.9.1
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/repo-tools': patch
|
||||
---
|
||||
|
||||
Updated dependency `@microsoft/tsdoc-config` to `0.16.2`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Implemented server side ordering in the entities endpoint
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Workspace ranges are no longer considered invalid by version commands.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user