Merge remote-tracking branch 'origin/master' into cost-insight-trending-line

# Conflicts:
#	plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx
#	plugins/cost-insights/src/testUtils/providers.tsx
This commit is contained in:
bnechyporenko
2022-11-22 16:31:23 +01:00
195 changed files with 2840 additions and 1172 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/plugin-scaffolder': patch
---
Enabling the customization of the last step in the scaffolder template.
To override the content you have to do the next:
```typescript jsx
<TemplatePage ReviewStepComponent={YourCustomComponent} />
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': minor
---
Introducing repo-tools package
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-github': patch
---
Fixes the assignment of group member references in `GithubMultiOrgProcessor` so membership relations are resolved correctly.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Initialize all `formData` in the `Stepper` in `/next`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org-react': patch
---
Bug fixes and adding the possibility to add a default value for the `GroupListPicker`. Fixes: Vertical size jump on text entry, left align for text, selecting a value closes the popup, auto focus on the popup when opening
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
Provide the ability to change the base currency from USD to any other currency in cost insights plugin
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Bump `msw` version in default plugin/app templates
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Add items prop to SupportButton. This prop can be used to override the items that would otherwise be grabbed from the config.
+193
View File
@@ -0,0 +1,193 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"example-app": "0.2.77",
"@backstage/app-defaults": "1.0.8",
"example-backend": "0.2.77",
"@backstage/backend-app-api": "0.2.3",
"@backstage/backend-common": "0.16.0",
"@backstage/backend-defaults": "0.1.3",
"example-backend-next": "0.0.5",
"@backstage/backend-plugin-api": "0.1.4",
"@backstage/backend-tasks": "0.3.7",
"@backstage/backend-test-utils": "0.1.30",
"@backstage/catalog-client": "1.1.2",
"@backstage/catalog-model": "1.1.3",
"@backstage/cli": "0.21.0",
"@backstage/cli-common": "0.1.10",
"@backstage/codemods": "0.1.41",
"@backstage/config": "1.0.4",
"@backstage/config-loader": "1.1.6",
"@backstage/core-app-api": "1.2.0",
"@backstage/core-components": "0.12.0",
"@backstage/core-plugin-api": "1.1.0",
"@backstage/create-app": "0.4.34",
"@backstage/dev-utils": "1.0.8",
"e2e-test": "0.2.0",
"@backstage/errors": "1.1.3",
"@backstage/integration": "1.4.0",
"@backstage/integration-react": "1.1.6",
"@backstage/release-manifests": "0.0.7",
"@backstage/repo-tools": "0.0.0",
"@techdocs/cli": "1.2.3",
"techdocs-cli-embedded-app": "0.2.76",
"@backstage/test-utils": "1.2.2",
"@backstage/theme": "0.2.16",
"@backstage/types": "1.0.1",
"@backstage/version-bridge": "1.0.2",
"@backstage/plugin-adr": "0.2.3",
"@backstage/plugin-adr-backend": "0.2.3",
"@backstage/plugin-adr-common": "0.2.3",
"@backstage/plugin-airbrake": "0.3.11",
"@backstage/plugin-airbrake-backend": "0.2.11",
"@backstage/plugin-allure": "0.1.27",
"@backstage/plugin-analytics-module-ga": "0.1.22",
"@backstage/plugin-apache-airflow": "0.2.4",
"@backstage/plugin-api-docs": "0.8.11",
"@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.0",
"@backstage/plugin-apollo-explorer": "0.1.4",
"@backstage/plugin-app-backend": "0.3.38",
"@backstage/plugin-auth-backend": "0.17.1",
"@backstage/plugin-auth-node": "0.2.7",
"@backstage/plugin-azure-devops": "0.2.2",
"@backstage/plugin-azure-devops-backend": "0.3.17",
"@backstage/plugin-azure-devops-common": "0.3.0",
"@backstage/plugin-azure-sites": "0.1.0",
"@backstage/plugin-azure-sites-backend": "0.1.0",
"@backstage/plugin-azure-sites-common": "0.1.0",
"@backstage/plugin-badges": "0.2.35",
"@backstage/plugin-badges-backend": "0.1.32",
"@backstage/plugin-bazaar": "0.2.0",
"@backstage/plugin-bazaar-backend": "0.2.1",
"@backstage/plugin-bitbucket-cloud-common": "0.2.1",
"@backstage/plugin-bitrise": "0.1.38",
"@backstage/plugin-catalog": "1.6.1",
"@backstage/plugin-catalog-backend": "1.5.1",
"@backstage/plugin-catalog-backend-module-aws": "0.1.11",
"@backstage/plugin-catalog-backend-module-azure": "0.1.9",
"@backstage/plugin-catalog-backend-module-bitbucket": "0.2.5",
"@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.5",
"@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.3",
"@backstage/plugin-catalog-backend-module-gerrit": "0.1.6",
"@backstage/plugin-catalog-backend-module-github": "0.2.0",
"@backstage/plugin-catalog-backend-module-gitlab": "0.1.9",
"@backstage/plugin-catalog-backend-module-ldap": "0.5.5",
"@backstage/plugin-catalog-backend-module-msgraph": "0.4.4",
"@backstage/plugin-catalog-backend-module-openapi": "0.1.4",
"@backstage/plugin-catalog-common": "1.0.8",
"@internal/plugin-catalog-customized": "0.0.4",
"@backstage/plugin-catalog-graph": "0.2.23",
"@backstage/plugin-catalog-graphql": "0.3.15",
"@backstage/plugin-catalog-import": "0.9.1",
"@backstage/plugin-catalog-node": "1.2.1",
"@backstage/plugin-catalog-react": "1.2.1",
"@backstage/plugin-cicd-statistics": "0.1.13",
"@backstage/plugin-cicd-statistics-module-gitlab": "0.1.7",
"@backstage/plugin-circleci": "0.3.11",
"@backstage/plugin-cloudbuild": "0.3.11",
"@backstage/plugin-code-climate": "0.1.11",
"@backstage/plugin-code-coverage": "0.2.4",
"@backstage/plugin-code-coverage-backend": "0.2.4",
"@backstage/plugin-codescene": "0.1.6",
"@backstage/plugin-config-schema": "0.1.34",
"@backstage/plugin-cost-insights": "0.12.0",
"@backstage/plugin-cost-insights-common": "0.1.1",
"@backstage/plugin-dynatrace": "1.0.1",
"@backstage/plugin-events-backend": "0.1.0",
"@backstage/plugin-events-backend-module-aws-sqs": "0.1.0",
"@backstage/plugin-events-backend-module-azure": "0.1.0",
"@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.0",
"@backstage/plugin-events-backend-module-gerrit": "0.1.0",
"@backstage/plugin-events-backend-module-github": "0.1.0",
"@backstage/plugin-events-backend-module-gitlab": "0.1.0",
"@backstage/plugin-events-backend-test-utils": "0.1.0",
"@backstage/plugin-events-node": "0.1.0",
"@internal/plugin-todo-list": "1.0.7",
"@internal/plugin-todo-list-backend": "1.0.7",
"@internal/plugin-todo-list-common": "1.0.6",
"@backstage/plugin-explore": "0.3.42",
"@backstage/plugin-explore-react": "0.0.23",
"@backstage/plugin-firehydrant": "0.1.28",
"@backstage/plugin-fossa": "0.2.43",
"@backstage/plugin-gcalendar": "0.3.7",
"@backstage/plugin-gcp-projects": "0.3.30",
"@backstage/plugin-git-release-manager": "0.3.24",
"@backstage/plugin-github-actions": "0.5.11",
"@backstage/plugin-github-deployments": "0.1.42",
"@backstage/plugin-github-issues": "0.2.0",
"@backstage/plugin-github-pull-requests-board": "0.1.5",
"@backstage/plugin-gitops-profiles": "0.3.29",
"@backstage/plugin-gocd": "0.1.17",
"@backstage/plugin-graphiql": "0.2.43",
"@backstage/plugin-graphql-backend": "0.1.28",
"@backstage/plugin-home": "0.4.27",
"@backstage/plugin-ilert": "0.2.0",
"@backstage/plugin-jenkins": "0.7.10",
"@backstage/plugin-jenkins-backend": "0.1.28",
"@backstage/plugin-jenkins-common": "0.1.10",
"@backstage/plugin-kafka": "0.3.11",
"@backstage/plugin-kafka-backend": "0.2.31",
"@backstage/plugin-kubernetes": "0.7.4",
"@backstage/plugin-kubernetes-backend": "0.8.0",
"@backstage/plugin-kubernetes-common": "0.4.4",
"@backstage/plugin-lighthouse": "0.3.11",
"@backstage/plugin-newrelic": "0.3.29",
"@backstage/plugin-newrelic-dashboard": "0.2.4",
"@backstage/plugin-org": "0.6.0",
"@backstage/plugin-org-react": "0.1.0",
"@backstage/plugin-pagerduty": "0.5.4",
"@backstage/plugin-periskop": "0.1.9",
"@backstage/plugin-periskop-backend": "0.1.9",
"@backstage/plugin-permission-backend": "0.5.13",
"@backstage/plugin-permission-common": "0.7.1",
"@backstage/plugin-permission-node": "0.7.1",
"@backstage/plugin-permission-react": "0.4.7",
"@backstage/plugin-playlist": "0.1.2",
"@backstage/plugin-playlist-backend": "0.2.1",
"@backstage/plugin-playlist-common": "0.1.2",
"@backstage/plugin-proxy-backend": "0.2.32",
"@backstage/plugin-rollbar": "0.4.11",
"@backstage/plugin-rollbar-backend": "0.1.35",
"@backstage/plugin-scaffolder": "1.8.0",
"@backstage/plugin-scaffolder-backend": "1.8.0",
"@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.13",
"@backstage/plugin-scaffolder-backend-module-rails": "0.4.6",
"@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.11",
"@backstage/plugin-scaffolder-common": "1.2.2",
"@backstage/plugin-search": "1.0.4",
"@backstage/plugin-search-backend": "1.1.1",
"@backstage/plugin-search-backend-module-elasticsearch": "1.0.4",
"@backstage/plugin-search-backend-module-pg": "0.4.2",
"@backstage/plugin-search-backend-node": "1.0.4",
"@backstage/plugin-search-common": "1.1.1",
"@backstage/plugin-search-react": "1.2.1",
"@backstage/plugin-sentry": "0.4.4",
"@backstage/plugin-shortcuts": "0.3.3",
"@backstage/plugin-sonarqube": "0.5.0",
"@backstage/plugin-sonarqube-backend": "0.1.3",
"@backstage/plugin-splunk-on-call": "0.4.0",
"@backstage/plugin-stack-overflow": "0.1.7",
"@backstage/plugin-stack-overflow-backend": "0.1.7",
"@backstage/plugin-tech-insights": "0.3.3",
"@backstage/plugin-tech-insights-backend": "0.5.4",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.22",
"@backstage/plugin-tech-insights-common": "0.2.8",
"@backstage/plugin-tech-insights-node": "0.3.6",
"@backstage/plugin-tech-radar": "0.5.18",
"@backstage/plugin-techdocs": "1.4.0",
"@backstage/plugin-techdocs-addons-test-utils": "1.0.6",
"@backstage/plugin-techdocs-backend": "1.4.1",
"@backstage/plugin-techdocs-module-addons-contrib": "1.0.6",
"@backstage/plugin-techdocs-node": "1.4.2",
"@backstage/plugin-techdocs-react": "1.0.6",
"@backstage/plugin-todo": "0.2.13",
"@backstage/plugin-todo-backend": "0.1.35",
"@backstage/plugin-user-settings": "0.5.1",
"@backstage/plugin-user-settings-backend": "0.1.2",
"@backstage/plugin-vault": "0.1.5",
"@backstage/plugin-vault-backend": "0.2.4",
"@backstage/plugin-xcmetrics": "0.2.31"
},
"changesets": []
}
+113
View File
@@ -0,0 +1,113 @@
---
'@backstage/backend-common': patch
'@backstage/backend-test-utils': patch
'@backstage/catalog-client': patch
'@backstage/cli': patch
'@backstage/config-loader': patch
'@backstage/core-app-api': patch
'@backstage/core-components': patch
'@backstage/core-plugin-api': patch
'@backstage/integration-react': patch
'@backstage/integration': patch
'@backstage/release-manifests': patch
'@backstage/test-utils': patch
'@backstage/plugin-adr-backend': patch
'@backstage/plugin-adr': patch
'@backstage/plugin-airbrake-backend': patch
'@backstage/plugin-airbrake': patch
'@backstage/plugin-allure': patch
'@backstage/plugin-analytics-module-ga': patch
'@backstage/plugin-apache-airflow': patch
'@backstage/plugin-api-docs': patch
'@backstage/plugin-apollo-explorer': patch
'@backstage/plugin-app-backend': patch
'@backstage/plugin-auth-backend': patch
'@backstage/plugin-auth-node': patch
'@backstage/plugin-azure-devops-backend': patch
'@backstage/plugin-azure-devops': patch
'@backstage/plugin-azure-sites-backend': patch
'@backstage/plugin-azure-sites': patch
'@backstage/plugin-badges': patch
'@backstage/plugin-bitbucket-cloud-common': patch
'@backstage/plugin-bitrise': patch
'@backstage/plugin-catalog-backend-module-azure': patch
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
'@backstage/plugin-catalog-backend-module-bitbucket-server': patch
'@backstage/plugin-catalog-backend-module-bitbucket': patch
'@backstage/plugin-catalog-backend-module-gerrit': patch
'@backstage/plugin-catalog-backend-module-github': patch
'@backstage/plugin-catalog-backend-module-gitlab': patch
'@backstage/plugin-catalog-backend-module-msgraph': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-graphql': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-circleci': patch
'@backstage/plugin-cloudbuild': patch
'@backstage/plugin-code-climate': patch
'@backstage/plugin-code-coverage-backend': patch
'@backstage/plugin-code-coverage': patch
'@backstage/plugin-codescene': patch
'@backstage/plugin-config-schema': patch
'@backstage/plugin-cost-insights': patch
'@backstage/plugin-dynatrace': patch
'@backstage/plugin-explore-react': patch
'@backstage/plugin-explore': patch
'@backstage/plugin-firehydrant': patch
'@backstage/plugin-fossa': patch
'@backstage/plugin-gcalendar': patch
'@backstage/plugin-gcp-projects': patch
'@backstage/plugin-git-release-manager': patch
'@backstage/plugin-github-actions': patch
'@backstage/plugin-github-deployments': patch
'@backstage/plugin-github-issues': patch
'@backstage/plugin-github-pull-requests-board': patch
'@backstage/plugin-gitops-profiles': patch
'@backstage/plugin-gocd': patch
'@backstage/plugin-graphiql': patch
'@backstage/plugin-graphql-backend': patch
'@backstage/plugin-home': patch
'@backstage/plugin-ilert': patch
'@backstage/plugin-jenkins-backend': patch
'@backstage/plugin-jenkins': patch
'@backstage/plugin-kafka': patch
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-lighthouse': patch
'@backstage/plugin-newrelic': patch
'@backstage/plugin-org-react': patch
'@backstage/plugin-org': patch
'@backstage/plugin-pagerduty': patch
'@backstage/plugin-periskop-backend': patch
'@backstage/plugin-periskop': patch
'@backstage/plugin-permission-backend': patch
'@backstage/plugin-permission-common': patch
'@backstage/plugin-permission-node': patch
'@backstage/plugin-playlist-backend': patch
'@backstage/plugin-playlist': patch
'@backstage/plugin-proxy-backend': patch
'@backstage/plugin-rollbar-backend': patch
'@backstage/plugin-rollbar': patch
'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-search': patch
'@backstage/plugin-sentry': patch
'@backstage/plugin-shortcuts': patch
'@backstage/plugin-sonarqube-backend': patch
'@backstage/plugin-sonarqube': patch
'@backstage/plugin-splunk-on-call': patch
'@backstage/plugin-stack-overflow': patch
'@backstage/plugin-tech-insights': patch
'@backstage/plugin-tech-radar': patch
'@backstage/plugin-techdocs-addons-test-utils': patch
'@backstage/plugin-techdocs-backend': patch
'@backstage/plugin-techdocs-module-addons-contrib': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-todo-backend': patch
'@backstage/plugin-todo': patch
'@backstage/plugin-user-settings': patch
'@backstage/plugin-vault-backend': patch
'@backstage/plugin-vault': patch
'@backstage/plugin-xcmetrics': patch
---
Updated dependency `msw` to `^0.49.0`.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-kubernetes-backend': patch
'@backstage/plugin-kubernetes-common': patch
---
Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-backend-node': patch
---
Fixed a bug that could cause a `max listeners exceeded warning` to be logged when more than 10 collators were running simultaneously.
+2 -2
View File
@@ -1,6 +1,6 @@
name: Automate merge message
on:
pull_request:
pull_request_target:
branches: ['master']
types: ['closed']
@@ -25,7 +25,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
ref: '${{ github.event.pull_request.merge_commit_sha }}'
ref: '${{ github.event.pull_request.head.sha }}'
- name: fetch base
run: git fetch --depth 1 origin ${{ github.event.pull_request.base.sha }}
+5 -5
View File
@@ -9,13 +9,13 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: backstage
ref: v${{ github.event.client_payload.version }}
- name: setup-node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
@@ -23,7 +23,7 @@ jobs:
# Beginning of yarn setup, keep in sync between all workflows.
# TODO(Rugvip): move this to composite action once all features we use are supported
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
@@ -31,7 +31,7 @@ jobs:
# Cache every node_modules folder inside the monorepo
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: '**/node_modules'
# We use both yarn.lock and package.json as cache keys to ensure that
@@ -47,7 +47,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
uses: actions/cache@v3
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
+6 -6
View File
@@ -1415,22 +1415,22 @@ __metadata:
linkType: hard
"typescript@npm:^4.1.3":
version: 4.8.4
resolution: "typescript@npm:4.8.4"
version: 4.9.3
resolution: "typescript@npm:4.9.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0
checksum: 17b8f816050b412403e38d48eef0e893deb6be522d6dc7caf105e54a72e34daf6835c447735fd2b28b66784e72bfbf87f627abb4818a8e43d1fa8106396128dc
languageName: node
linkType: hard
"typescript@patch:typescript@^4.1.3#~builtin<compat/typescript>":
version: 4.8.4
resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin<compat/typescript>::version=4.8.4&hash=a1c5e5"
version: 4.9.3
resolution: "typescript@patch:typescript@npm%3A4.9.3#~builtin<compat/typescript>::version=4.9.3&hash=a1c5e5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 563a0ef47abae6df27a9a3ab38f75fc681f633ccf1a3502b1108e252e187787893de689220f4544aaf95a371a4eb3141e4a337deb9895de5ac3c1ca76430e5f0
checksum: ef65c22622d864497d0a0c5db693523329b3284c15fe632e93ad9aa059e8dc38ef3bd767d6f26b1e5ecf9446f49bd0f6c4e5714a2eeaf352805dc002479843d1
languageName: node
linkType: hard
+4 -4
View File
@@ -141,14 +141,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: actions/setup-python@v2
- uses: actions/setup-node@v3
- uses: actions/setup-python@v4
# the 2 steps below can be removed if you aren't using plantuml in your documentation
- name: setup java
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
+3 -6
View File
@@ -11,7 +11,7 @@
"build:backend": "yarn workspace backend build",
"build:all": "backstage-cli repo build --all",
"build:api-reports": "yarn build:api-reports:only --tsc",
"build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts",
"build:api-reports:only": "backstage-repo-tools api-reports",
"build:api-docs": "LANG=en_EN yarn build:api-reports --docs",
"tsc": "tsc",
"tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false",
@@ -49,16 +49,13 @@
"version": "1.8.0",
"dependencies": {
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@microsoft/api-documenter": "^7.17.11",
"@microsoft/api-extractor": "^7.23.0",
"@microsoft/api-extractor-model": "^7.17.2",
"@microsoft/tsdoc": "^0.14.1"
"@manypkg/get-packages": "^1.1.3"
},
"devDependencies": {
"@backstage/cli": "workspace:*",
"@backstage/codemods": "workspace:*",
"@backstage/create-app": "workspace:*",
"@backstage/repo-tools": "workspace:*",
"@changesets/cli": "^2.14.0",
"@octokit/rest": "^19.0.3",
"@spotify/prettier-config": "^14.0.0",
@@ -19,7 +19,6 @@ import {
Content,
DocsIcon,
Header,
Lifecycle,
Page,
useSidebarPinState,
} from '@backstage/core-components';
@@ -66,7 +65,7 @@ const SearchPage = () => {
return (
<Page themeId="home">
{!isMobile && <Header title="Search" subtitle={<Lifecycle alpha />} />}
{!isMobile && <Header title="Search" />}
<Content>
<Grid container direction="row">
<Grid item xs={12}>
+1 -1
View File
@@ -117,7 +117,7 @@
"better-sqlite3": "^7.5.0",
"http-errors": "^2.0.0",
"mock-fs": "^5.1.0",
"msw": "^0.48.0",
"msw": "^0.49.0",
"mysql2": "^2.2.5",
"recursive-readdir": "^2.2.2",
"supertest": "^6.1.3"
+1 -1
View File
@@ -41,7 +41,7 @@
"@backstage/config": "workspace:^",
"better-sqlite3": "^7.5.0",
"knex": "^2.0.0",
"msw": "^0.48.0",
"msw": "^0.49.0",
"mysql2": "^2.2.5",
"pg": "^8.3.0",
"testcontainers": "^8.1.2",
+1 -1
View File
@@ -38,7 +38,7 @@
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -155,7 +155,7 @@
"@types/yarnpkg__lockfile": "^1.1.4",
"del": "^6.0.0",
"mock-fs": "^5.1.0",
"msw": "^0.48.0",
"msw": "^0.49.0",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0",
"type-fest": "^2.0.0"
@@ -41,7 +41,7 @@
"@backstage/cli": "{{versionQuery '@backstage/cli'}}",
"@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}",
"supertest": "{{versionQuery 'supertest' '6.2.4'}}",
"msw": "{{versionQuery 'msw' '0.47.0'}}"
"msw": "{{versionQuery 'msw' '0.49.0'}}"
},
"files": [
"dist"
@@ -48,7 +48,7 @@
"@testing-library/react": "{{versionQuery '@testing-library/react' '12.1.3'}}",
"@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '14.0.0'}}",
"@types/node": "{{versionQuery '@types/node' '16.11.26'}}",
"msw": "{{versionQuery 'msw' '0.47.0'}}",
"msw": "{{versionQuery 'msw' '0.49.0'}}",
"cross-fetch": "{{versionQuery 'cross-fetch' '3.1.5'}}"
},
"files": [
+1 -1
View File
@@ -56,7 +56,7 @@
"@types/node": "^16.11.26",
"@types/yup": "^0.29.13",
"mock-fs": "^5.1.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -57,7 +57,7 @@
"@types/node": "^16.11.26",
"@types/zen-observable": "^0.8.0",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0",
"msw": "^0.49.0",
"react-router-beta": "npm:react-router@6.0.0-beta.0",
"react-router-dom-beta": "npm:react-router-dom@6.0.0-beta.0",
"react-router-dom-stable": "npm:react-router-dom@^6.3.0",
+1 -1
View File
@@ -99,7 +99,7 @@
"@types/react-window": "^1.8.5",
"@types/zen-observable": "^0.8.0",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
@@ -15,31 +15,38 @@
*/
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import {
render,
act,
RenderResult,
waitFor,
fireEvent,
screen,
} from '@testing-library/react';
import { wrapInTestApp, renderInTestApp } from '@backstage/test-utils';
MockConfigApi,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { SupportButton } from './SupportButton';
import { configApiRef } from '@backstage/core-plugin-api';
const configApi = new MockConfigApi({
app: {
support: {
url: 'https://github.com',
items: [
{
title: 'Github',
icon: 'github',
links: [{ title: 'Github Issues', url: '/issues' }],
},
],
},
},
});
const SUPPORT_BUTTON_ID = 'support-button';
const POPOVER_ID = 'support-button-popover';
describe('<SupportButton />', () => {
it('renders without exploding', async () => {
let renderResult: RenderResult;
await renderInTestApp(<SupportButton />);
await act(async () => {
renderResult = render(wrapInTestApp(<SupportButton />));
});
await waitFor(() =>
expect(renderResult.getByTestId(SUPPORT_BUTTON_ID)).toBeInTheDocument(),
);
expect(screen.getByTestId(SUPPORT_BUTTON_ID)).toBeInTheDocument();
});
it('supports passing a title', async () => {
@@ -48,26 +55,52 @@ describe('<SupportButton />', () => {
expect(screen.getByText('Custom title')).toBeInTheDocument();
});
it('supports passing link items through props', async () => {
await renderInTestApp(
<SupportButton
items={[
{
title: 'Documentation',
icon: 'description',
links: [{ title: 'Show docs', url: '/docs' }],
},
]}
/>,
);
const supportButton = screen.getByTestId(SUPPORT_BUTTON_ID);
expect(supportButton).toBeInTheDocument();
fireEvent.click(supportButton);
const documentationItem = screen.getByText('Documentation');
expect(documentationItem).toBeInTheDocument();
});
it('shows items from support config', async () => {
await renderInTestApp(
<TestApiProvider apis={[[configApiRef, configApi]]}>
<SupportButton />
</TestApiProvider>,
);
const supportButton = screen.getByTestId(SUPPORT_BUTTON_ID);
expect(supportButton).toBeInTheDocument();
fireEvent.click(supportButton);
const defaultGithubSupportConfig = screen.getByText('Github Issues');
expect(defaultGithubSupportConfig).toBeInTheDocument();
});
it('shows popover on click', async () => {
let renderResult: RenderResult;
await renderInTestApp(<SupportButton />);
await act(async () => {
renderResult = render(wrapInTestApp(<SupportButton />));
});
const supportButton = screen.getByTestId(SUPPORT_BUTTON_ID);
expect(supportButton).toBeInTheDocument();
let button: HTMLElement;
fireEvent.click(supportButton);
await waitFor(() => {
expect(renderResult.getByTestId(SUPPORT_BUTTON_ID)).toBeInTheDocument();
button = renderResult.getByTestId(SUPPORT_BUTTON_ID);
});
await act(async () => {
fireEvent.click(button);
});
await waitFor(() => {
expect(renderResult.getByTestId(POPOVER_ID)).toBeInTheDocument();
});
expect(screen.getByTestId(POPOVER_ID)).toBeInTheDocument();
});
});
@@ -35,6 +35,7 @@ import { Link } from '../Link';
type SupportButtonProps = {
title?: string;
items?: SupportItem[];
children?: React.ReactNode;
};
@@ -82,8 +83,8 @@ const SupportListItem = ({ item }: { item: SupportItem }) => {
};
export function SupportButton(props: SupportButtonProps) {
const { title, children } = props;
const { items } = useSupportConfig();
const { title, items, children } = props;
const { items: configItems } = useSupportConfig();
const [popoverOpen, setPopoverOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
@@ -150,10 +151,9 @@ export function SupportButton(props: SupportButtonProps) {
{child}
</ListItem>
))}
{items &&
items.map((item, i) => (
<SupportListItem item={item} key={`item-${i}`} />
))}
{(items ?? configItems).map((item, i) => (
<SupportListItem item={item} key={`item-${i}`} />
))}
</List>
<DialogActions>
<Button color="primary" onClick={popoverCloseHandler}>
+1 -1
View File
@@ -57,7 +57,7 @@
"@types/prop-types": "^15.7.3",
"@types/zen-observable": "^0.8.0",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -45,7 +45,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -46,7 +46,7 @@
"@backstage/config-loader": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@types/luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -38,7 +38,7 @@
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@types/node": "^16.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+5
View File
@@ -0,0 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'no-console': 0,
},
});
+11
View File
@@ -0,0 +1,11 @@
# @backstage/repo-tools
This package provides a CLI for backstage repo tooling.
## Installation
Install the package via Yarn:
```sh
yarn add @backstage/repo-tools
```
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env node
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
// Figure out whether we're running inside the backstage repo or as an installed dependency
/* eslint-disable-next-line no-restricted-syntax */
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
require('..');
} else {
require('ts-node').register({
transpileOnly: true,
/* eslint-disable-next-line no-restricted-syntax */
project: path.resolve(__dirname, '../../../tsconfig.json'),
compilerOptions: {
module: 'CommonJS',
},
});
require('../src');
}
+29
View File
@@ -0,0 +1,29 @@
## CLI Report file for "@backstage/repo-tools"
> Do not edit this file. It is a report generated by `yarn build:api-reports`
### `backstage-repo-tools`
```
Usage: backstage-repo-tools [options] [command]
Options:
-V, --version
-h, --help
Commands:
api-reports [options] [path...]
help [command]
```
### `backstage-repo-tools api-reports`
```
Usage: backstage-repo-tools api-reports [options] [path...]
Options:
--ci
--tsc
--docs
-h, --help
```
+52
View File
@@ -0,0 +1,52 @@
{
"name": "@backstage/repo-tools",
"description": "CLI for Backstage repo tooling ",
"version": "0.0.0",
"publishConfig": {
"access": "public"
},
"backstage": {
"role": "cli"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/repo-tools"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"main": "dist/index.cjs.js",
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"start": "nodemon --"
},
"bin": {
"backstage-repo-tools": "bin/backstage-repo-tools"
},
"dependencies": {
"@backstage/errors": "workspace:^",
"@microsoft/api-documenter": "^7.17.11",
"@microsoft/api-extractor": "^7.23.0",
"@microsoft/api-extractor-model": "^7.17.2",
"@microsoft/tsdoc": "0.14.1",
"chalk": "^4.0.0",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
"ts-node": "^10.0.0"
},
"files": [
"bin",
"dist/**/*.js"
],
"nodemonConfig": {
"watch": "./src",
"exec": "bin/backstage-repo-tools",
"ext": "ts"
}
}
@@ -24,7 +24,7 @@ import {
dirname,
join,
} from 'path';
import { spawnSync, execFile } from 'child_process';
import { execFile } from 'child_process';
import prettier from 'prettier';
import fs from 'fs-extra';
import {
@@ -49,11 +49,17 @@ import {
import { DocTable } from '@microsoft/api-documenter/lib/nodes/DocTable';
import { DocTableRow } from '@microsoft/api-documenter/lib/nodes/DocTableRow';
import { DocHeading } from '@microsoft/api-documenter/lib/nodes/DocHeading';
import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter';
import {
CustomMarkdownEmitter,
ICustomMarkdownEmitterOptions,
} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter';
import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter';
import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration';
const tmpDir = resolvePath(__dirname, '../node_modules/.cache/api-extractor');
const tmpDir = resolvePath(
process.cwd(),
'./node_modules/.cache/api-extractor',
);
/**
* All of this monkey patching below is because MUI has these bare package.json file as a method
@@ -99,7 +105,9 @@ function patchFileMessageFetcher(
} = router;
router.fetchAssociatedMessagesForReviewFile =
function patchedFetchAssociatedMessagesForReviewFile(ast) {
function patchedFetchAssociatedMessagesForReviewFile(
ast: AstDeclaration | undefined,
) {
const messages = fetchAssociatedMessagesForReviewFile.call(this, ast);
return transform(messages, ast);
};
@@ -113,7 +121,10 @@ function patchFileMessageFetcher(
const originalGenerateReviewFileContent =
ApiReportGenerator.generateReviewFileContent;
ApiReportGenerator.generateReviewFileContent =
function decoratedGenerateReviewFileContent(collector, ...moreArgs) {
function decoratedGenerateReviewFileContent(
collector: { program: Program; messageRouter: any },
...moreArgs: any[]
) {
const program = collector.program as Program;
// The purpose of this override is to allow the @ignore tag to be used to ignore warnings
@@ -137,7 +148,9 @@ ApiReportGenerator.generateReviewFileContent =
}
const [, symbolName] = symbolMatch;
const sourceFile = program.getSourceFile(message.sourceFilePath);
const sourceFile =
message.sourceFilePath &&
program.getSourceFile(message.sourceFilePath);
if (!sourceFile) {
throw new Error(
`Failed to find source file in program at path "${message.sourceFilePath}"`,
@@ -210,7 +223,7 @@ const ALLOW_WARNINGS = [
async function resolvePackagePath(
packagePath: string,
): Promise<string | undefined> {
const projectRoot = resolvePath(__dirname, '..');
const projectRoot = resolvePath(process.cwd());
const fullPackageDir = resolvePath(projectRoot, packagePath);
const stat = await fs.stat(fullPackageDir);
@@ -228,7 +241,7 @@ async function resolvePackagePath(
return relativePath(projectRoot, fullPackageDir);
}
async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) {
export async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) {
const packageDirs = new Array<string>();
for (const unresolvedPackageDir of unresolvedPackageDirs) {
@@ -246,9 +259,9 @@ async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) {
return packageDirs;
}
async function findPackageDirs() {
export async function findPackageDirs() {
const packageDirs = new Array<string>();
const projectRoot = resolvePath(__dirname, '..');
const projectRoot = resolvePath(process.cwd());
for (const packageRoot of PACKAGE_ROOTS) {
const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot));
@@ -265,8 +278,8 @@ async function findPackageDirs() {
return packageDirs;
}
async function createTemporaryTsConfig(includedPackageDirs: string[]) {
const path = resolvePath(__dirname, '..', 'tsconfig.tmp.json');
export async function createTemporaryTsConfig(includedPackageDirs: string[]) {
const path = resolvePath(process.cwd(), 'tsconfig.tmp.json');
process.once('exit', () => {
fs.removeSync(path);
@@ -284,7 +297,7 @@ async function createTemporaryTsConfig(includedPackageDirs: string[]) {
return path;
}
async function countApiReportWarnings(projectFolder: string) {
export async function countApiReportWarnings(projectFolder: string) {
const path = resolvePath(projectFolder, 'api-report.md');
try {
const content = await fs.readFile(path, 'utf8');
@@ -313,7 +326,7 @@ async function countApiReportWarnings(projectFolder: string) {
}
}
async function getTsDocConfig() {
export async function getTsDocConfig() {
const tsdocConfigFile = await TSDocConfigFile.loadFile(
require.resolve('@microsoft/api-extractor/extends/tsdoc-base.json'),
);
@@ -349,7 +362,7 @@ interface ApiExtractionOptions {
tsconfigFilePath: string;
}
async function runApiExtraction({
export async function runApiExtraction({
packageDirs,
outputDir,
isLocalBuild,
@@ -358,7 +371,10 @@ async function runApiExtraction({
await fs.remove(outputDir);
const entryPoints = packageDirs.map(packageDir => {
return resolvePath(__dirname, `../dist-types/${packageDir}/src/index.d.ts`);
return resolvePath(
process.cwd(),
`./dist-types/${packageDir}/src/index.d.ts`,
);
});
let compilerState: CompilerState | undefined = undefined;
@@ -367,8 +383,12 @@ async function runApiExtraction({
for (const packageDir of packageDirs) {
console.log(`## Processing ${packageDir}`);
const projectFolder = resolvePath(__dirname, '..', packageDir);
const packageFolder = resolvePath(__dirname, '../dist-types', packageDir);
const projectFolder = resolvePath(process.cwd(), packageDir);
const packageFolder = resolvePath(
process.cwd(),
'./dist-types',
packageDir,
);
const warningCountBefore = await countApiReportWarnings(projectFolder);
@@ -618,7 +638,7 @@ class ApiModelTransforms {
if (serialized.members.length !== 1) {
throw new Error(
`Unexpected members in serialized ApiPackage, [${serialized.members
.map(m => m.kind)
.map((m: { kind: any }) => m.kind)
.join(' ')}]`,
);
}
@@ -634,7 +654,7 @@ class ApiModelTransforms {
members: [
{
...entryPoint,
members: entryPoint.members.map(member =>
members: entryPoint.members.map((member: any) =>
transforms.reduce((m, t) => t(m), member),
),
},
@@ -753,7 +773,7 @@ class ApiModelTransforms {
};
}
async function buildDocs({
export async function buildDocs({
inputDir,
outputDir,
}: {
@@ -847,7 +867,11 @@ async function buildDocs({
}
/** @override */
emit(stringBuilder, docNode, options) {
emit(
stringBuilder: any,
docNode: DocNode,
options: ICustomMarkdownEmitterOptions,
) {
// Hack to get rid of the leading comment of each file, since
// we want the front matter to come first
stringBuilder._chunks.length = 0;
@@ -886,7 +910,7 @@ async function buildDocs({
// so we hook in wherever we can. In this case we add the front matter
// just before writing the breadcrumbs at the top.
/** @override */
_writeBreadcrumb(output, apiItem) {
_writeBreadcrumb(output: any, apiItem: ApiItem & { name: string }) {
let title;
let description;
@@ -897,9 +921,12 @@ async function buildDocs({
} else if (apiItem.kind === 'Model') {
title = 'Package Index';
description = 'Index of all Backstage Packages';
} else {
} else if (apiItem.name) {
title = apiItem.name;
description = `API Reference for ${apiItem.name}`;
} else {
title = apiItem.displayName;
description = `API Reference for ${apiItem.displayName}`;
}
// Add our front matter
@@ -925,7 +952,10 @@ async function buildDocs({
};
}
_writeModelTable(output, apiModel): void {
_writeModelTable(
output: { appendNode: (arg0: DocTable | DocHeading) => void },
apiModel: { members: any },
): void {
const configuration = this._tsdocConfiguration;
const packagesTable = new DocTable({
@@ -998,7 +1028,10 @@ async function buildDocs({
documenter.generateFiles();
}
async function categorizePackageDirs(projectRoot, packageDirs) {
export async function categorizePackageDirs(
projectRoot: string,
packageDirs: any[],
) {
const dirs = packageDirs.slice();
const tsPackageDirs = new Array<string>();
const cliPackageDirs = new Array<string>();
@@ -1086,11 +1119,11 @@ function parseHelpPage(helpPageContent: string) {
// Trim away documentation
const sectionItems = sectionLines
.map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1])
.filter(Boolean);
.filter(Boolean) as string[];
if (sectionName.toLocaleLowerCase('en-US') === 'options:') {
if (sectionName?.toLocaleLowerCase('en-US') === 'options:') {
options = sectionItems;
} else if (sectionName.toLocaleLowerCase('en-US') === 'commands:') {
} else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') {
commands = sectionItems;
} else {
throw new Error(`Unknown CLI section: ${sectionName}`);
@@ -1185,7 +1218,7 @@ interface CliExtractionOptions {
isLocalBuild: boolean;
}
async function runCliExtraction({
export async function runCliExtraction({
projectRoot,
packageDirs,
isLocalBuild,
@@ -1250,96 +1283,3 @@ async function runCliExtraction({
}
}
}
async function main() {
const projectRoot = resolvePath(__dirname, '..');
const isCiBuild = process.argv.includes('--ci');
const isDocsBuild = process.argv.includes('--docs');
const runTsc = process.argv.includes('--tsc');
const selectedPackageDirs = await findSpecificPackageDirs(
process.argv.slice(2).filter(arg => !arg.startsWith('--')),
);
if (selectedPackageDirs && isCiBuild) {
throw new Error(
'Package path arguments are not supported together with the --ci flag',
);
}
if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) {
console.log('');
console.log(
'TIP: You can generate api-reports for select packages by passing package paths:',
);
console.log('');
console.log(
' yarn build:api-reports packages/config packages/core-plugin-api',
);
console.log('');
}
let temporaryTsConfigPath: string | undefined;
if (selectedPackageDirs) {
temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs);
}
const tsconfigFilePath =
temporaryTsConfigPath ?? resolvePath(projectRoot, 'tsconfig.json');
if (runTsc) {
await fs.remove(resolvePath(projectRoot, 'dist-types'));
const { status } = spawnSync(
'yarn',
[
'tsc',
['--project', tsconfigFilePath],
['--skipLibCheck', 'false'],
['--incremental', 'false'],
].flat(),
{
stdio: 'inherit',
shell: true,
cwd: projectRoot,
},
);
if (status !== 0) {
process.exit(status);
}
}
const packageDirs = selectedPackageDirs ?? (await findPackageDirs());
const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs(
projectRoot,
packageDirs,
);
if (tsPackageDirs.length > 0) {
console.log('# Generating package API reports');
await runApiExtraction({
packageDirs: tsPackageDirs,
outputDir: tmpDir,
isLocalBuild: !isCiBuild,
tsconfigFilePath,
});
}
if (cliPackageDirs.length > 0) {
console.log('# Generating package CLI reports');
await runCliExtraction({
projectRoot,
packageDirs: cliPackageDirs,
isLocalBuild: !isCiBuild,
});
}
if (isDocsBuild) {
console.log('# Generating package documentation');
await buildDocs({
inputDir: tmpDir,
outputDir: resolvePath(projectRoot, 'docs/reference'),
});
}
}
main().catch(error => {
console.error(error.stack || String(error));
process.exit(1);
});
@@ -0,0 +1,120 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { OptionValues } from 'commander';
import { resolve as resolvePath } from 'path';
import fs from 'fs-extra';
import { spawnSync } from 'child_process';
import {
findSpecificPackageDirs,
createTemporaryTsConfig,
findPackageDirs,
categorizePackageDirs,
runApiExtraction,
runCliExtraction,
buildDocs,
} from './api-extractor';
export default async (paths: string[], opts: OptionValues) => {
const tmpDir = resolvePath(
process.cwd(),
'./node_modules/.cache/api-extractor',
);
const projectRoot = resolvePath(process.cwd());
const isCiBuild = opts.ci;
const isDocsBuild = opts.docs;
const runTsc = opts.tsc;
const selectedPackageDirs = await findSpecificPackageDirs(paths);
if (selectedPackageDirs && isCiBuild) {
throw new Error(
'Package path arguments are not supported together with the --ci flag',
);
}
if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) {
console.log('');
console.log(
'TIP: You can generate api-reports for select packages by passing package paths:',
);
console.log('');
console.log(
' yarn build:api-reports packages/config packages/core-plugin-api',
);
console.log('');
}
let temporaryTsConfigPath: string | undefined;
if (selectedPackageDirs) {
temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs);
}
const tsconfigFilePath =
temporaryTsConfigPath ?? resolvePath(projectRoot, 'tsconfig.json');
if (runTsc) {
await fs.remove(resolvePath(projectRoot, 'dist-types'));
const { status } = spawnSync(
'yarn',
[
'tsc',
['--project', tsconfigFilePath],
['--skipLibCheck', 'false'],
['--incremental', 'false'],
].flat(),
{
stdio: 'inherit',
shell: true,
cwd: projectRoot,
},
);
if (status !== 0) {
process.exit(status || undefined);
}
}
const packageDirs = selectedPackageDirs ?? (await findPackageDirs());
const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs(
projectRoot,
packageDirs,
);
if (tsPackageDirs.length > 0) {
console.log('# Generating package API reports');
await runApiExtraction({
packageDirs: tsPackageDirs,
outputDir: tmpDir,
isLocalBuild: !isCiBuild,
tsconfigFilePath,
});
}
if (cliPackageDirs.length > 0) {
console.log('# Generating package CLI reports');
await runCliExtraction({
projectRoot,
packageDirs: cliPackageDirs,
isLocalBuild: !isCiBuild,
});
}
if (isDocsBuild) {
console.log('# Generating package documentation');
await buildDocs({
inputDir: tmpDir,
outputDir: resolvePath(projectRoot, 'docs/reference'),
});
}
};
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { Command } from 'commander';
import { exitWithError } from '../lib/errors';
export function registerCommands(program: Command) {
program
.command('api-reports [path...]')
.option('--ci', 'CI run checks that there is no changes on API reports')
.option('--tsc', 'executes the tsc compilation before extracting the APIs')
.option('--docs', 'generates the api documentation')
.description('Generate an API report for selected packages')
.action(
lazy(() => import('./api-reports/api-reports').then(m => m.default)),
);
}
// Wraps an action function so that it always exits and handles errors
function lazy(
getActionFunc: () => Promise<(...args: any[]) => Promise<void>>,
): (...args: any[]) => Promise<never> {
return async (...args: any[]) => {
try {
const actionFunc = await getActionFunc();
await actionFunc(...args);
process.exit(0);
} catch (error) {
assertError(error);
exitWithError(error);
}
};
}
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* CLI for Backstage repo tooling
*
* @packageDocumentation
*/
import { program } from 'commander';
import chalk from 'chalk';
import { exitWithError } from './lib/errors';
import { registerCommands } from './commands';
const main = (argv: string[]) => {
program.name('backstage-repo-tools').version('1.0');
registerCommands(program);
program.on('command:*', () => {
console.log();
console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`));
console.log();
program.outputHelp();
process.exit(1);
});
program.parse(argv);
};
process.on('unhandledRejection', rejection => {
if (rejection instanceof Error) {
exitWithError(rejection);
} else {
exitWithError(new Error(`Unknown rejection: '${rejection}'`));
}
});
main(process.argv);
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import chalk from 'chalk';
export class CustomError extends Error {
get name(): string {
return this.constructor.name;
}
}
export class ExitCodeError extends CustomError {
readonly code: number;
constructor(code: number, command?: string) {
super(
command
? `Command '${command}' exited with code ${code}`
: `Child exited with code ${code}`,
);
this.code = code;
}
}
export function exitWithError(error: Error): never {
if (error instanceof ExitCodeError) {
process.stderr.write(`\n${chalk.red(error.message)}\n\n`);
process.exit(error.code);
} else {
process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`);
process.exit(1);
}
}
export class NotFoundError extends CustomError {}
+1 -1
View File
@@ -58,7 +58,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/node": "^16.11.26",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -46,7 +46,7 @@
"@backstage/cli": "workspace:^",
"@types/marked": "^4.0.0",
"@types/supertest": "^2.0.8",
"msw": "^0.48.0",
"msw": "^0.49.0",
"supertest": "^6.1.3"
},
"files": [
+1 -1
View File
@@ -55,7 +55,7 @@
"@types/git-url-parse": "^9.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -35,7 +35,7 @@
"@backstage/cli": "workspace:^",
"@types/http-proxy-middleware": "^0.19.3",
"@types/supertest": "^2.0.8",
"msw": "^0.48.0",
"msw": "^0.49.0",
"supertest": "^6.1.6"
},
"files": [
+1 -1
View File
@@ -50,7 +50,7 @@
"@types/node": "^16.11.26",
"@types/object-hash": "^2.2.1",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -47,7 +47,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -45,7 +45,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -43,7 +43,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -66,7 +66,7 @@
"@types/node": "^16.11.26",
"@types/swagger-ui-react": "^4.1.1",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -45,7 +45,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -58,7 +58,7 @@
"@types/supertest": "^2.0.8",
"get-port": "^6.1.2",
"mock-fs": "^5.1.0",
"msw": "^0.48.0",
"msw": "^0.49.0",
"node-fetch": "^2.6.7",
"supertest": "^6.1.3"
},
+1 -1
View File
@@ -89,7 +89,7 @@
"@types/passport-saml": "^1.1.3",
"@types/passport-strategy": "^0.2.35",
"@types/xml2js": "^0.4.7",
"msw": "^0.48.0",
"msw": "^0.49.0",
"supertest": "^6.1.3"
},
"files": [
+1 -1
View File
@@ -35,7 +35,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"lodash": "^4.17.21",
"msw": "^0.48.0",
"msw": "^0.49.0",
"uuid": "^8.0.0"
},
"files": [
+1 -1
View File
@@ -37,7 +37,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
"msw": "^0.48.0",
"msw": "^0.49.0",
"supertest": "^6.1.6"
},
"files": [
+1 -1
View File
@@ -56,7 +56,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -48,7 +48,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -57,7 +57,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -54,7 +54,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -33,7 +33,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@openapitools/openapi-generator-cli": "^2.4.26",
"msw": "^0.48.0",
"msw": "^0.49.0",
"ts-morph": "^15.0.0"
},
"files": [
+1 -1
View File
@@ -51,7 +51,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"@types/recharts": "^1.8.15",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
@@ -53,7 +53,7 @@
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"alpha",
@@ -54,7 +54,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"alpha",
@@ -50,7 +50,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"alpha",
@@ -49,7 +49,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
@@ -48,7 +48,7 @@
"@backstage/cli": "workspace:^",
"@types/fs-extra": "^9.0.1",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
@@ -57,7 +57,7 @@
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
@@ -79,7 +79,7 @@ describe('assignGroupsToUsers', () => {
spec: {
type: 'team',
children: [],
members: ['u1', 'u2'],
members: ['default/u1', 'default/u2'],
},
},
{
@@ -17,6 +17,8 @@
import {
DEFAULT_NAMESPACE,
GroupEntity,
parseEntityRef,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
@@ -65,14 +67,20 @@ export function assignGroupsToUsers(
group.metadata.namespace !== DEFAULT_NAMESPACE
? `${group.metadata.namespace}/${group.metadata.name}`
: group.metadata.name;
return [groupKey, group.spec.members || []];
// Fully qualify member refs so they can be keyed off of since they may contain namespace prefixes
return [
groupKey,
group.spec.members?.map(m =>
stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })),
) || [],
];
}),
);
const usersByName = new Map(users.map(u => [u.metadata.name, u]));
for (const [groupName, userNames] of groupMemberUsers.entries()) {
for (const userName of userNames) {
const user = usersByName.get(userName);
const usersByRef = new Map(users.map(u => [stringifyEntityRef(u), u]));
for (const [groupName, userRefs] of groupMemberUsers.entries()) {
for (const ref of userRefs) {
const user = usersByRef.get(ref);
if (user && !user.spec.memberOf?.includes(groupName)) {
if (!user.spec.memberOf) {
user.spec.memberOf = [];
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { GroupEntity } from '@backstage/catalog-model';
import {
DEFAULT_NAMESPACE,
GroupEntity,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
DefaultGithubCredentialsProvider,
@@ -36,6 +41,7 @@ import {
assignGroupsToUsers,
buildOrgHierarchy,
defaultOrganizationTeamTransformer,
defaultUserTransformer,
getOrganizationTeams,
getOrganizationUsers,
GithubMultiOrgConfig,
@@ -141,8 +147,19 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
client,
orgConfig.name,
tokenType,
this.options.userTransformer,
async (githubUser, ctx): Promise<UserEntity | undefined> => {
const result = this.options.userTransformer
? await this.options.userTransformer(githubUser, ctx)
: await defaultUserTransformer(githubUser, ctx);
if (result) {
result.metadata.namespace = orgConfig.userNamespace;
}
return result;
},
);
const { groups } = await getOrganizationTeams(
client,
orgConfig.name,
@@ -153,6 +170,13 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
if (result) {
result.metadata.namespace = orgConfig.groupNamespace;
// Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here
result.spec.members = team.members.map(
user =>
`${orgConfig.userNamespace ?? DEFAULT_NAMESPACE}/${
user.login
}`,
);
}
return result;
@@ -164,15 +188,18 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
`Read ${users.length} GitHub users and ${groups.length} GitHub teams from ${orgConfig.name} in ${duration} seconds`,
);
let prefix: string = orgConfig.userNamespace ?? '';
if (prefix.length > 0) prefix += '/';
users.forEach(u => {
if (!allUsersMap.has(prefix + u.metadata.name)) {
allUsersMap.set(prefix + u.metadata.name, u);
// Grab current users from `allUsersMap` if they already exist in our
// pending users so we can append to their group membership relations
const pendingUsers = users.map(u => {
const userRef = stringifyEntityRef(u);
if (!allUsersMap.has(userRef)) {
allUsersMap.set(userRef, u);
}
return allUsersMap.get(userRef);
});
assignGroupsToUsers(users, groups);
assignGroupsToUsers(pendingUsers, groups);
buildOrgHierarchy(groups);
for (const group of groups) {
@@ -185,6 +212,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
}
}
// Emit all users at the end after all orgs have been processed
// so all memberships across org groups are accounted for
const allUsers = Array.from(allUsersMap.values());
for (const user of allUsers) {
emit(processingResult.entity(location, user));
@@ -85,7 +85,10 @@ describe('GithubOrgReaderProcessor', () => {
mockClient
.mockResolvedValueOnce({
organization: {
membersWithRole: { pageInfo: { hasNextPage: false }, nodes: [{}] },
membersWithRole: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'foo' }],
},
},
})
.mockResolvedValueOnce({
@@ -93,7 +96,12 @@ describe('GithubOrgReaderProcessor', () => {
teams: {
pageInfo: { hasNextPage: false },
nodes: [
{ members: { pageInfo: { hasNextPage: false }, nodes: [{}] } },
{
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'foo' }],
},
},
],
},
},
@@ -134,7 +142,10 @@ describe('GithubOrgReaderProcessor', () => {
mockClient
.mockResolvedValueOnce({
organization: {
membersWithRole: { pageInfo: { hasNextPage: false }, nodes: [{}] },
membersWithRole: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'foo' }],
},
},
})
.mockResolvedValueOnce({
@@ -142,7 +153,12 @@ describe('GithubOrgReaderProcessor', () => {
teams: {
pageInfo: { hasNextPage: false },
nodes: [
{ members: { pageInfo: { hasNextPage: false }, nodes: [{}] } },
{
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'foo' }],
},
},
],
},
},
@@ -54,7 +54,7 @@
"@types/lodash": "^4.14.151",
"@types/uuid": "^8.0.0",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"alpha",
@@ -55,7 +55,7 @@
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
"luxon": "^3.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"alpha",
+1 -1
View File
@@ -80,7 +80,7 @@
"@types/uuid": "^8.0.0",
"better-sqlite3": "^7.5.0",
"luxon": "^3.0.0",
"msw": "^0.48.0",
"msw": "^0.49.0",
"supertest": "^6.1.3",
"wait-for-expect": "^3.0.2"
},
+1 -1
View File
@@ -53,7 +53,7 @@
"@graphql-codegen/typescript": "^2.4.2",
"@graphql-codegen/typescript-resolvers": "^2.4.3",
"@graphql-tools/schema": "^9.0.0",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -68,7 +68,7 @@
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.0.0",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -63,7 +63,7 @@
"@types/humanize-duration": "^3.25.1",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -60,7 +60,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -49,7 +49,7 @@
"@types/luxon": "^3.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -42,7 +42,7 @@
"@backstage/cli": "workspace:^",
"@types/express-xml-bodyparser": "^0.3.2",
"@types/supertest": "^2.0.8",
"msw": "^0.48.0",
"msw": "^0.49.0",
"supertest": "^6.1.6",
"xml2js": "^0.4.23"
},
+1 -1
View File
@@ -56,7 +56,7 @@
"@types/node": "^16.11.26",
"@types/recharts": "^1.8.15",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+1 -1
View File
@@ -47,7 +47,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
+1 -1
View File
@@ -49,7 +49,7 @@
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist"
+15
View File
@@ -178,6 +178,21 @@ costInsights:
name: Metric C
```
### Base Currency (Optional)
In the case you would like to show your baseline costs on the graph on other currency than US dollars.
```yaml
## ./app-config.yaml
costInsights:
engineerCost: 200000
baseCurrency:
locale: nl-NL
options:
currency: EUR
minimumFractionDigits: 3
```
### Currencies (Optional)
In the `Cost Overview` panel, users can choose from a dropdown of currencies to see costs in, such as Engineers or USD. Currencies must be defined as keys on the `currencies` field. A user-friendly label and unit are **required**. If not set, the `defaultCurrencies` in `currency.ts` will be used.
+1
View File
@@ -227,6 +227,7 @@ export type ChartData = {
// @public (undocumented)
export type ConfigContextProps = {
baseCurrency: Intl.NumberFormat;
metrics: Metric[];
products: Product[];
icons: Icon[];
+52
View File
@@ -21,6 +21,58 @@ export interface Config {
*/
engineerCost: number;
/**
* @visibility frontend
*/
baseCurrency?: {
/**
* @visibility frontend
*/
locale?: string;
options?: {
/**
* @visibility frontend
*/
localeMatcher?: string | undefined;
/**
* @visibility frontend
*/
style?: string | undefined;
/**
* @visibility frontend
*/
currency?: string | undefined;
/**
* @visibility frontend
*/
currencySign?: string | undefined;
/**
* @visibility frontend
*/
useGrouping?: boolean | undefined;
/**
* @visibility frontend
*/
minimumIntegerDigits?: number | undefined;
/**
* @visibility frontend
*/
minimumFractionDigits?: number | undefined;
/**
* @visibility frontend
*/
maximumFractionDigits?: number | undefined;
/**
* @visibility frontend
*/
minimumSignificantDigits?: number | undefined;
/**
* @visibility frontend
*/
maximumSignificantDigits?: number | undefined;
};
};
products?: {
[kind: string]: {
/**
+1 -1
View File
@@ -75,7 +75,7 @@
"@types/yup": "^0.29.13",
"canvas": "^2.10.2",
"cross-fetch": "^3.1.5",
"msw": "^0.48.0"
"msw": "^0.49.0"
},
"files": [
"dist",
@@ -18,7 +18,7 @@ import React from 'react';
import { fireEvent } from '@testing-library/react';
import { BarChart, BarChartProps } from './BarChart';
import { ResourceData } from '../../types';
import { createMockEntity } from '../../testUtils';
import { createMockEntity, MockConfigProvider } from '../../testUtils';
import { resourceSort } from '../../utils/sort';
import { renderInTestApp } from '@backstage/test-utils';
@@ -46,11 +46,13 @@ const renderWithProps = ({
resources = MockResources,
}: BarChartProps) => {
return renderInTestApp(
<BarChart
responsive={responsive}
displayAmount={displayAmount}
resources={resources}
/>,
<MockConfigProvider>
<BarChart
responsive={responsive}
displayAmount={displayAmount}
resources={resources}
/>
</MockConfigProvider>,
);
};
@@ -40,20 +40,24 @@ import { notEmpty } from '../../utils/assert';
import { useBarChartStyles } from '../../utils/styles';
import { resourceSort } from '../../utils/sort';
import { isInvalid, titleOf, tooltipItemOf } from '../../utils/graphs';
import { TooltipRenderer } from '../../types/Tooltip';
import { TooltipRenderer } from '../../types';
import { useConfig } from '../../hooks';
export const defaultTooltip: TooltipRenderer = ({ label, payload = [] }) => {
if (isInvalid({ label, payload })) return null;
const defaultTooltip = (baseCurrency: Intl.NumberFormat) => {
const tooltip: TooltipRenderer = ({ label, payload = [] }) => {
if (isInvalid({ label, payload })) return null;
const title = titleOf(label);
const items = payload.map(tooltipItemOf).filter(notEmpty);
return (
<BarChartTooltip title={title}>
{items.map((item, index) => (
<BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
))}
</BarChartTooltip>
);
const title = titleOf(label);
const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty);
return (
<BarChartTooltip title={title}>
{items.map((item, index) => (
<BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
))}
</BarChartTooltip>
);
};
return tooltip;
};
/** @public */
@@ -69,12 +73,14 @@ export type BarChartProps = {
/** @public */
export const BarChart = (props: BarChartProps) => {
const { baseCurrency } = useConfig();
const {
resources,
responsive = true,
displayAmount = 6,
options = {},
tooltip = defaultTooltip,
tooltip = defaultTooltip(baseCurrency),
onClick,
onMouseMove,
} = props;
@@ -164,7 +170,7 @@ export const BarChart = (props: BarChartProps) => {
tick={BarChartTick}
/>
<YAxis
tickFormatter={currencyFormatter.format}
tickFormatter={currencyFormatter(baseCurrency).format}
domain={[() => 0, globalResourcesMax]}
tick={styles.axis}
/>
@@ -17,11 +17,14 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { BarChartLegend } from './BarChartLegend';
import { MockConfigProvider } from '../../testUtils';
describe('<BarChartLegend />', () => {
it(`Should display the correct cost start and end`, async () => {
const rendered = await renderInTestApp(
<BarChartLegend costStart={1000} costEnd={5000} />,
<MockConfigProvider>
<BarChartLegend costStart={1000} costEnd={5000} />,
</MockConfigProvider>,
);
expect(rendered.getByText(/\$1,000/)).toBeInTheDocument();
expect(rendered.queryByText(/\$5,000/)).toBeInTheDocument();
@@ -20,6 +20,7 @@ import { LegendItem } from '../LegendItem';
import { currencyFormatter } from '../../utils/formatters';
import { CostInsightsTheme } from '../../types';
import { useBarChartLayoutStyles as useStyles } from '../../utils/styles';
import { useConfig } from '../../hooks';
/** @public */
export type BarChartLegendOptions = {
@@ -45,6 +46,7 @@ export const BarChartLegend = (
const theme = useTheme<CostInsightsTheme>();
const classes = useStyles();
const { baseCurrency } = useConfig();
const data = Object.assign(
{
@@ -63,7 +65,7 @@ export const BarChartLegend = (
title={data.previousName}
markerColor={options.hideMarker ? undefined : data.previousFill}
>
{currencyFormatter.format(costStart)}
{currencyFormatter(baseCurrency).format(costStart)}
</LegendItem>
</Box>
<Box marginRight={2}>
@@ -71,7 +73,7 @@ export const BarChartLegend = (
title={data.currentName}
markerColor={options.hideMarker ? undefined : data.currentFill}
>
{currencyFormatter.format(costEnd)}
{currencyFormatter(baseCurrency).format(costEnd)}
</LegendItem>
</Box>
{children}
@@ -16,41 +16,41 @@
import React, { useState } from 'react';
import { DateTime } from 'luxon';
import {
useTheme,
Box,
Typography,
Divider,
emphasize,
Typography,
useTheme,
} from '@material-ui/core';
import { default as FullScreenIcon } from '@material-ui/icons/Fullscreen';
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip as RechartsTooltip,
XAxis,
YAxis,
Tooltip as RechartsTooltip,
Area,
ResponsiveContainer,
CartesianGrid,
} from 'recharts';
import { Cost, DEFAULT_DATE_FORMAT, CostInsightsTheme } from '../../types';
import { Cost, CostInsightsTheme, DEFAULT_DATE_FORMAT } from '../../types';
import {
BarChartLegend,
BarChartTooltip as Tooltip,
BarChartTooltipItem as TooltipItem,
BarChartLegend,
} from '../BarChart';
import {
overviewGraphTickFormatter,
formatGraphValue,
isInvalid,
overviewGraphTickFormatter,
} from '../../utils/graphs';
import { useCostOverviewStyles as useStyles } from '../../utils/styles';
import { useFilters, useLastCompleteBillingDate } from '../../hooks';
import { useConfig, useFilters, useLastCompleteBillingDate } from '../../hooks';
import { mapFiltersToProps } from './selector';
import { getPreviousPeriodTotalCost } from '../../utils/change';
import { formatPeriod } from '../../utils/formatters';
import { aggregationSum } from '../../utils/sum';
import { BarChartLegendOptions } from '../BarChart/BarChartLegend';
import { TooltipRenderer } from '../../types/Tooltip';
import { BarChartLegendOptions } from '../BarChart';
import { TooltipRenderer } from '../../types';
export type CostOverviewBreakdownChartProps = {
costBreakdown: Cost[];
@@ -65,6 +65,7 @@ export const CostOverviewBreakdownChart = ({
}: CostOverviewBreakdownChartProps) => {
const theme = useTheme<CostInsightsTheme>();
const classes = useStyles(theme);
const { baseCurrency } = useConfig();
const lastCompleteBillingDate = useLastCompleteBillingDate();
const { duration } = useFilters(mapFiltersToProps);
const [isExpanded, setExpanded] = useState(false);
@@ -185,9 +186,10 @@ export const CostOverviewBreakdownChart = ({
? DateTime.fromMillis(label)
: DateTime.fromISO(label!);
const dateTitle = date.toUTC().toFormat(DEFAULT_DATE_FORMAT);
const formatGraphValueWith = formatGraphValue(baseCurrency);
const items = payload.map((p, i) => ({
label: p.dataKey as string,
value: formatGraphValue(Number(p.value), i),
value: formatGraphValueWith(Number(p.value), i),
fill: p.color!,
}));
const expandText = (
@@ -253,7 +255,7 @@ export const CostOverviewBreakdownChart = ({
<YAxis
domain={[() => 0, 'dataMax']}
tick={{ fill: classes.axis.fill }}
tickFormatter={formatGraphValue}
tickFormatter={formatGraphValue(baseCurrency)}
width={classes.yAxis.width}
/>
{renderAreas()}
@@ -50,6 +50,7 @@ import { aggregationSort } from '../../utils/sort';
import { CostOverviewLegend } from './CostOverviewLegend';
import { TooltipRenderer } from '../../types';
import { useCostInsightsOptions } from '../../options';
import { useConfig } from '../../hooks';
type CostOverviewChartProps = {
metric: Maybe<Metric>;
@@ -66,6 +67,7 @@ export const CostOverviewChart = ({
}: CostOverviewChartProps) => {
const theme = useTheme<CostInsightsTheme>();
const styles = useStyles(theme);
const { baseCurrency } = useConfig();
const data = {
dailyCost: {
@@ -107,6 +109,7 @@ export const CostOverviewChart = ({
? DateTime.fromMillis(label)
: DateTime.fromISO(label!);
const title = date.toUTC().toFormat(DEFAULT_DATE_FORMAT);
const formatGraphValueWith = formatGraphValue(baseCurrency);
const items = payload
.filter(p => dataKeys.includes(p.dataKey as string))
.map((p, i) => ({
@@ -116,8 +119,8 @@ export const CostOverviewChart = ({
: data.metric.name,
value:
p.dataKey === data.dailyCost.dataKey
? formatGraphValue(Number(p.value), i, data.dailyCost.format)
: formatGraphValue(Number(p.value), i, data.metric.format),
? formatGraphValueWith(Number(p.value), i, data.dailyCost.format)
: formatGraphValueWith(Number(p.value), i, data.metric.format),
fill:
p.dataKey === data.dailyCost.dataKey
? theme.palette.blue
@@ -160,7 +163,7 @@ export const CostOverviewChart = ({
<YAxis
domain={[() => 0, 'dataMax']}
tick={{ fill: styles.axis.fill }}
tickFormatter={formatGraphValue}
tickFormatter={formatGraphValue(baseCurrency)}
width={styles.yAxis.width}
yAxisId={data.dailyCost.dataKey}
/>
@@ -19,6 +19,7 @@ import { wrapInTestApp } from '@backstage/test-utils';
import { ProductEntityDialog } from './ProductEntityDialog';
import { render } from '@testing-library/react';
import { Entity } from '../../types';
import { MockConfigProvider } from '../../testUtils';
const atomicEntity: Entity = {
id: null,
@@ -86,11 +87,13 @@ describe('<ProductEntityDialog/>', () => {
it('Should show a tab for a single sub-entity type', () => {
const { getByText } = render(
wrapInTestApp(
<ProductEntityDialog
open
entity={singleBreakdownEntity}
onClose={jest.fn()}
/>,
<MockConfigProvider>
<ProductEntityDialog
open
entity={singleBreakdownEntity}
onClose={jest.fn()}
/>
</MockConfigProvider>,
),
);
expect(getByText('Breakdown by SKU')).toBeInTheDocument();
@@ -99,11 +102,13 @@ describe('<ProductEntityDialog/>', () => {
it('Should show tabs when multiple sub-entity types exist', () => {
const { getByText } = render(
wrapInTestApp(
<ProductEntityDialog
open
entity={multiBreakdownEntity}
onClose={jest.fn()}
/>,
<MockConfigProvider>
<ProductEntityDialog
open
entity={multiBreakdownEntity}
onClose={jest.fn()}
/>
</MockConfigProvider>,
),
);
expect(getByText('Breakdown by SKU')).toBeInTheDocument();
@@ -17,11 +17,12 @@
import React from 'react';
import classnames from 'classnames';
import { Typography } from '@material-ui/core';
import { costFormatter, formatChange } from '../../utils/formatters';
import { formatChange } from '../../utils/formatters';
import { useEntityDialogStyles as useStyles } from '../../utils/styles';
import { CostGrowthIndicator } from '../CostGrowth';
import { BarChartOptions, ChangeStatistic, Entity } from '../../types';
import { Table, TableColumn } from '@backstage/core-components';
import { useConfig } from '../../hooks';
export type ProductEntityTableOptions = Partial<
Pick<BarChartOptions, 'previousName' | 'currentName'>
@@ -35,36 +36,38 @@ type RowData = {
change: ChangeStatistic;
};
function createRenderer(col: keyof RowData, classes: Record<string, string>) {
return function render(rowData: {}): JSX.Element {
const row = rowData as RowData;
const rowStyles = classnames(classes.row, {
[classes.rowTotal]: row.id === 'total',
[classes.colFirst]: col === 'label',
[classes.colLast]: col === 'change',
});
const createRenderer =
(baseCurrency: Intl.NumberFormat) =>
(col: keyof RowData, classes: Record<string, string>) => {
return function render(rowData: {}): JSX.Element {
const row = rowData as RowData;
const rowStyles = classnames(classes.row, {
[classes.rowTotal]: row.id === 'total',
[classes.colFirst]: col === 'label',
[classes.colLast]: col === 'change',
});
switch (col) {
case 'previous':
case 'current':
return (
<Typography className={rowStyles}>
{costFormatter.format(row[col])}
</Typography>
);
case 'change':
return (
<CostGrowthIndicator
className={rowStyles}
change={row.change}
formatter={formatChange}
/>
);
default:
return <Typography className={rowStyles}>{row.label}</Typography>;
}
switch (col) {
case 'previous':
case 'current':
return (
<Typography className={rowStyles}>
{baseCurrency.format(row[col])}
</Typography>
);
case 'change':
return (
<CostGrowthIndicator
className={rowStyles}
change={row.change}
formatter={formatChange}
/>
);
default:
return <Typography className={rowStyles}>{row.label}</Typography>;
}
};
};
}
// material-table does not support fixed rows. Override the sorting algorithm
// to force Total row to bottom by default or when a user sort toggles a column.
@@ -99,6 +102,7 @@ export const ProductEntityTable = ({
options,
}: ProductEntityTableProps) => {
const classes = useStyles();
const { baseCurrency } = useConfig();
const entities = entity.entities[entityLabel];
const data = Object.assign(
@@ -116,7 +120,7 @@ export const ProductEntityTable = ({
{
field: 'label',
title: <Typography className={firstColClasses}>{entityLabel}</Typography>,
render: createRenderer('label', classes),
render: createRenderer(baseCurrency)('label', classes),
customSort: createSorter('label'),
width: '33.33%',
},
@@ -126,7 +130,7 @@ export const ProductEntityTable = ({
<Typography className={classes.column}>{data.previousName}</Typography>
),
align: 'right',
render: createRenderer('previous', classes),
render: createRenderer(baseCurrency)('previous', classes),
customSort: createSorter('previous'),
},
{
@@ -135,14 +139,14 @@ export const ProductEntityTable = ({
<Typography className={classes.column}>{data.currentName}</Typography>
),
align: 'right',
render: createRenderer('current', classes),
render: createRenderer(baseCurrency)('current', classes),
customSort: createSorter('current'),
},
{
field: 'change',
title: <Typography className={lastColClasses}>Change</Typography>,
align: 'right',
render: createRenderer('change', classes),
render: createRenderer(baseCurrency)('change', classes),
customSort: createSorter('change'),
},
];
@@ -50,7 +50,8 @@ import {
} from '../../utils/styles';
import { Duration, Entity, Maybe } from '../../types';
import { choose } from '../../utils/change';
import { TooltipRenderer } from '../../types/Tooltip';
import { TooltipRenderer } from '../../types';
import { useConfig } from '../../hooks';
export type ProductInsightsChartProps = {
billingDate: string;
@@ -65,6 +66,7 @@ export const ProductInsightsChart = ({
}: ProductInsightsChartProps) => {
const classes = useStyles();
const layoutClasses = useLayoutStyles();
const { baseCurrency } = useConfig();
// Only a single entities Record for the root product entity is supported
const entities = useMemo(() => {
@@ -131,7 +133,7 @@ export const ProductInsightsChart = ({
const id = label === '' ? null : label;
const title = titleOf(label);
const items = payload.map(tooltipItemOf).filter(notEmpty);
const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty);
const activeEntity = findAlways(entities, e => e.id === id);
const breakdowns = Object.keys(activeEntity.entities);
@@ -19,6 +19,7 @@ import { UnlabeledDataflowAlertCard } from './UnlabeledDataflowAlertCard';
import {
createMockUnlabeledDataflowData,
createMockUnlabeledDataflowAlertProject,
MockConfigProvider,
} from '../../testUtils';
import { renderInTestApp } from '@backstage/test-utils';
@@ -61,9 +62,11 @@ describe('<UnlabeledDataflowAlertCard />', () => {
'projects with unlabeled Dataflow jobs in the last 30 days.',
);
const rendered = await renderInTestApp(
<UnlabeledDataflowAlertCard
alert={MockUnlabeledDataflowAlertMultipleProjects}
/>,
<MockConfigProvider>
<UnlabeledDataflowAlertCard
alert={MockUnlabeledDataflowAlertMultipleProjects}
/>
</MockConfigProvider>,
);
expect(rendered.getByText(subheader)).toBeInTheDocument();
});
@@ -71,9 +74,11 @@ describe('<UnlabeledDataflowAlertCard />', () => {
it('renders the correct subheader for a single project', async () => {
const subheader = new RegExp('1 project');
const rendered = await renderInTestApp(
<UnlabeledDataflowAlertCard
alert={MockUnlabeledDataflowAlertSingleProject}
/>,
<MockConfigProvider>
<UnlabeledDataflowAlertCard
alert={MockUnlabeledDataflowAlertSingleProject}
/>
</MockConfigProvider>,
);
expect(rendered.getByText(subheader)).toBeInTheDocument();
});
+46 -1
View File
@@ -25,7 +25,7 @@ import { Config as BackstageConfig } from '@backstage/config';
import { Currency, Icon, Metric, Product } from '../types';
import { getIcon } from '../utils/navigation';
import { validateCurrencies, validateMetrics } from '../utils/config';
import { defaultCurrencies } from '../utils/currency';
import { createCurrencyFormat, defaultCurrencies } from '../utils/currency';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
/*
@@ -46,6 +46,11 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api';
* default: true
* metricB:
* name: Metric B
* baseCurrency:
* locale: nl-NL
* options:
* currency: EUR
* minimumFractionDigits: 3
* currencies:
* currencyA:
* label: Currency A
@@ -60,6 +65,7 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api';
/** @public */
export type ConfigContextProps = {
baseCurrency: Intl.NumberFormat;
metrics: Metric[];
products: Product[];
icons: Icon[];
@@ -72,6 +78,7 @@ export const ConfigContext = createContext<ConfigContextProps | undefined>(
);
const defaultState: ConfigContextProps = {
baseCurrency: createCurrencyFormat(),
metrics: [],
products: [],
icons: [],
@@ -110,6 +117,42 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
return [];
}
function getBaseCurrency(): Intl.NumberFormat {
const baseCurrency = c.getOptionalConfig('costInsights.baseCurrency');
if (baseCurrency) {
const options = baseCurrency.getOptionalConfig('options');
return new Intl.NumberFormat(
baseCurrency.getOptionalString('locale'),
options
? {
localeMatcher: options.getOptionalString('localeMatcher'),
style: 'currency',
currency: options.getOptionalString('currency'),
currencySign: options.getOptionalString('currencySign'),
useGrouping: options.getOptionalBoolean('useGrouping'),
minimumIntegerDigits: options.getOptionalNumber(
'minimumIntegerDigits',
),
minimumFractionDigits: options.getOptionalNumber(
'minimumFractionDigits',
),
maximumFractionDigits: options.getOptionalNumber(
'maximumFractionDigits',
),
minimumSignificantDigits: options.getOptionalNumber(
'minimumSignificantDigits',
),
maximumSignificantDigits: options.getOptionalNumber(
'maximumSignificantDigits',
),
}
: undefined,
);
}
return defaultState.baseCurrency;
}
function getCurrencies(): Currency[] {
const currencies = c.getOptionalConfig('costInsights.currencies');
if (currencies) {
@@ -141,6 +184,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
}
function getConfig() {
const baseCurrency = getBaseCurrency();
const products = getProducts();
const metrics = getMetrics();
const engineerCost = getEngineerCost();
@@ -152,6 +196,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => {
setConfig(prevState => ({
...prevState,
baseCurrency,
metrics,
products,
engineerCost,

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