Compare commits

..

No commits in common. "main" and "@backstage-community/plugin-jfrog-artifactory@1.18.1" have entirely different histories.

394 changed files with 25497 additions and 42742 deletions

4
.github/CODEOWNERS vendored
View File

@ -13,7 +13,6 @@ yarn.lock @backsta
/workspaces/acr @backstage/community-plugins-maintainers @christoph-jerolimov @ciiay @debsmita1 @divyanshiGupta @its-mitesh-kumar @logonoff
/workspaces/acs @backstage/community-plugins-maintainers @sachaudh @alwayshooin @dvail @maknop
/workspaces/adr @backstage/community-plugins-maintainers @kuangp
/workspaces/agent-forge @backstage/community-plugins-maintainers @sbraicu @sriaradhyula @subbaksh @suwhang-cisco
/workspaces/airbrake @backstage/community-plugins-maintainers
/workspaces/allure @backstage/community-plugins-maintainers
/workspaces/amplication @backstage/community-plugins-maintainers @itainathaniel
@ -32,7 +31,6 @@ yarn.lock @backsta
/workspaces/bazaar @backstage/community-plugins-maintainers
/workspaces/bitrise @backstage/community-plugins-maintainers @backstage/sda-se-reviewers
/workspaces/blackduck @backstage/community-plugins-maintainers @deepan10
/workspaces/bookmarks @backstage/community-plugins-maintainers @logonoff @christoph-jerolimov @ciiay @debsmita1 @divyanshiGupta @its-mitesh-kumar @lokanandaprabhu
/workspaces/cicd-statistics @backstage/community-plugins-maintainers
/workspaces/cloudbuild @backstage/community-plugins-maintainers
/workspaces/code-climate @backstage/community-plugins-maintainers
@ -81,7 +79,7 @@ yarn.lock @backsta
/workspaces/nexus-repository-manager @backstage/community-plugins-maintainers @ciiay @debsmita1 @jessicajhee
/workspaces/nomad @backstage/community-plugins-maintainers
/workspaces/noop @backstage/community-plugins-maintainers
/workspaces/npm @backstage/community-plugins-maintainers @christoph-jerolimov @ciiay @karthikjeeyar @logonoff
/workspaces/npm @backstage/community-plugins-maintainers @christoph-jerolimov @ciiay @karthikjeeyar
/workspaces/ocm @backstage/community-plugins-maintainers @christoph-jerolimov @ciiay @debsmita1 @divyanshiGupta @its-mitesh-kumar @logonoff
/workspaces/octopus-deploy @backstage/community-plugins-maintainers @jmezach
/workspaces/odo @backstage/community-plugins-maintainers

View File

@ -33,7 +33,6 @@ body:
- bazaar
- bitrise
- blackduck
- bookmarks
- cicd-statistics
- cloudbuild
- code-climate

View File

@ -34,7 +34,6 @@ body:
- bazaar
- bitrise
- blackduck
- bookmarks
- cicd-statistics
- cloudbuild
- code-climate

View File

@ -24,7 +24,6 @@ attributes:
- bazaar
- bitrise
- blackduck
- bookmarks
- cicd-statistics
- cloudbuild
- code-climate

3
.github/labeler.yml vendored
View File

@ -58,9 +58,6 @@ workspace/bitrise:
workspace/blackduck:
- "Workspace\\s*blackduck"
workspace/bookmarks:
- "Workspace\\s*bookmarks"
workspace/cicd-statistics:
- "Workspace\\s*cicd-statistics"

View File

@ -78,13 +78,6 @@ jobs:
- name: 'Set release name'
id: set_release_name
run: node scripts/ci/set-release-name.js ${{ matrix.workspace }} ${{ inputs.release_line || 'main' }}
- name: 'Check current and release versions'
id: check
run: |
if [[ "${{ steps.set_release_name.outputs.release_version }}" == "${{ steps.set_release_name.outputs.current_version }}" ]]; then
echo "Backstage release version and current workspace version are the same, skipping version bump"
exit 1 # Non-zero exit code fails the step and job
fi
- name: 'Configure git'
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
@ -104,7 +97,7 @@ jobs:
YARN_ENABLE_IMMUTABLE_INSTALLS: false
- name: Run dedupe
working-directory: ./workspaces/${{ matrix.workspace }}
run: yarn dedupe
run: yarn dedupe
- name: 'Check for changes'
id: check_for_changes
run: |

View File

@ -28,7 +28,7 @@
"eslint-plugin-notice": "^0.9.10",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-testing-library": "^6.0.0",
"fs-extra": "11.3.1",
"fs-extra": "11.3.0",
"husky": "^9.0.11",
"js-yaml": "^4.1.0",
"lint-staged": "^15.2.2",

View File

@ -1,5 +0,0 @@
---
'@backstage-community/plugin-acr': patch
---
Updated dependency `@testing-library/jest-dom` to `6.7.0`.

View File

@ -62,7 +62,7 @@
"@backstage/core-app-api": "^1.18.0",
"@backstage/dev-utils": "^1.1.12",
"@backstage/test-utils": "^1.7.10",
"@testing-library/jest-dom": "6.7.0",
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "14.3.1",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.6.1",

View File

@ -2725,7 +2725,7 @@ __metadata:
"@backstage/theme": "npm:^0.6.7"
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.11.3"
"@testing-library/jest-dom": "npm:6.7.0"
"@testing-library/jest-dom": "npm:6.6.4"
"@testing-library/react": "npm:14.3.1"
"@testing-library/react-hooks": "npm:8.0.1"
"@testing-library/user-event": "npm:14.6.1"
@ -11690,17 +11690,18 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/jest-dom@npm:6.7.0, @testing-library/jest-dom@npm:^6.0.0":
version: 6.7.0
resolution: "@testing-library/jest-dom@npm:6.7.0"
"@testing-library/jest-dom@npm:6.6.4, @testing-library/jest-dom@npm:^6.0.0":
version: 6.6.4
resolution: "@testing-library/jest-dom@npm:6.6.4"
dependencies:
"@adobe/css-tools": "npm:^4.4.0"
aria-query: "npm:^5.0.0"
css.escape: "npm:^1.5.1"
dom-accessibility-api: "npm:^0.6.3"
lodash: "npm:^4.17.21"
picocolors: "npm:^1.1.1"
redent: "npm:^3.0.0"
checksum: 10/c994f028b6f2d49c18c9fd6050af7f3316fb0afd03d0ba15d03b177f0f046a0308302dd52ab289fad8794e16a88e4d724b5f23caa007cf343a4b5e435efb84d9
checksum: 10/5e67112c789f884fb75b279c2cddfdd0995a012a7847a03c474e4134f0d213934ee70c97433bca26b45e3a5ffa56faafe6499c8e57841179c4f2bd80eef429cd
languageName: node
linkType: hard

View File

@ -47,7 +47,7 @@
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.9",
"@backstage/plugin-catalog-backend-module-unprocessed": "^0.6.1",
"@backstage/plugin-scaffolder": "^1.32.0",
"@backstage/plugin-scaffolder-backend": "^2.1.1",
"@backstage/plugin-scaffolder-backend": "^2.0.0",
"@backstage/repo-tools": "^0.14.0",
"@changesets/cli": "^2.28.1",
"@eslint/js": "^9.19.0",

View File

@ -39,7 +39,7 @@
"@backstage/plugin-permission-node": "^0.10.1",
"@backstage/plugin-proxy-backend": "^0.6.3",
"@backstage/plugin-scaffolder": "^1.32.0",
"@backstage/plugin-scaffolder-backend": "^2.1.1",
"@backstage/plugin-scaffolder-backend": "^2.0.0",
"@backstage/plugin-search-backend": "^2.0.3",
"@backstage/plugin-search-backend-module-catalog": "^0.3.5",
"@backstage/plugin-search-backend-module-pg": "^0.5.45",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
---
'@backstage-community/plugin-analytics-module-matomo': patch
'@backstage-community/plugin-analytics-provider-segment': patch
---
Updated dependency `@testing-library/jest-dom` to `6.7.0`.

View File

@ -44,7 +44,7 @@
"@backstage/core-app-api": "^1.17.1",
"@backstage/core-components": "^0.17.3",
"@backstage/dev-utils": "^1.1.11",
"@testing-library/jest-dom": "6.7.0",
"@testing-library/jest-dom": "6.6.4",
"@types/node": "22.15.29",
"cross-fetch": "4.0.0",
"msw": "1.3.5"

View File

@ -54,7 +54,7 @@
"@backstage/dev-utils": "^1.1.11",
"@backstage/test-utils": "^1.7.9",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "6.7.0",
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "14.3.1",
"@testing-library/user-event": "14.6.1",
"@types/node": "22.15.29",

View File

@ -1791,7 +1791,7 @@ __metadata:
"@backstage/core-components": "npm:^0.17.3"
"@backstage/core-plugin-api": "npm:^1.10.8"
"@backstage/dev-utils": "npm:^1.1.11"
"@testing-library/jest-dom": "npm:6.7.0"
"@testing-library/jest-dom": "npm:6.6.4"
"@types/node": "npm:22.15.29"
cross-fetch: "npm:4.0.0"
msw: "npm:1.3.5"
@ -1838,7 +1838,7 @@ __metadata:
"@material-ui/lab": "npm:4.0.0-alpha.61"
"@segment/analytics-next": "npm:^1.58.0"
"@testing-library/dom": "npm:9.3.4"
"@testing-library/jest-dom": "npm:6.7.0"
"@testing-library/jest-dom": "npm:6.6.4"
"@testing-library/react": "npm:14.3.1"
"@testing-library/user-event": "npm:14.6.1"
"@types/node": "npm:22.15.29"
@ -6491,17 +6491,18 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/jest-dom@npm:6.7.0, @testing-library/jest-dom@npm:^6.0.0":
version: 6.7.0
resolution: "@testing-library/jest-dom@npm:6.7.0"
"@testing-library/jest-dom@npm:6.6.4, @testing-library/jest-dom@npm:^6.0.0":
version: 6.6.4
resolution: "@testing-library/jest-dom@npm:6.6.4"
dependencies:
"@adobe/css-tools": "npm:^4.4.0"
aria-query: "npm:^5.0.0"
css.escape: "npm:^1.5.1"
dom-accessibility-api: "npm:^0.6.3"
lodash: "npm:^4.17.21"
picocolors: "npm:^1.1.1"
redent: "npm:^3.0.0"
checksum: 10/c994f028b6f2d49c18c9fd6050af7f3316fb0afd03d0ba15d03b177f0f046a0308302dd52ab289fad8794e16a88e4d724b5f23caa007cf343a4b5e435efb84d9
checksum: 10/5e67112c789f884fb75b279c2cddfdd0995a012a7847a03c474e4134f0d213934ee70c97433bca26b45e3a5ffa56faafe6499c8e57841179c4f2bd80eef429cd
languageName: node
linkType: hard

File diff suppressed because one or more lines are too long

View File

@ -1,4 +0,0 @@
plugins:
- checksum: 8af7b3f2d7d19cacc7a3712f871efcb6208ba283a1f532260b0cba80c2cb66ed772b207b5ba41b8c5d64dd8d5e0c0e15bbb445bd14afac491712965211ba027c
path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs
spec: 'https://versions.backstage.io/v1/releases/1.42.3/yarn-plugin'

View File

@ -1,7 +1,6 @@
app:
title: Azure DevOps Example App
baseUrl: http://localhost:3000
packages: all
organization:
name: Azure DevOps Example

View File

@ -1,3 +1,3 @@
{
"version": "1.42.3"
"version": "1.41.1"
}

View File

@ -36,9 +36,9 @@
"directory": "workspaces/azure-devops"
},
"devDependencies": {
"@backstage/cli": "backstage:^",
"@backstage/e2e-test-utils": "backstage:^",
"@backstage/repo-tools": "backstage:^",
"@backstage/cli": "^0.33.1",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/repo-tools": "^0.15.0",
"@changesets/cli": "^2.27.1",
"knip": "^5.27.4",
"node-gyp": "^10.0.0",
@ -58,5 +58,9 @@
"*.{json,md}": [
"prettier --write"
]
},
"dependencies": {
"@backstage-community/plugin-azure-devops": "workspace:^",
"@backstage-community/plugin-azure-devops-backend": "workspace:^"
}
}

View File

@ -20,29 +20,29 @@
},
"dependencies": {
"@backstage-community/plugin-azure-devops": "workspace:^",
"@backstage/app-defaults": "backstage:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/cli": "backstage:^",
"@backstage/core-app-api": "backstage:^",
"@backstage/core-components": "backstage:^",
"@backstage/core-plugin-api": "backstage:^",
"@backstage/integration-react": "backstage:^",
"@backstage/plugin-api-docs": "backstage:^",
"@backstage/plugin-catalog": "backstage:^",
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-catalog-graph": "backstage:^",
"@backstage/plugin-catalog-import": "backstage:^",
"@backstage/plugin-catalog-react": "backstage:^",
"@backstage/plugin-org": "backstage:^",
"@backstage/plugin-permission-react": "backstage:^",
"@backstage/plugin-scaffolder": "backstage:^",
"@backstage/plugin-search": "backstage:^",
"@backstage/plugin-search-react": "backstage:^",
"@backstage/plugin-techdocs": "backstage:^",
"@backstage/plugin-techdocs-module-addons-contrib": "backstage:^",
"@backstage/plugin-techdocs-react": "backstage:^",
"@backstage/plugin-user-settings": "backstage:^",
"@backstage/theme": "backstage:^",
"@backstage/app-defaults": "^1.6.4",
"@backstage/catalog-model": "^1.7.5",
"@backstage/cli": "^0.33.1",
"@backstage/core-app-api": "^1.18.0",
"@backstage/core-components": "^0.17.4",
"@backstage/core-plugin-api": "^1.10.9",
"@backstage/integration-react": "^1.2.9",
"@backstage/plugin-api-docs": "^0.12.9",
"@backstage/plugin-catalog": "^1.31.1",
"@backstage/plugin-catalog-common": "^1.1.5",
"@backstage/plugin-catalog-graph": "^0.4.21",
"@backstage/plugin-catalog-import": "^0.13.3",
"@backstage/plugin-catalog-react": "^1.19.1",
"@backstage/plugin-org": "^0.6.41",
"@backstage/plugin-permission-react": "^0.4.36",
"@backstage/plugin-scaffolder": "^1.33.0",
"@backstage/plugin-search": "^1.4.28",
"@backstage/plugin-search-react": "^1.9.2",
"@backstage/plugin-techdocs": "^1.13.2",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.26",
"@backstage/plugin-techdocs-react": "^1.3.1",
"@backstage/plugin-user-settings": "^0.8.24",
"@backstage/theme": "^0.6.7",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"react": "^18.0.2",
@ -52,7 +52,7 @@
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/test-utils": "backstage:^",
"@backstage/test-utils": "^1.7.10",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",

View File

@ -25,26 +25,26 @@
"@backstage-community/plugin-catalog-backend-module-azure-devops-annotator-processor": "workspace:^",
"@backstage-community/plugin-scaffolder-backend-module-azure-devops": "workspace:^",
"@backstage-community/plugin-scaffolder-backend-module-dotnet": "workspace:^",
"@backstage/backend-defaults": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/plugin-app-backend": "backstage:^",
"@backstage/plugin-auth-backend": "backstage:^",
"@backstage/plugin-auth-backend-module-guest-provider": "backstage:^",
"@backstage/plugin-auth-node": "backstage:^",
"@backstage/plugin-catalog-backend": "backstage:^",
"@backstage/plugin-catalog-backend-module-logs": "backstage:^",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "backstage:^",
"@backstage/plugin-permission-backend": "backstage:^",
"@backstage/plugin-permission-backend-module-allow-all-policy": "backstage:^",
"@backstage/plugin-permission-common": "backstage:^",
"@backstage/plugin-permission-node": "backstage:^",
"@backstage/plugin-proxy-backend": "backstage:^",
"@backstage/plugin-scaffolder-backend": "backstage:^",
"@backstage/plugin-search-backend": "backstage:^",
"@backstage/plugin-search-backend-module-catalog": "backstage:^",
"@backstage/plugin-search-backend-module-techdocs": "backstage:^",
"@backstage/plugin-search-backend-node": "backstage:^",
"@backstage/plugin-techdocs-backend": "backstage:^",
"@backstage/backend-defaults": "^0.11.1",
"@backstage/config": "^1.3.3",
"@backstage/plugin-app-backend": "^0.5.4",
"@backstage/plugin-auth-backend": "^0.25.2",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.10",
"@backstage/plugin-auth-node": "^0.6.5",
"@backstage/plugin-catalog-backend": "^3.0.0",
"@backstage/plugin-catalog-backend-module-logs": "^0.1.12",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.10",
"@backstage/plugin-permission-backend": "^0.7.2",
"@backstage/plugin-permission-backend-module-allow-all-policy": "^0.2.10",
"@backstage/plugin-permission-common": "^0.9.1",
"@backstage/plugin-permission-node": "^0.10.2",
"@backstage/plugin-proxy-backend": "^0.6.4",
"@backstage/plugin-scaffolder-backend": "^2.1.0",
"@backstage/plugin-search-backend": "^2.0.4",
"@backstage/plugin-search-backend-module-catalog": "^0.3.6",
"@backstage/plugin-search-backend-module-techdocs": "^0.4.4",
"@backstage/plugin-search-backend-node": "^1.3.13",
"@backstage/plugin-techdocs-backend": "^2.0.4",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"dockerode": "^3.3.1",
@ -53,7 +53,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "backstage:^"
"@backstage/cli": "^0.33.1"
},
"files": [
"dist"

View File

@ -1,16 +1,5 @@
# @backstage-community/plugin-azure-devops-backend
## 0.20.0
### Minor Changes
- 8367480: Backstage version bump to v1.42.3
### Patch Changes
- Updated dependencies [8367480]
- @backstage-community/plugin-azure-devops-common@0.14.0
## 0.19.0
### Minor Changes

View File

@ -1,6 +1,6 @@
{
"name": "@backstage-community/plugin-azure-devops-backend",
"version": "0.20.0",
"version": "0.19.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@ -35,17 +35,17 @@
},
"dependencies": {
"@backstage-community/plugin-azure-devops-common": "workspace:^",
"@backstage/backend-defaults": "backstage:^",
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/errors": "backstage:^",
"@backstage/integration": "backstage:^",
"@backstage/plugin-auth-node": "backstage:^",
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-catalog-node": "backstage:^",
"@backstage/plugin-permission-common": "backstage:^",
"@backstage/plugin-permission-node": "backstage:^",
"@backstage/backend-defaults": "^0.11.1",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/catalog-model": "^1.7.5",
"@backstage/config": "^1.3.3",
"@backstage/errors": "^1.2.7",
"@backstage/integration": "^1.17.1",
"@backstage/plugin-auth-node": "^0.6.5",
"@backstage/plugin-catalog-common": "^1.1.5",
"@backstage/plugin-catalog-node": "^1.17.2",
"@backstage/plugin-permission-common": "^0.9.1",
"@backstage/plugin-permission-node": "^0.10.2",
"@types/express": "^4.17.6",
"azure-devops-node-api": "^13.0.0",
"express": "^4.17.1",
@ -54,8 +54,8 @@
"p-limit": "^3.1.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "backstage:^",
"@backstage/cli": "backstage:^",
"@backstage/backend-test-utils": "^1.7.0",
"@backstage/cli": "^0.33.1",
"@types/lodash": "^4.14.151",
"@types/mime-types": "^2.1.0",
"@types/supertest": "^6.0.0",

View File

@ -1,11 +1,5 @@
# @backstage-community/plugin-azure-devops-common
## 0.14.0
### Minor Changes
- 8367480: Backstage version bump to v1.42.3
## 0.13.0
### Minor Changes

View File

@ -1,6 +1,6 @@
{
"name": "@backstage-community/plugin-azure-devops-common",
"version": "0.14.0",
"version": "0.13.0",
"backstage": {
"role": "common-library",
"pluginId": "azure-devops",
@ -41,11 +41,11 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/catalog-model": "backstage:^",
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-permission-common": "backstage:^"
"@backstage/catalog-model": "^1.7.5",
"@backstage/plugin-catalog-common": "^1.1.5",
"@backstage/plugin-permission-common": "^0.9.1"
},
"devDependencies": {
"@backstage/cli": "backstage:^"
"@backstage/cli": "^0.33.1"
}
}

View File

@ -1,16 +1,5 @@
# @backstage-community/plugin-azure-devops
## 0.19.0
### Minor Changes
- 8367480: Backstage version bump to v1.42.3
### Patch Changes
- Updated dependencies [8367480]
- @backstage-community/plugin-azure-devops-common@0.14.0
## 0.18.0
### Minor Changes

View File

@ -412,43 +412,3 @@ async handle(
};
}
```
## New Frontend System (Alpha)
The Azure DevOps plugin currently support the New Frontend System via an `/alpha` export, here's how to use it:
### Use new frontend system
1. Install the frontend plugin:
```bash
# From your Backstage root directory
yarn --cwd packages/app add @backstage-community/plugin-azure-devops
```
2. Enable the plugin in your `packages/app(-next)/src/App.tsx`:
After all other imports:
```tsx
import azureDevOpsPlugin from '@backstage-community/plugin-azure-devops';
```
```tsx
export const app = createApp({
features: [
catalogPlugin,
catalogImportPlugin,
userSettingsPlugin,
azureDevOpsPlugin,
// ...
],
});
```
Alternatively you can simply use feature discover and skip the above step by adding the following yo your `app-config.yaml` file:
```yaml
app:
packages: all
```

View File

@ -1,6 +1,6 @@
{
"name": "@backstage-community/plugin-azure-devops",
"version": "0.19.0",
"version": "0.18.0",
"backstage": {
"role": "frontend-plugin",
"pluginId": "azure-devops",
@ -52,14 +52,14 @@
},
"dependencies": {
"@backstage-community/plugin-azure-devops-common": "workspace:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/core-compat-api": "backstage:^",
"@backstage/core-components": "backstage:^",
"@backstage/core-plugin-api": "backstage:^",
"@backstage/errors": "backstage:^",
"@backstage/frontend-plugin-api": "backstage:^",
"@backstage/plugin-catalog-react": "backstage:^",
"@backstage/plugin-permission-react": "backstage:^",
"@backstage/catalog-model": "^1.7.5",
"@backstage/core-compat-api": "^0.4.4",
"@backstage/core-components": "^0.17.4",
"@backstage/core-plugin-api": "^1.10.9",
"@backstage/errors": "^1.2.7",
"@backstage/frontend-plugin-api": "^0.10.4",
"@backstage/plugin-catalog-react": "^1.19.1",
"@backstage/plugin-permission-react": "^0.4.36",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
@ -68,9 +68,9 @@
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/cli": "backstage:^",
"@backstage/dev-utils": "backstage:^",
"@backstage/test-utils": "backstage:^",
"@backstage/cli": "^0.33.1",
"@backstage/dev-utils": "^1.1.12",
"@backstage/test-utils": "^1.7.10",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",

View File

@ -7,19 +7,17 @@
import { AnyApiFactory } from '@backstage/core-plugin-api';
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { ApiFactory } from '@backstage/core-plugin-api';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { EntityCardType } from '@backstage/plugin-catalog-react/alpha';
import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha';
import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
const _default: OverridableFrontendPlugin<
const _default: FrontendPlugin<
{},
{},
{
@ -28,17 +26,15 @@ const _default: OverridableFrontendPlugin<
name: undefined;
config: {};
configInput: {};
output: ExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>;
output: ConfigurableExtensionDataRef<
AnyApiFactory,
'core.api.factory',
{}
>;
inputs: {};
params: <
TApi,
TImpl extends TApi,
TDeps extends {
[x: string]: unknown;
},
>(
params: ApiFactory<TApi, TImpl, TDeps>,
) => ExtensionBlueprintParams<AnyApiFactory>;
params: {
factory: AnyApiFactory;
};
}>;
'entity-card:azure-devops/readme': ExtensionDefinition<{
kind: 'entity-card';
@ -52,22 +48,22 @@ const _default: OverridableFrontendPlugin<
type?: 'content' | 'summary' | 'info' | undefined;
};
output:
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
EntityCardType,
'catalog.entity-card-type',
{
@ -97,31 +93,35 @@ const _default: OverridableFrontendPlugin<
group?: string | false | undefined;
};
output:
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-content-title',
{}
>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>
| ExtensionDataRef<string, 'catalog.entity-content-title', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-content-group',
{
@ -130,12 +130,10 @@ const _default: OverridableFrontendPlugin<
>;
inputs: {};
params: {
defaultPath?: [Error: "Use the 'path' param instead"] | undefined;
path: string;
defaultTitle?: [Error: "Use the 'title' param instead"] | undefined;
title: string;
defaultGroup?: [Error: "Use the 'group' param instead"] | undefined;
group?:
loader: () => Promise<JSX.Element>;
defaultPath: string;
defaultTitle: string;
defaultGroup?:
| (string & {})
| 'development'
| 'deployment'
@ -144,7 +142,6 @@ const _default: OverridableFrontendPlugin<
| 'operation'
| 'observability'
| undefined;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
filter?: EntityPredicate | ((entity: Entity) => boolean) | undefined;
};
@ -165,31 +162,35 @@ const _default: OverridableFrontendPlugin<
group?: string | false | undefined;
};
output:
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-content-title',
{}
>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>
| ExtensionDataRef<string, 'catalog.entity-content-title', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-content-group',
{
@ -198,12 +199,10 @@ const _default: OverridableFrontendPlugin<
>;
inputs: {};
params: {
defaultPath?: [Error: "Use the 'path' param instead"] | undefined;
path: string;
defaultTitle?: [Error: "Use the 'title' param instead"] | undefined;
title: string;
defaultGroup?: [Error: "Use the 'group' param instead"] | undefined;
group?:
loader: () => Promise<JSX.Element>;
defaultPath: string;
defaultTitle: string;
defaultGroup?:
| (string & {})
| 'development'
| 'deployment'
@ -212,7 +211,6 @@ const _default: OverridableFrontendPlugin<
| 'operation'
| 'observability'
| undefined;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
filter?: EntityPredicate | ((entity: Entity) => boolean) | undefined;
};
@ -233,31 +231,35 @@ const _default: OverridableFrontendPlugin<
group?: string | false | undefined;
};
output:
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-content-title',
{}
>
| ConfigurableExtensionDataRef<
(entity: Entity) => boolean,
'catalog.entity-filter-function',
{
optional: true;
}
>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-filter-expression',
{
optional: true;
}
>
| ExtensionDataRef<string, 'catalog.entity-content-title', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<
string,
'catalog.entity-content-group',
{
@ -266,12 +268,10 @@ const _default: OverridableFrontendPlugin<
>;
inputs: {};
params: {
defaultPath?: [Error: "Use the 'path' param instead"] | undefined;
path: string;
defaultTitle?: [Error: "Use the 'title' param instead"] | undefined;
title: string;
defaultGroup?: [Error: "Use the 'group' param instead"] | undefined;
group?:
loader: () => Promise<JSX.Element>;
defaultPath: string;
defaultTitle: string;
defaultGroup?:
| (string & {})
| 'development'
| 'deployment'
@ -280,7 +280,6 @@ const _default: OverridableFrontendPlugin<
| 'operation'
| 'observability'
| undefined;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
filter?: EntityPredicate | ((entity: Entity) => boolean) | undefined;
};
@ -295,9 +294,9 @@ const _default: OverridableFrontendPlugin<
path?: string | undefined;
};
output:
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ExtensionDataRef<string, 'core.routing.path', {}>
| ExtensionDataRef<
| ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
| ConfigurableExtensionDataRef<string, 'core.routing.path', {}>
| ConfigurableExtensionDataRef<
RouteRef<AnyRouteRefParams>,
'core.routing.ref',
{
@ -306,8 +305,7 @@ const _default: OverridableFrontendPlugin<
>;
inputs: {};
params: {
defaultPath?: [Error: "Use the 'path' param instead"] | undefined;
path: string;
defaultPath: string;
loader: () => Promise<JSX.Element>;
routeRef?: RouteRef<AnyRouteRefParams> | undefined;
};

View File

@ -16,6 +16,7 @@
import {
ApiBlueprint,
createApiFactory,
PageBlueprint,
createFrontendPlugin,
discoveryApiRef,
@ -35,8 +36,8 @@ import { isAzureDevOpsAvailable, isAzurePipelinesAvailable } from '../plugin';
/** @alpha */
export const azureDevOpsApi = ApiBlueprint.make({
params: defineParams =>
defineParams({
params: {
factory: createApiFactory({
api: azureDevOpsApiRef,
deps: {
discoveryApi: discoveryApiRef,
@ -45,12 +46,13 @@ export const azureDevOpsApi = ApiBlueprint.make({
factory: ({ discoveryApi, fetchApi }) =>
new AzureDevOpsClient({ discoveryApi, fetchApi }),
}),
},
});
/** @alpha */
export const azureDevOpsPullRequestPage = PageBlueprint.make({
params: {
path: '/azure-pull-requests',
defaultPath: '/azure-pull-requests',
routeRef: convertLegacyRouteRef(azurePullRequestDashboardRouteRef),
loader: () =>
import('../components/PullRequestsPage').then(m =>
@ -63,8 +65,8 @@ export const azureDevOpsPullRequestPage = PageBlueprint.make({
export const azureDevOpsPipelinesEntityContent = EntityContentBlueprint.make({
name: 'pipelines',
params: {
path: '/pipelines',
title: 'Pipelines',
defaultPath: '/pipelines',
defaultTitle: 'Pipelines',
filter: isAzurePipelinesAvailable,
loader: () =>
import('../components/EntityPageAzurePipelines').then(m =>
@ -77,8 +79,8 @@ export const azureDevOpsPipelinesEntityContent = EntityContentBlueprint.make({
export const azureDevOpsGitTagsEntityContent = EntityContentBlueprint.make({
name: 'git-tags',
params: {
path: '/git-tags',
title: 'Git Tags',
defaultPath: '/git-tags',
defaultTitle: 'Git Tags',
filter: isAzureDevOpsAvailable,
loader: () =>
import('../components/EntityPageAzureGitTags').then(m =>
@ -92,8 +94,8 @@ export const azureDevOpsPullRequestsEntityContent = EntityContentBlueprint.make(
{
name: 'pull-requests',
params: {
path: '/pull-requests',
title: 'Pull Requests',
defaultPath: '/pull-requests',
defaultTitle: 'Pull Requests',
filter: isAzureDevOpsAvailable,
loader: () =>
import('../components/EntityPageAzurePullRequests').then(m =>

View File

@ -1,16 +1,5 @@
# @backstage-community/plugin-catalog-backend-module-azure-devops-annotator-processor
## 0.11.0
### Minor Changes
- 8367480: Backstage version bump to v1.42.3
### Patch Changes
- Updated dependencies [8367480]
- @backstage-community/plugin-azure-devops-common@0.14.0
## 0.10.0
### Minor Changes

View File

@ -1,7 +1,7 @@
{
"name": "@backstage-community/plugin-catalog-backend-module-azure-devops-annotator-processor",
"description": "The azure-devops-annotator-processor backend module for the catalog plugin.",
"version": "0.11.0",
"version": "0.10.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@ -31,17 +31,17 @@
},
"dependencies": {
"@backstage-community/plugin-azure-devops-common": "workspace:^",
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/catalog-model": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/integration": "backstage:^",
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-catalog-node": "backstage:^",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/catalog-model": "^1.7.5",
"@backstage/config": "^1.3.3",
"@backstage/integration": "^1.17.1",
"@backstage/plugin-catalog-common": "^1.1.5",
"@backstage/plugin-catalog-node": "^1.17.2",
"lodash": "^4.17.21"
},
"devDependencies": {
"@backstage/backend-test-utils": "backstage:^",
"@backstage/cli": "backstage:^"
"@backstage/backend-test-utils": "^1.7.0",
"@backstage/cli": "^0.33.1"
},
"files": [
"dist"

View File

@ -1,11 +1,5 @@
# @backstage-community/plugin-scaffolder-backend-module-azure-devops
## 0.13.0
### Minor Changes
- 8367480: Backstage version bump to v1.42.3
## 0.12.0
### Minor Changes

View File

@ -1,7 +1,7 @@
{
"name": "@backstage-community/plugin-scaffolder-backend-module-azure-devops",
"description": "The azure-devops module for @backstage/plugin-scaffolder-backend",
"version": "0.13.0",
"version": "0.12.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@ -39,17 +39,17 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/errors": "backstage:^",
"@backstage/integration": "backstage:^",
"@backstage/plugin-scaffolder-node": "backstage:^",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/config": "^1.3.3",
"@backstage/errors": "^1.2.7",
"@backstage/integration": "^1.17.1",
"@backstage/plugin-scaffolder-node": "^0.10.0",
"azure-devops-node-api": "^14.1.0",
"yaml": "^2.6.0"
},
"devDependencies": {
"@backstage/cli": "backstage:^",
"@backstage/plugin-scaffolder-node-test-utils": "backstage:^"
"@backstage/cli": "^0.33.1",
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.1"
},
"files": [
"dist"

View File

@ -1,11 +1,5 @@
# @backstage-community/plugin-scaffolder-backend-module-dotnet
## 0.6.0
### Minor Changes
- 8367480: Backstage version bump to v1.42.3
## 0.5.0
### Minor Changes

View File

@ -1,5 +1,5 @@
{
"version": "0.6.0",
"version": "0.5.0",
"license": "Apache-2.0",
"name": "@backstage-community/plugin-scaffolder-backend-module-dotnet",
"description": "The azure-devops module for @backstage/plugin-scaffolder-backend",
@ -39,15 +39,15 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/plugin-scaffolder-node": "backstage:^",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/plugin-scaffolder-node": "^0.10.0",
"fs-extra": "^11.3.0",
"yaml": "^2.6.0",
"zod": "^3.24.3"
},
"devDependencies": {
"@backstage/cli": "backstage:^",
"@backstage/plugin-scaffolder-node-test-utils": "backstage:^",
"@backstage/cli": "^0.33.1",
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.1",
"@types/fs-extra": "^11"
},
"files": [

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
---
'@backstage-community/plugin-azure-storage-explorer': minor
'@backstage-community/plugin-azure-storage-explorer-backend': minor
---
Backstage version bump to v1.41.1

View File

@ -1,3 +1,3 @@
{
"version": "1.41.1"
"version": "1.40.2"
}

View File

@ -36,9 +36,9 @@
"directory": "workspaces/azure-storage-explorer"
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/cli": "^0.33.0",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/repo-tools": "^0.15.0",
"@backstage/repo-tools": "^0.14.0",
"@changesets/cli": "^2.27.1",
"knip": "^5.27.4",
"node-gyp": "^9.0.0",

View File

@ -20,29 +20,29 @@
},
"dependencies": {
"@backstage-community/plugin-azure-storage-explorer": "workspace:^",
"@backstage/app-defaults": "^1.6.4",
"@backstage/catalog-model": "^1.7.5",
"@backstage/cli": "^0.33.1",
"@backstage/core-app-api": "^1.18.0",
"@backstage/core-components": "^0.17.4",
"@backstage/core-plugin-api": "^1.10.9",
"@backstage/integration-react": "^1.2.9",
"@backstage/plugin-api-docs": "^0.12.9",
"@backstage/plugin-catalog": "^1.31.1",
"@backstage/plugin-catalog-common": "^1.1.5",
"@backstage/plugin-catalog-graph": "^0.4.21",
"@backstage/plugin-catalog-import": "^0.13.3",
"@backstage/plugin-catalog-react": "^1.19.1",
"@backstage/plugin-org": "^0.6.41",
"@backstage/plugin-permission-react": "^0.4.36",
"@backstage/plugin-scaffolder": "^1.33.0",
"@backstage/plugin-search": "^1.4.28",
"@backstage/plugin-search-react": "^1.9.2",
"@backstage/plugin-techdocs": "^1.13.2",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.26",
"@backstage/plugin-techdocs-react": "^1.3.1",
"@backstage/plugin-user-settings": "^0.8.24",
"@backstage/theme": "^0.6.7",
"@backstage/app-defaults": "^1.6.3",
"@backstage/catalog-model": "^1.7.4",
"@backstage/cli": "^0.33.0",
"@backstage/core-app-api": "^1.17.1",
"@backstage/core-components": "^0.17.3",
"@backstage/core-plugin-api": "^1.10.8",
"@backstage/integration-react": "^1.2.8",
"@backstage/plugin-api-docs": "^0.12.8",
"@backstage/plugin-catalog": "^1.31.0",
"@backstage/plugin-catalog-common": "^1.1.4",
"@backstage/plugin-catalog-graph": "^0.4.20",
"@backstage/plugin-catalog-import": "^0.13.2",
"@backstage/plugin-catalog-react": "^1.19.0",
"@backstage/plugin-org": "^0.6.40",
"@backstage/plugin-permission-react": "^0.4.35",
"@backstage/plugin-scaffolder": "^1.32.0",
"@backstage/plugin-search": "^1.4.27",
"@backstage/plugin-search-react": "^1.9.1",
"@backstage/plugin-techdocs": "^1.13.1",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.25",
"@backstage/plugin-techdocs-react": "^1.3.0",
"@backstage/plugin-user-settings": "^0.8.23",
"@backstage/theme": "^0.6.6",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"react": "^18.0.2",
@ -51,7 +51,7 @@
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@backstage/test-utils": "^1.7.10",
"@backstage/test-utils": "^1.7.9",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",

View File

@ -22,27 +22,27 @@
},
"dependencies": {
"@backstage-community/plugin-azure-storage-explorer-backend": "workspace:^",
"@backstage/backend-defaults": "^0.11.1",
"@backstage/catalog-client": "^1.10.2",
"@backstage/config": "^1.3.3",
"@backstage/plugin-app-backend": "^0.5.4",
"@backstage/plugin-auth-backend": "^0.25.2",
"@backstage/plugin-auth-backend-module-github-provider": "^0.3.5",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.10",
"@backstage/plugin-auth-node": "^0.6.5",
"@backstage/plugin-catalog-backend": "^3.0.0",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.10",
"@backstage/plugin-permission-backend": "^0.7.2",
"@backstage/plugin-permission-backend-module-allow-all-policy": "^0.2.10",
"@backstage/plugin-permission-common": "^0.9.1",
"@backstage/plugin-permission-node": "^0.10.2",
"@backstage/plugin-proxy-backend": "^0.6.4",
"@backstage/plugin-scaffolder-backend": "^2.1.0",
"@backstage/plugin-search-backend": "^2.0.4",
"@backstage/plugin-search-backend-module-catalog": "^0.3.6",
"@backstage/plugin-search-backend-module-techdocs": "^0.4.4",
"@backstage/plugin-search-backend-node": "^1.3.13",
"@backstage/plugin-techdocs-backend": "^2.0.4",
"@backstage/backend-defaults": "^0.11.0",
"@backstage/catalog-client": "^1.10.1",
"@backstage/config": "^1.3.2",
"@backstage/plugin-app-backend": "^0.5.3",
"@backstage/plugin-auth-backend": "^0.25.1",
"@backstage/plugin-auth-backend-module-github-provider": "^0.3.4",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.9",
"@backstage/plugin-auth-node": "^0.6.4",
"@backstage/plugin-catalog-backend": "^2.1.0",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.9",
"@backstage/plugin-permission-backend": "^0.7.1",
"@backstage/plugin-permission-backend-module-allow-all-policy": "^0.2.9",
"@backstage/plugin-permission-common": "^0.9.0",
"@backstage/plugin-permission-node": "^0.10.1",
"@backstage/plugin-proxy-backend": "^0.6.3",
"@backstage/plugin-scaffolder-backend": "^2.0.0",
"@backstage/plugin-search-backend": "^2.0.3",
"@backstage/plugin-search-backend-module-catalog": "^0.3.5",
"@backstage/plugin-search-backend-module-techdocs": "^0.4.3",
"@backstage/plugin-search-backend-node": "^1.3.12",
"@backstage/plugin-techdocs-backend": "^2.0.3",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"dockerode": "^3.3.1",
@ -52,7 +52,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.33.1"
"@backstage/cli": "^0.33.0"
},
"files": [
"dist"

View File

@ -40,19 +40,19 @@
"dependencies": {
"@azure/identity": "4.10.2",
"@azure/storage-blob": "12.27.0",
"@backstage/backend-defaults": "^0.11.1",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/config": "^1.3.3",
"@backstage/backend-defaults": "^0.11.0",
"@backstage/backend-plugin-api": "^1.4.0",
"@backstage/config": "^1.3.2",
"@backstage/errors": "^1.2.7",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.7.0",
"@backstage/cli": "^0.33.1",
"@backstage/plugin-auth-backend": "^0.25.2",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.10",
"@backstage/backend-test-utils": "^1.6.0",
"@backstage/cli": "^0.33.0",
"@backstage/plugin-auth-backend": "^0.25.1",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.9",
"@types/supertest": "^6.0.0",
"supertest": "^7.0.0"
},

View File

@ -39,10 +39,10 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "^0.17.4",
"@backstage/core-plugin-api": "^1.10.9",
"@backstage/core-components": "^0.17.3",
"@backstage/core-plugin-api": "^1.10.8",
"@backstage/errors": "^1.2.7",
"@backstage/theme": "^0.6.7",
"@backstage/theme": "^0.6.6",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
@ -54,10 +54,10 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/core-app-api": "^1.18.0",
"@backstage/dev-utils": "^1.1.12",
"@backstage/test-utils": "^1.7.10",
"@backstage/cli": "^0.33.0",
"@backstage/core-app-api": "^1.17.1",
"@backstage/dev-utils": "^1.1.11",
"@backstage/test-utils": "^1.7.9",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

View File

@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

View File

@ -1,8 +0,0 @@
.git
.yarn/cache
.yarn/install-state.gz
node_modules
packages/*/src
packages/*/node_modules
plugins
*.local.yaml

View File

@ -1 +0,0 @@
playwright.config.ts

View File

@ -1,3 +0,0 @@
module.exports = {
root: true,
};

View File

@ -1,56 +0,0 @@
# macOS
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Coverage directory generated when running tests with coverage
coverage
# Dependencies
node_modules/
# Yarn files
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Node version directives
.nvmrc
# dotenv environment variables file
.env
.env.test
# Build output
dist
dist-types
# Temporary change files created by Vim
*.swp
# MkDocs build output
site
# Local configuration files
*.local.yaml
# Sensitive credentials
*-credentials.yaml
# vscode database functionality support files
*.session.sql
# E2E test reports
e2e-test-report/
# Cache
.cache/

View File

@ -1,6 +0,0 @@
.vscode
coverage
dist
dist-types
knip-report.md
report.api.md

View File

@ -1,13 +0,0 @@
# [Backstage](https://backstage.io)
> [!NOTE]
> See the [plugin README](./plugins/bookmarks/README.md) for more information about the Bookmarks plugin.
This is your newly scaffolded Backstage App, Good Luck!
To start the app, run:
```sh
yarn install
yarn start
```

View File

@ -1,64 +0,0 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
# Uncomment the following host directive to bind to specific interfaces
# host: 127.0.0.1
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
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
# This is for local development only, it is not recommended to use this in production
# The production database configuration is stored in app-config.production.yaml
database:
client: better-sqlite3
connection: ':memory:'
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
auth:
# see https://backstage.io/docs/auth/ to learn about auth providers
providers:
# See https://backstage.io/docs/auth/guest/provider
guest: {}
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [Component, System, API, Resource, Location]
locations:
# Local example data, file locations are relative to the backend process, typically `packages/backend`
- type: file
target: ../../examples/entities.yaml
# Local example organizational data
- type: file
target: ../../examples/org.yaml
rules:
- allow: [User, Group]
- type: file
target: ./plugins/bookmarks/examples/component/catalog-info.yaml
## Uncomment these lines to add more example data
# - type: url
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
## Uncomment these lines to add an example org
# - type: url
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
# rules:
# - allow: [User, Group]
# see https://backstage.io/docs/permissions/getting-started for more on the permission framework
permission:
# setting this to `false` will disable permissions
enabled: false

View File

@ -1,3 +0,0 @@
{
"version": "1.41.0"
}

View File

@ -1,3 +0,0 @@
{
"knip-reports": true
}

View File

@ -1,13 +0,0 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: bookmarks-test-app
description: An example of a Backstage application.
# Example for optional annotations
# annotations:
# github.com/project-slug: backstage/backstage
# backstage.io/techdocs-ref: dir:.
spec:
type: website
owner: john@example.com
lifecycle: experimental

View File

@ -1,66 +0,0 @@
{
"name": "@internal/bookmarks",
"version": "1.0.0",
"private": true,
"engines": {
"node": "20 || 22"
},
"scripts": {
"start": "backstage-cli repo start",
"build:backend": "yarn workspace backend build",
"build:all": "backstage-cli repo build --all",
"build:api-reports": "yarn build:api-reports:only",
"build:api-reports:only": "backstage-repo-tools api-reports -o ae-wrong-input-file-type,ae-undocumented --validate-release-tags",
"build:knip-reports": "backstage-repo-tools knip-reports",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"fix": "backstage-cli repo fix",
"lint": "backstage-cli repo lint --since origin/master",
"lint:all": "backstage-cli repo lint",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"new": "backstage-cli new"
},
"workspaces": {
"packages": [
"packages/*",
"plugins/*"
]
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/repo-tools": "^0.15.0",
"@changesets/cli": "^2.29.5",
"node-gyp": "^11.3.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2"
},
"resolutions": {
"@types/react": "^18",
"@types/react-dom": "^18"
},
"prettier": "@backstage/cli/config/prettier",
"lint-staged": {
"*.{js,jsx,ts,tsx,mjs,cjs}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
},
"jest": {
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
}
}

View File

@ -1,12 +0,0 @@
# The Plugins Folder
> [!NOTE]
> See the [plugin README](./bookmarks/README.md) for more information about the Bookmarks plugin.
This is where your own plugins and their associated modules live, each in a
separate folder of its own.
If you want to create a new plugin here, go to your project root directory, run
the command `yarn new`, and follow the on-screen instructions.
You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)!

View File

@ -1 +0,0 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@ -1,7 +0,0 @@
# @backstage-community/plugin-bookmarks
## 0.2.0
### Minor Changes
- 80b21b2: Initial release

View File

@ -1,58 +0,0 @@
# Bookmarks plugin
The Bookmarks plugin is a simple tool for saving and viewing links to your favorite websites, Google Docs, and other online resources directly within Backstage.
Bookmarks are stored in the `metadata` of a Backstage entity, making it easy to manage and access them within your Backstage Software Catalog.
![A screenshot of the Bookmarks plugin](https://i.imgur.com/guMtiax.png)
## Installation
To install the Bookmarks plugin, follow these steps:
1. Install `@backstage-community/plugin-bookmarks` to your frontend packages
```bash
yarn --cwd packages/app add @backstage-community/plugin-bookmarks
```
2. Add `EntityBookmarksContent` to the `EntityPage` routes:
```diff
// In your packages/app/src/components/EntityPage.tsx
import { EntityBookmarksContent, isBookmarksAvailable } from '@backstage-community/plugin-bookmarks';
// add to defaultEntityPage, etc. to see them in the other entity pages
const serviceEntityPage = (
<EntityLayout>
{/* other routes */}
+ <EntityLayout.Route path="/bookmarks" title="Bookmarks" if={isBookmarksAvailable}>
+ <EntityBookmarksContent />
+ </EntityLayout.Route>
</EntityLayout>
);
```
3. Add bookmarks to your entities by including them in the `metadata` section of the entity YAML file:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
spec: # spec fields here...
metadata:
name: my-component
bookmarks:
'Cool link': https://example.com/cool-link
```
4. Done! Enjoy your bookmarks by visiting the updated entity page in Backstage through your company catalog.
## Usage
Once installed, you can view bookmarks by navigating to the "Bookmarks" tab in the entity page of your Backstage application.
Note that only certain pages can be bookmarked. Due to cross-origin policy, you can only bookmark pages that allow embedding in an iframe. This means that some websites may not be viewable directly within Backstage.
## License
Apache-2.0

View File

@ -1,65 +0,0 @@
/*
* Copyright 2025 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 { Header, Page, TabbedLayout } from '@backstage/core-components';
import { UrlTree } from '../../src/types';
import { StrictMode } from 'react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { EntityBookmarksContent } from '../../src/components/EntityBookmarksContent/EntityBookmarksContent';
import { Entity } from '@backstage/catalog-model';
const testData: UrlTree = {
'Life story':
'https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic',
'My cool gadgets and gizmos': {
'fortune cowsay lolcat': 'https://logonoff.co/projects/fcl/index.html',
'XP tour': 'https://logonoff.co/projects/windowsxptour/index.html',
notepad: 'https://notepad.logonoff.co',
},
'Important documents': {
'Team sync notes':
'https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic',
Manifesto: {
'Agile manifesto':
'https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic',
'Scrum manifesto':
'https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic',
},
'Sprint planning':
'https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic',
},
};
const testEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { bookmarks: testData, name: 'my-service' },
};
export const PluginTestPage = () => (
<Page themeId="tool">
<Header type="component — service" title="Bookmark plugin demo" />
<TabbedLayout>
<TabbedLayout.Route path="/" title="Bookmarks">
<EntityProvider entity={testEntity}>
<StrictMode>
<EntityBookmarksContent />
</StrictMode>
</EntityProvider>
</TabbedLayout.Route>
</TabbedLayout>
</Page>
);

View File

@ -1,16 +0,0 @@
import { createDevApp } from '@backstage/dev-utils';
import { bookmarksPlugin } from '../src/plugin';
import { PluginTestPage } from './PluginTestPage/PluginTestPage';
import { bookmarksTranslations } from '../src';
import { AVAILABLE_LANGUAGES } from '../src/translations/translations';
createDevApp()
.registerPlugin(bookmarksPlugin)
.addTranslationResource(bookmarksTranslations)
.setAvailableLanguages(AVAILABLE_LANGUAGES)
.addPage({
element: <PluginTestPage />,
title: 'Root Page',
path: '/bookmarks',
})
.render();

View File

@ -1,23 +0,0 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-website
bookmarks:
'Life story': https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic
'My cool gadgets and gizmos':
'fortune cowsay lolcat': https://logonoff.co/projects/fcl/index.html
'XP tour': https://logonoff.co/projects/windowsxptour/index.html
'notepad': https://notepad.logonoff.co
'Important documents':
'Team sync notes': https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic
'Manifesto':
'Agile manifesto': https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic
'Scrum manifesto': https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic
'Sprint planning': https://docs.google.com/document/d/1qaLicIa3FZKyup4JXo9ivNgWDmkbX6-XBaQNfKeKjpw/mobilebasic
spec:
type: website
lifecycle: experimental
owner: guests
system: examples
providesApis: [example-grpc-api]

View File

@ -1,2 +0,0 @@
# Knip report

View File

@ -1,74 +0,0 @@
{
"name": "@backstage-community/plugin-bookmarks",
"version": "0.2.0",
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin",
"pluginId": "bookmarks",
"pluginPackages": [
"@backstage-community/plugin-bookmarks"
]
},
"sideEffects": false,
"scripts": {
"start": "backstage-cli package start",
"prepublish": "node replace-style-injection-paths.js",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^1.7.5",
"@backstage/core-components": "^0.17.4",
"@backstage/core-plugin-api": "^1.10.9",
"@backstage/plugin-catalog-react": "^1.19.1",
"@backstage/theme": "^0.6.7",
"@mui/icons-material": "5.18.0",
"@mui/material": "^5.18.0",
"@mui/system": "5.18.0",
"@mui/x-tree-view": "8.10.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/dev-utils": "^1.1.12",
"@backstage/test-utils": "^1.7.10",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "~6.27.0"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/bookmarks/plugins/bookmarks"
},
"keywords": [
"backstage",
"plugin"
],
"bugs": "https://github.com/backstage/community-plugins/issues",
"maintainers": [
"@logonoff"
]
}

View File

@ -1,48 +0,0 @@
## API Report File for "@backstage-community/plugin-bookmarks"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackstagePlugin } from '@backstage/core-plugin-api';
import type { Entity } from '@backstage/catalog-model';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { RouteRef } from '@backstage/core-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
import { TranslationResource } from '@backstage/core-plugin-api/alpha';
// @public
export const AVAILABLE_LANGUAGES: string[];
// @public
export const bookmarksPlugin: BackstagePlugin< {
entityContent: RouteRef<undefined>;
}, {}, {}>;
// @public
export const bookmarksTranslationRef: TranslationRef<"bookmarks", {
readonly "bookmarkViewer.newTab": "Open in new tab";
readonly "bookmarkViewer.navButton.next": "Next";
readonly "bookmarkViewer.navButton.previous": "Previous";
readonly "bookmarkViewer.mobileView.toc": "Table of Contents";
readonly "bookmarkViewerFrame.devModeWarning": "You may have to reload the page for the iframe to load correctly in development mode";
readonly "entityBookmarksContent.invalid.title": "Invalid bookmarks format";
readonly "entityBookmarksContent.invalid.description": "Ensure your bookmarks are structured correctly.";
readonly "entityBookmarksContent.notFound.title": "No bookmarks found";
readonly "entityBookmarksContent.notFound.description": "Add bookmarks to your entity's metadata to see them here.";
}>;
// @public
export const bookmarksTranslations: TranslationResource<"bookmarks">;
// @public
export const EntityBookmarksContent: () => JSX_2.Element;
// @public
export const isBookmarksAvailable: (entity: Entity) => entity is Entity & {
metadata: {
bookmarks?: unknown;
};
};
```

View File

@ -1,187 +0,0 @@
/*
* Copyright 2025 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 { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { BookmarksViewer } from './BookmarksViewer';
import { TEST_IDS } from '../../consts/testids';
import { useIsDesktop } from '../../hooks/useIsDesktop';
import { act } from 'react';
jest.mock('../../hooks/useIsDesktop', () => ({
useIsDesktop: jest.fn(),
}));
const mockUseIsDesktop = useIsDesktop as jest.Mock;
const simpleTree = {
foo: {
bar: 'https://example.com/bar',
baz: 'https://example.com/baz',
},
};
const complexTree = {
foo: {
bar: {
baz: 'https://example.com/baz',
qux: 'https://example.com/qux',
},
quux1: 'https://example.com/quux1',
quux2: 'https://example.com/quux2',
quux3: 'https://example.com/quux3',
},
quuz: {
corge: 'https://example.com/corge',
grault: 'https://example.com/grault',
garply: 'https://example.com/garply',
},
};
const flattenedComplexTree = [
{ label: 'baz', href: 'https://example.com/baz' },
{ label: 'qux', href: 'https://example.com/qux' },
{ label: 'quux1', href: 'https://example.com/quux1' },
{ label: 'quux2', href: 'https://example.com/quux2' },
{ label: 'quux3', href: 'https://example.com/quux3' },
{ label: 'corge', href: 'https://example.com/corge' },
{ label: 'grault', href: 'https://example.com/grault' },
{ label: 'garply', href: 'https://example.com/garply' },
];
describe('BookmarksViewer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders viewer, TOC, and navigation buttons', async () => {
mockUseIsDesktop.mockReturnValue(true);
await renderInTestApp(<BookmarksViewer tree={simpleTree} />);
expect(
screen.getByTestId(TEST_IDS.BookmarkViewerFrame.iframe),
).toBeInTheDocument();
expect(
screen.getByTestId(TEST_IDS.TableOfContents.wrapper),
).toBeInTheDocument();
expect(
screen.queryByTestId(TEST_IDS.NavButton.previous),
).not.toBeInTheDocument();
expect(screen.getByTestId(TEST_IDS.NavButton.next)).toBeInTheDocument();
expect(
screen.getByTestId(TEST_IDS.BookmarksViewer.newTab),
).toBeInTheDocument();
// when clicking the next button, the previous button should be enabled
act(() => {
screen.getByTestId(TEST_IDS.NavButton.next).click();
});
expect(screen.getByTestId(TEST_IDS.NavButton.previous)).toBeEnabled();
expect(
screen.queryByTestId(TEST_IDS.NavButton.next),
).not.toBeInTheDocument();
});
it('renders open in new tab button with correct href', async () => {
await renderInTestApp(<BookmarksViewer tree={simpleTree} />);
const openTabButton = screen.getByTestId(TEST_IDS.BookmarksViewer.newTab);
expect(openTabButton).toHaveAttribute('href', 'https://example.com/bar');
expect(openTabButton).toHaveAttribute('target', '_blank');
});
it('has correct next and previous labels', async () => {
mockUseIsDesktop.mockReturnValue(true);
await renderInTestApp(<BookmarksViewer tree={complexTree} />);
const iframe = screen.queryByTestId(TEST_IDS.BookmarkViewerFrame.iframe);
const tableOfContents = screen.getByTestId(
TEST_IDS.TableOfContents.wrapper,
);
/* forwards */
// i = 0
expect(
screen.queryByTestId(TEST_IDS.NavButton.previous),
).not.toBeInTheDocument();
expect(screen.getByTestId(TEST_IDS.NavButton.next)).toHaveTextContent(
flattenedComplexTree[1].label,
);
expect(tableOfContents).toHaveTextContent(flattenedComplexTree[1].label);
expect(iframe).toHaveAttribute('src', flattenedComplexTree[0].href);
act(() => {
screen.getByTestId(TEST_IDS.NavButton.next).click(); // i ++
});
// i = 1 to i = last - 1
for (let i = 1; i < flattenedComplexTree.length - 1; i++) {
expect(tableOfContents).toHaveTextContent(flattenedComplexTree[i].label);
expect(screen.getByTestId(TEST_IDS.NavButton.previous)).toBeEnabled();
expect(screen.getByTestId(TEST_IDS.NavButton.next)).toHaveTextContent(
flattenedComplexTree[i + 1].label,
);
expect(iframe).toHaveAttribute('src', flattenedComplexTree[i].href);
act(() => {
screen.getByTestId(TEST_IDS.NavButton.next).click(); // i ++
});
}
// i = last
expect(screen.getByTestId(TEST_IDS.NavButton.previous)).toBeEnabled();
expect(
screen.queryByTestId(TEST_IDS.NavButton.next),
).not.toBeInTheDocument();
expect(tableOfContents).toHaveTextContent(
flattenedComplexTree[flattenedComplexTree.length - 1].label,
);
expect(iframe).toHaveAttribute(
'src',
flattenedComplexTree[flattenedComplexTree.length - 1].href,
);
/* backwards */
// i = last
act(() => {
screen.getByTestId(TEST_IDS.NavButton.previous).click(); // i --
});
// i = last - 1 to i = 1
for (let i = flattenedComplexTree.length - 2; i > 0; i--) {
expect(screen.getByTestId(TEST_IDS.NavButton.previous)).toBeEnabled();
expect(screen.getByTestId(TEST_IDS.NavButton.next)).toHaveTextContent(
flattenedComplexTree[i + 1].label,
);
expect(tableOfContents).toHaveTextContent(flattenedComplexTree[i].label);
expect(iframe).toHaveAttribute('src', flattenedComplexTree[i].href);
act(() => {
screen.getByTestId(TEST_IDS.NavButton.previous).click(); // i --
});
}
// i = 0
expect(
screen.queryByTestId(TEST_IDS.NavButton.previous),
).not.toBeInTheDocument();
expect(screen.getByTestId(TEST_IDS.NavButton.next)).toHaveTextContent(
flattenedComplexTree[1].label,
);
expect(tableOfContents).toHaveTextContent(flattenedComplexTree[0].label);
expect(iframe).toHaveAttribute('src', flattenedComplexTree[0].href);
});
});

View File

@ -1,114 +0,0 @@
/*
* Copyright 2025 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 ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import Button from '@mui/material/Button';
import { ReactNode, memo, useMemo, useState } from 'react';
import { UrlTree } from '../../types';
import { TEST_IDS } from '../../consts/testids';
import { FlattenedNode, useFlattenTree } from '../../hooks/useFlattenTree';
import { useIsDesktop } from '../../hooks/useIsDesktop';
import { useTranslation } from '../../hooks/useTranslation';
import { BookmarkDesktopView } from './helpers/BookmarkDesktopView';
import { BookmarkMobileView } from './helpers/BookmarkMobileView';
import { BookmarkViewerFrame } from './helpers/BookmarkViewerFrame';
import { NavButton } from './helpers/NavButton';
import { TableOfContents } from './helpers/TableOfContents';
/** Props for layout components */
export type BookmarkViewerLayoutProps = {
toc: ReactNode;
previousButton: ReactNode;
viewer: ReactNode;
openInNewTab: ReactNode;
nextButton: ReactNode;
};
export const BookmarksViewer = memo(({ tree }: { tree: UrlTree }) => {
const flattenedTree = useFlattenTree(tree);
const [currentNode, setCurrentNode] = useState<FlattenedNode>(
flattenedTree[0],
);
const { t } = useTranslation();
const isDesktop = useIsDesktop();
const View = isDesktop ? BookmarkDesktopView : BookmarkMobileView;
const currentFlattenedIndex = useMemo(() => {
return flattenedTree.findIndex(url => url.key === currentNode.key);
}, [flattenedTree, currentNode]);
const previousButton = useMemo(() => {
const previousUrl = flattenedTree[currentFlattenedIndex - 1];
return previousUrl ? (
<NavButton
direction="previous"
treeKey={previousUrl.key}
onClick={() => {
setCurrentNode(previousUrl);
}}
/>
) : null;
}, [flattenedTree, setCurrentNode, currentFlattenedIndex]);
const nextButton = useMemo(() => {
const nextUrl = flattenedTree[currentFlattenedIndex + 1];
return nextUrl ? (
<NavButton
direction="next"
treeKey={nextUrl.key}
onClick={() => {
setCurrentNode(nextUrl);
}}
/>
) : null;
}, [flattenedTree, setCurrentNode, currentFlattenedIndex]);
const viewer = <BookmarkViewerFrame src={currentNode.value} />;
const toc = (
<TableOfContents
tree={tree}
currentNode={currentNode}
setCurrentNode={setCurrentNode}
/>
);
const openInNewTab = (
<Button
href={currentNode.value}
target="_blank"
rel="noopener"
sx={{ mb: 2 }}
endIcon={<ArrowForwardIcon />}
data-testid={TEST_IDS.BookmarksViewer.newTab}
>
{t('bookmarkViewer.newTab')}
</Button>
);
return (
<View
toc={toc}
previousButton={previousButton}
viewer={viewer}
openInNewTab={openInNewTab}
nextButton={nextButton}
/>
);
});

View File

@ -1,43 +0,0 @@
/*
* Copyright 2025 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 { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { BookmarkDesktopView } from './BookmarkDesktopView';
import { TEST_IDS } from '../../../consts/testids';
describe('BookmarkDesktopView', () => {
it('should render the bookmark desktop view with all the required elements', async () => {
await renderInTestApp(
<BookmarkDesktopView
toc={<div data-testid="toc" />}
openInNewTab={<div data-testid="openInNewTab" />}
viewer={<div data-testid="viewer" />}
nextButton={<div data-testid="nextButton" />}
previousButton={<div data-testid="previousButton" />}
/>,
);
expect(
screen.getByTestId(TEST_IDS.BookmarkDesktopView.wrapper),
).toBeInTheDocument();
expect(screen.getByTestId('toc')).toBeInTheDocument();
expect(screen.getByTestId('openInNewTab')).toBeInTheDocument();
expect(screen.getByTestId('viewer')).toBeInTheDocument();
expect(screen.getByTestId('nextButton')).toBeInTheDocument();
expect(screen.getByTestId('previousButton')).toBeInTheDocument();
});
});

View File

@ -1,49 +0,0 @@
/*
* Copyright 2025 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 Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import type { BookmarkViewerLayoutProps } from '../BookmarksViewer';
import { TEST_IDS } from '../../../consts/testids';
export const BookmarkDesktopView = ({
toc,
previousButton,
viewer,
openInNewTab,
nextButton,
}: BookmarkViewerLayoutProps) => (
<Grid
direction="row"
container
spacing={2}
sx={{ height: '100%' }}
data-testid={TEST_IDS.BookmarkDesktopView.wrapper}
>
<Grid item md={2} sx={{ display: 'flex', flexDirection: 'column' }}>
{toc}
{previousButton}
</Grid>
<Grid item md={8} sx={{ display: 'flex', flexDirection: 'column' }}>
{viewer}
</Grid>
<Grid item md={2} sx={{ display: 'flex', flexDirection: 'column' }}>
{openInNewTab}
<Box sx={{ flexGrow: 1 }} />
{nextButton}
</Grid>
</Grid>
);

View File

@ -1,67 +0,0 @@
/*
* Copyright 2025 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 { renderInTestApp } from '@backstage/test-utils';
import { act, screen, waitFor } from '@testing-library/react';
import { BookmarkMobileView } from './BookmarkMobileView';
import { TEST_IDS } from '../../../consts/testids';
describe('BookmarkMobileView', () => {
it('should render the bookmark mobile view with all the required elements', async () => {
await renderInTestApp(
<BookmarkMobileView
toc={<div data-testid="toc" />}
openInNewTab={<div data-testid="openInNewTab" />}
viewer={<div data-testid="viewer" />}
nextButton={<div data-testid="nextButton" />}
previousButton={<div data-testid="previousButton" />}
/>,
);
expect(
screen.getByTestId(TEST_IDS.BookmarkMobileView.wrapper),
).toBeInTheDocument();
expect(
screen.getByTestId(TEST_IDS.BookmarkMobileView.toggleToc),
).toBeInTheDocument();
expect(screen.getByTestId('openInNewTab')).toBeInTheDocument();
expect(screen.getByTestId('viewer')).toBeInTheDocument();
expect(screen.getByTestId('nextButton')).toBeInTheDocument();
// toc is hidden by default and expanded by clicking the toggle
expect(screen.queryByTestId('toc')).not.toBeInTheDocument();
// we hide the previous button in mobile view
expect(screen.queryByTestId('previousButton')).not.toBeInTheDocument();
// clicking the toc toggle should open the drawer
act(() => {
screen.getByTestId(TEST_IDS.BookmarkMobileView.toggleToc).click();
});
expect(screen.getByTestId('toc')).toBeInTheDocument();
// clicking the backdrop should close the drawer
act(() => {
screen.getByTestId(TEST_IDS.BookmarkMobileView.backdrop).click();
});
await waitFor(() => {
expect(
screen.queryByTestId(TEST_IDS.BookmarkMobileView.backdrop),
).not.toBeInTheDocument();
});
expect(screen.queryByTestId('toc')).not.toBeInTheDocument();
});
});

View File

@ -1,91 +0,0 @@
/*
* Copyright 2025 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 TocIcon from '@mui/icons-material/Toc';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { BookmarkViewerLayoutProps } from '../BookmarksViewer';
import { useState } from 'react';
import { useTranslation } from '../../../hooks/useTranslation';
import { TEST_IDS } from '../../../consts/testids';
export const BookmarkMobileView = ({
toc,
openInNewTab,
nextButton,
viewer,
}: BookmarkViewerLayoutProps) => {
const { t } = useTranslation();
const [tocDrawerOpen, setTocDrawerOpen] = useState(false);
return (
<Box
sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}
data-testid={TEST_IDS.BookmarkMobileView.wrapper}
>
<Drawer
anchor="left"
open={tocDrawerOpen}
onClose={() => setTocDrawerOpen(false)}
ModalProps={{
slotProps: {
backdrop: {
'data-testid': TEST_IDS.BookmarkMobileView.backdrop,
} as React.HTMLAttributes<HTMLDivElement>,
},
}}
>
<Box sx={{ minWidth: 250, padding: 2 }}>{toc}</Box>
</Drawer>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
pb: 2,
}}
>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
<Tooltip title={t('bookmarkViewer.mobileView.toc')}>
<IconButton
onClick={() => setTocDrawerOpen(prev => !prev)}
sx={{ mb: 2 }}
data-testid={TEST_IDS.BookmarkMobileView.toggleToc}
>
<TocIcon />
</IconButton>
</Tooltip>
{openInNewTab}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
{nextButton}
</Box>
</Box>
<Box
sx={{ flexGrow: 1, width: '100%', minHeight: '50vh', height: '100%' }}
>
{viewer}
</Box>
</Box>
);
};

View File

@ -1,64 +0,0 @@
/*
* Copyright 2025 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 { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { BookmarkViewerFrame } from './BookmarkViewerFrame';
import { TEST_IDS } from '../../../consts/testids';
import { useIsIframeLoading } from '../../../hooks/useIsIframeLoading';
jest.mock('../../../hooks/useIsIframeLoading', () => ({
useIsIframeLoading: jest.fn(),
}));
const mockUseIsIframeLoading = useIsIframeLoading as jest.Mock;
describe('BookmarkViewerFrame', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('shows loading spinner when iframe is loading', async () => {
process.env.NODE_ENV = 'production';
mockUseIsIframeLoading.mockReturnValue(true);
await renderInTestApp(<BookmarkViewerFrame src="https://example.com" />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
const iframe = screen.getByTestId(TEST_IDS.BookmarkViewerFrame.iframe);
expect(iframe).toHaveStyle('visibility: hidden');
expect(iframe).toHaveAttribute('src', 'https://example.com');
expect(
screen.queryByText('bookmarkViewerFrame.devModeWarning'),
).not.toBeInTheDocument();
});
it('shows iframe when not loading', async () => {
mockUseIsIframeLoading.mockReturnValue(false);
await renderInTestApp(<BookmarkViewerFrame src="https://example.com" />);
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
const iframe = screen.getByTestId(TEST_IDS.BookmarkViewerFrame.iframe);
expect(iframe).toHaveStyle('visibility: visible');
expect(iframe).toHaveAttribute('src', 'https://example.com');
});
it('shows dev mode warning in development', async () => {
process.env.NODE_ENV = 'development';
mockUseIsIframeLoading.mockReturnValue(true);
await renderInTestApp(<BookmarkViewerFrame src="https://example.com" />);
expect(
screen.getByText('bookmarkViewerFrame.devModeWarning'),
).toBeInTheDocument();
});
});

View File

@ -1,78 +0,0 @@
/*
* Copyright 2025 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 Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { memo, useRef } from 'react';
import { useIsIframeLoading } from '../../../hooks/useIsIframeLoading';
import { TEST_IDS } from '../../../consts/testids';
import Typography from '@mui/material/Typography';
import { useTranslation } from '../../../hooks/useTranslation';
export const BookmarkViewerFrame = memo(({ src }: { src: string }) => {
const { t } = useTranslation();
const iframeRef = useRef<HTMLIFrameElement>(null);
const isIframeLoading = useIsIframeLoading(iframeRef, src);
/**
* In development mode, React's Fast Refresh (live reload) can remount components,
* causing this hook to reset its loading state. This leads the iframe to incorrectly
* appearing as loading.
*/
const isDevEnv = process.env.NODE_ENV === 'development';
return (
<>
{isIframeLoading && (
<Box
sx={{
mt: 5,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<CircularProgress />
{isDevEnv && (
<Typography variant="body2" sx={{ mt: 2 }}>
{t('bookmarkViewerFrame.devModeWarning')}
</Typography>
)}
</Box>
)}
<Box
component="iframe"
data-testid={TEST_IDS.BookmarkViewerFrame.iframe}
ref={iframeRef}
referrerPolicy="no-referrer"
src={src}
// disallow top-navigation, top-navigation-by-user-activation, popups-to-escape-sandbox
sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-presentation allow-modals allow-orientation-lock allow-pointer-lock"
sx={{
flexGrow: 1,
border: 'none',
background: 'white',
visibility: isIframeLoading ? 'hidden' : 'visible',
width: '100%',
height: '100%',
}}
/>
</>
);
});

View File

@ -1,59 +0,0 @@
/*
* Copyright 2025 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 { renderInTestApp } from '@backstage/test-utils';
import { screen, fireEvent } from '@testing-library/react';
import { NavButton } from './NavButton';
import { PATH_SEPARATOR } from '../../../consts/consts';
const TREE_KEY = ['foo', 'bar'].join(PATH_SEPARATOR);
describe('NavButton', () => {
it('renders next button with correct treeKey', async () => {
const handleClick = jest.fn();
await renderInTestApp(
<NavButton direction="next" onClick={handleClick} treeKey={TREE_KEY} />,
);
expect(
screen.getByText('bookmarkViewer.navButton.next'),
).toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument();
});
it('renders previous button with correct treeKey', async () => {
const handleClick = jest.fn();
await renderInTestApp(
<NavButton
direction="previous"
onClick={handleClick}
treeKey={TREE_KEY}
/>,
);
expect(
screen.getByText('bookmarkViewer.navButton.previous'),
).toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = jest.fn();
await renderInTestApp(
<NavButton direction="next" onClick={handleClick} treeKey={TREE_KEY} />,
);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,77 +0,0 @@
/*
* Copyright 2025 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 ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { PATH_SEPARATOR } from '../../../consts/consts';
import { useTranslation } from '../../../hooks/useTranslation';
import { TEST_IDS } from '../../../consts/testids';
/** Button component for navigating between bookmarks */
export const NavButton = ({
direction,
onClick,
treeKey,
}: {
direction: 'next' | 'previous';
onClick: () => void;
treeKey: string;
}) => {
const { t } = useTranslation();
const isNext = direction === 'next';
const buttonText = t(`bookmarkViewer.navButton.${direction}`);
/** last path item is the button label */
const bookmarkName = treeKey.split(PATH_SEPARATOR).pop();
return (
<Button
aria-label={`${buttonText}: ${bookmarkName}`}
color="inherit"
data-testid={TEST_IDS.NavButton[direction]}
onClick={onClick}
startIcon={!isNext ? <ArrowBackIcon /> : undefined}
endIcon={isNext ? <ArrowForwardIcon /> : undefined}
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: isNext ? 'flex-end' : 'flex-start',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
textAlign: isNext ? 'right' : 'left',
}}
>
<Typography
component="small"
sx={{ color: theme => theme.palette.text.secondary }}
variant="body2"
>
{buttonText}
</Typography>
<Typography component="span" variant="body1">
{bookmarkName}
</Typography>
</Box>
</Button>
);
};

View File

@ -1,98 +0,0 @@
/*
* Copyright 2025 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 { renderInTestApp } from '@backstage/test-utils';
import { act, screen, waitFor } from '@testing-library/react';
import { TableOfContents } from './TableOfContents';
import { PATH_SEPARATOR } from '../../../consts/consts';
import type { UrlTree } from '../../../types';
import type { FlattenedNode } from '../../../hooks/useFlattenTree';
const tree: UrlTree = {
Foo: {
Bar: 'https://example.com/bar',
Baz: 'https://example.com/baz',
},
Qux: 'https://example.com/qux',
Goo: {
SubGoo: 'https://example.com/sub-goo',
},
};
const currentNode: FlattenedNode = {
key: ['Foo', 'Bar', 'https://example.com/bar'].join(PATH_SEPARATOR),
value: 'https://example.com/bar',
};
describe('TableOfContents', () => {
it('renders all tree items', async () => {
await renderInTestApp(
<TableOfContents
tree={tree}
currentNode={currentNode}
setCurrentNode={jest.fn()}
/>,
);
expect(screen.getByText('Foo')).toBeInTheDocument();
expect(screen.getByText('Bar')).toBeInTheDocument();
expect(screen.getByText('Baz')).toBeInTheDocument();
expect(screen.getByText('Qux')).toBeInTheDocument();
});
it('expands and collapses tree items', async () => {
await renderInTestApp(
<TableOfContents
tree={tree}
currentNode={currentNode}
setCurrentNode={jest.fn()}
/>,
);
const gooItem = screen.getByText('Goo');
expect(screen.queryByText('SubGoo')).not.toBeInTheDocument();
act(() => {
gooItem.click();
});
expect(screen.getByText('SubGoo')).toBeVisible();
act(() => {
gooItem.click();
});
waitFor(() => {
expect(screen.queryByText('SubGoo')).not.toBeInTheDocument();
});
});
it('calls setCurrentNode when an item is clicked', async () => {
const setCurrentNodeMock = jest.fn();
await renderInTestApp(
<TableOfContents
tree={tree}
currentNode={currentNode}
setCurrentNode={setCurrentNodeMock}
/>,
);
const barItem = screen.getByText('Bar');
act(() => {
barItem.click();
});
expect(setCurrentNodeMock).toHaveBeenCalledWith({
key: ['Foo', 'Bar'].join(PATH_SEPARATOR),
value: 'https://example.com/bar',
});
});
});

View File

@ -1,113 +0,0 @@
/*
* Copyright 2025 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 { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';
import { useEffect, useState } from 'react';
import { UrlTree } from '../../../types';
import { PATH_SEPARATOR } from '../../../consts/consts';
import {
FlattenedNode,
useUnorderedFlattenedTree,
} from '../../../hooks/useFlattenTree';
import { TEST_IDS } from '../../../consts/testids';
/** Recursively render a portion of a UrlTree */
const RecursiveTreeItem = ({
treeKey,
subTree,
path = [],
}: {
treeKey: string;
subTree: UrlTree;
path?: string[];
}) => {
const value = subTree[treeKey];
const currentPath = [...path, treeKey];
const itemId = currentPath.join(PATH_SEPARATOR);
if (typeof value === 'string') {
return (
<TreeItem
itemId={itemId}
label={treeKey}
data-testid={TEST_IDS.TableOfContents.leaf}
/>
);
}
// if the value is not a string this must be a subtree
return (
<TreeItem itemId={itemId} label={treeKey}>
{Object.keys(value).map(subKey => (
<RecursiveTreeItem
key={subKey}
treeKey={subKey}
subTree={value}
path={currentPath}
/>
))}
</TreeItem>
);
};
/** Component to render a table of contents from a UrlTree */
export const TableOfContents = ({
tree,
currentNode: { key: pathKey },
setCurrentNode,
}: {
tree: UrlTree;
currentNode: FlattenedNode;
setCurrentNode: (url: FlattenedNode) => void;
}) => {
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const urlLookup = useUnorderedFlattenedTree(tree);
// auto expand items when the current path changes
useEffect(() => {
const parts = pathKey.split(PATH_SEPARATOR);
const parents = parts.map((_, i) =>
parts.slice(0, i + 1).join(PATH_SEPARATOR),
);
setExpandedItems(prev => Array.from(new Set([...prev, ...parents])));
}, [pathKey]);
return (
<SimpleTreeView
selectedItems={pathKey}
onSelectedItemsChange={(_, itemId: string | null) => {
if (!itemId || !urlLookup[itemId]) return;
setCurrentNode({ value: urlLookup[itemId], key: itemId });
}}
expandedItems={expandedItems}
onItemExpansionToggle={(_, itemId: string) => {
setExpandedItems(prev =>
prev.includes(itemId)
? prev.filter(id => id !== itemId)
: [...prev, itemId],
);
}}
sx={{ flexGrow: 1 }}
data-testid={TEST_IDS.TableOfContents.wrapper}
>
{Object.keys(tree).map(key => (
<RecursiveTreeItem key={key} treeKey={key} subTree={tree} />
))}
</SimpleTreeView>
);
};

View File

@ -1,77 +0,0 @@
/*
* Copyright 2025 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 { useEntity } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { EntityBookmarksContent } from './EntityBookmarksContent';
jest.mock('@backstage/plugin-catalog-react', () => ({
useEntity: jest.fn(),
}));
const validBookmarks = { foo: { bar: 'https://example.com' } };
const useEntityMock = useEntity as jest.Mock;
describe('EntityBookmarksContent', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('shows empty state when no bookmarks', async () => {
useEntityMock.mockReturnValue({ entity: { metadata: {} } });
await renderInTestApp(<EntityBookmarksContent />);
expect(
screen.getByText('entityBookmarksContent.notFound.title'),
).toBeInTheDocument();
expect(
screen.getByText('entityBookmarksContent.notFound.description'),
).toBeInTheDocument();
});
it('shows invalid format state when bookmarks are invalid', async () => {
useEntityMock.mockReturnValue({
entity: { metadata: { bookmarks: { foo: 123 } } },
});
await renderInTestApp(<EntityBookmarksContent />);
expect(
screen.getByText('entityBookmarksContent.invalid.title'),
).toBeInTheDocument();
expect(
screen.getByText('entityBookmarksContent.invalid.description'),
).toBeInTheDocument();
expect(
screen.queryByText('entityBookmarksContent.notFound.title'),
).not.toBeInTheDocument();
expect(
screen.queryByText('entityBookmarksContent.notFound.description'),
).not.toBeInTheDocument();
});
it('renders BookmarksViewer when bookmarks are valid', async () => {
useEntityMock.mockReturnValue({
entity: { metadata: { bookmarks: validBookmarks } },
});
await renderInTestApp(<EntityBookmarksContent />);
expect(
screen.queryByText('entityBookmarksContent.invalid.title'),
).not.toBeInTheDocument();
expect(
screen.queryByText('entityBookmarksContent.notFound.title'),
).not.toBeInTheDocument();
});
});

View File

@ -1,48 +0,0 @@
/*
* Copyright 2025 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 { BookmarksViewer } from '../BookmarksViewer/BookmarksViewer';
import { EmptyState } from '@backstage/core-components';
import { useTranslation } from '../../hooks/useTranslation';
import { USE_TREE_ERROR, useTree } from '../../hooks/useTree';
export const EntityBookmarksContent = () => {
const { tree, error } = useTree();
const { t } = useTranslation();
switch (error) {
case USE_TREE_ERROR.INVALID:
return (
<EmptyState
title={t('entityBookmarksContent.invalid.title')}
description={t('entityBookmarksContent.invalid.description')}
missing="data"
/>
);
case USE_TREE_ERROR.NOT_FOUND:
return (
<EmptyState
title={t('entityBookmarksContent.notFound.title')}
description={t('entityBookmarksContent.notFound.description')}
missing="data"
/>
);
default:
return <BookmarksViewer tree={tree} />;
}
};

View File

@ -1,21 +0,0 @@
/*
* Copyright 2025 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.
*/
/**
* Magic value used as a separator for path keys, that is not
* valid in any URL and is unlikely to appear in folder names
*/
export const PATH_SEPARATOR = ':::PLUGIN-BOOKMARKS-SEPARATOR:::';

View File

@ -1,41 +0,0 @@
/*
* Copyright 2025 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.
*/
/** Test selector IDs */
export const TEST_IDS = {
BookmarkDesktopView: {
wrapper: 'plugin-bookmarks-view-desktop',
},
BookmarkMobileView: {
backdrop: 'plugin-bookmarks-view-mobile-backdrop',
toggleToc: 'plugin-bookmarks-view-mobile-toggle-toc',
wrapper: 'plugin-bookmarks-view-mobile',
},
BookmarksViewer: {
newTab: 'plugin-bookmarks-new-tab',
},
BookmarkViewerFrame: {
iframe: 'plugin-bookmarks-iframe',
},
TableOfContents: {
leaf: 'plugin-bookmarks-toc-leaf',
wrapper: 'plugin-bookmarks-toc-wrapper',
},
NavButton: {
next: 'plugin-bookmarks-nav-next',
previous: 'plugin-bookmarks-nav-previous',
},
};

View File

@ -1,121 +0,0 @@
/*
* Copyright 2025 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 { renderHook } from '@testing-library/react';
import { useFlattenTree, useUnorderedFlattenedTree } from './useFlattenTree';
import { PATH_SEPARATOR } from '../consts/consts';
describe('useFlattenTree', () => {
it('should export hook', () => {
expect(useFlattenTree).toBeDefined();
});
it('should flatten a simple UrlTree', () => {
const tree = {
docs: 'https://docs.example.com',
blog: 'https://blog.example.com',
};
const { result } = renderHook(() => useFlattenTree(tree));
expect(result.current).toEqual([
{
key: ['docs'].join(PATH_SEPARATOR),
value: 'https://docs.example.com',
},
{
key: ['blog'].join(PATH_SEPARATOR),
value: 'https://blog.example.com',
},
]);
});
it('should flatten a nested UrlTree', () => {
const tree = {
docs: {
gettingStarted: 'https://docs.example.com/getting-started',
api: 'https://docs.example.com/api',
},
blog: 'https://blog.example.com',
};
const { result } = renderHook(() => useFlattenTree(tree));
expect(result.current).toEqual([
{
key: ['docs', 'gettingStarted'].join(PATH_SEPARATOR),
value: 'https://docs.example.com/getting-started',
},
{
key: ['docs', 'api'].join(PATH_SEPARATOR),
value: 'https://docs.example.com/api',
},
{
key: ['blog'].join(PATH_SEPARATOR),
value: 'https://blog.example.com',
},
]);
});
it('should return empty array for empty tree', () => {
const tree = {};
const { result } = renderHook(() => useFlattenTree(tree));
expect(result.current).toEqual([]);
});
it('should handle deeply nested UrlTree', () => {
const tree = {
a: {
b: {
c: 'url1',
},
d: 'url2',
},
e: 'url3',
};
const { result } = renderHook(() => useFlattenTree(tree));
expect(result.current).toEqual([
{ key: ['a', 'b', 'c'].join(PATH_SEPARATOR), value: 'url1' },
{ key: ['a', 'd'].join(PATH_SEPARATOR), value: 'url2' },
{ key: ['e'].join(PATH_SEPARATOR), value: 'url3' },
]);
});
});
describe('useUnorderedFlattenedTree', () => {
it('should convert flattened tree to key-value map', () => {
const tree = {
docs: {
gettingStarted: 'https://docs.example.com/getting-started',
api: 'https://docs.example.com/api',
},
blog: 'https://blog.example.com',
};
const { result } = renderHook(() => useUnorderedFlattenedTree(tree));
const gettingStartedKey = ['docs', 'gettingStarted'].join(PATH_SEPARATOR);
const apiKey = ['docs', 'api'].join(PATH_SEPARATOR);
const blogKey = ['blog'].join(PATH_SEPARATOR);
expect(result.current).toEqual({
[gettingStartedKey]: 'https://docs.example.com/getting-started',
[apiKey]: 'https://docs.example.com/api',
[blogKey]: 'https://blog.example.com',
});
});
it('should return empty object for empty tree', () => {
const tree = {};
const { result } = renderHook(() => useUnorderedFlattenedTree(tree));
expect(result.current).toEqual({});
});
});

View File

@ -1,52 +0,0 @@
/*
* Copyright 2025 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 { useMemo } from 'react';
import { UrlTree } from '../types';
import { PATH_SEPARATOR } from '../consts/consts';
/** Flattened node type with key and value */
export type FlattenedNode = { key: string; value: string };
/** Flattened representation of a UrlTree */
export type FlattenedTree = FlattenedNode[];
/** Flatten the UrlTree to an array of URLs, with key as full path */
export const useFlattenTree = (tree: UrlTree): FlattenedTree =>
useMemo(() => {
const acc: FlattenedTree = [];
const inOrderTraverse = (node: UrlTree, path: string[] = []) =>
Object.entries(node).forEach(([key, value]) =>
typeof value === 'string'
? acc.push({ key: [...path, key].join(PATH_SEPARATOR), value })
: inOrderTraverse(value, [...path, key]),
);
inOrderTraverse(tree);
return acc;
}, [tree]);
export type UnorderedFlattenedTree = { [key: string]: string };
/** Convert a UrlTree to a key-value map of flattened URLs for easy lookup */
export const useUnorderedFlattenedTree = (
tree: UrlTree,
): UnorderedFlattenedTree => {
const flatTree = useFlattenTree(tree);
return useMemo(
() => Object.fromEntries(flatTree.map(({ key, value }) => [key, value])),
[flatTree],
);
};

View File

@ -1,37 +0,0 @@
/*
* Copyright 2025 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 { renderHook } from '@testing-library/react';
import { useIsDesktop } from './useIsDesktop';
import { mockBreakpoint } from '@backstage/core-components/testUtils';
describe('useIsDesktop', () => {
it('should export hook', () => {
expect(useIsDesktop).toBeDefined();
});
it('should return true for desktop viewports', () => {
mockBreakpoint({ matches: true });
const { result } = renderHook(() => useIsDesktop());
expect(result.current).toBe(true);
});
it('should return false for mobile viewports', () => {
mockBreakpoint({ matches: false });
const { result } = renderHook(() => useIsDesktop());
expect(result.current).toBe(false);
});
});

View File

@ -1,24 +0,0 @@
/*
* Copyright 2025 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 { useTheme } from '@mui/material/styles';
import { useMediaQuery } from '@mui/system';
/** A hook that determines if the current viewport is considered desktop-sized. */
export const useIsDesktop = (): boolean => {
const theme = useTheme();
return useMediaQuery(theme.breakpoints.up('md'));
};

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