Compare commits

...

55 Commits

Author SHA1 Message Date
Backstage Service Account 6a65a4cea3
Version Packages (#5041)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-25 08:27:10 -05:00
Backstage Service Account c2261cf6ab
Version Packages (#5042)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-25 08:26:56 -05:00
Backstage Service Account 727c25df51
Version Packages (#5047)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-25 08:26:41 -05:00
Andre Wanlin ff785e582c
Skip version bump when release version matches workspace version (#5043)
Signed-off-by: Andre Wanlin <awanlin@spotify.com>
2025-08-25 08:15:41 -05:00
Backstage Service Account dcffece576
sonarqube - version:bump to v1.42.3 (#5032)
* v1.42.3 version bump

* sonarqube: align to latest nfs specs

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>

---------

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
2025-08-25 15:14:38 +02:00
Andre Wanlin ceeadd1e1a
cost-insights - New Frontend System Fixes (#4839)
* cost-insights - New Frontend System Fixes

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

* Updated API report

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

---------

Signed-off-by: Andre Wanlin <awanlin@spotify.com>
2025-08-25 08:14:14 -05:00
Andre Wanlin 7bafbd4fc8
various workspaces - Added auto bump file (#5044)
Signed-off-by: Andre Wanlin <awanlin@spotify.com>
2025-08-25 07:29:45 -05:00
Backstage Service Account 0493206694
github-actions - version:bump to v1.42.3 (#5021)
* v1.42.3 version bump

* github-actions: nfs fixes and switch to yarn plugin

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>

* github-actions: prettier tweaks

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>

* github-actions: update api reports for nfs

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>

---------

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
2025-08-25 10:47:10 +02:00
Backstage Service Account ca3c813046
linguist - version:bump to v1.42.3 (#5039)
* v1.42.3 version bump

* Use yarn plugin

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

* Manual changes for release

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

* Maintenance

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

---------

Signed-off-by: Andre Wanlin <awanlin@spotify.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andre Wanlin <awanlin@spotify.com>
2025-08-24 13:42:42 -05:00
Backstage Service Account 8367480d11
azure-devops - version:bump to v1.42.3 (#5040)
* v1.42.3 version bump

* Use yarn plugin

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

* Manual changes for release

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

* Maintenance

Signed-off-by: Andre Wanlin <awanlin@spotify.com>

---------

Signed-off-by: Andre Wanlin <awanlin@spotify.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andre Wanlin <awanlin@spotify.com>
2025-08-24 13:42:27 -05:00
Backstage Service Account 6e6b88413f
Version Packages (#4840)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-22 16:05:08 -05:00
Backstage Service Account 4815dfd531
v1.41.1 version bump (#4973)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-22 15:51:41 -05:00
Juan Pablo Garcia Ripa c2b33a16aa
github-pull-request-board decouple board from entity page (#4710)
* Decouple entities from the board logic for reuse the board on other places

Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>

* mock useEntity

Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>

* revert the change
Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>

Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>

---------

Signed-off-by: Juan Pablo Garcia Ripa <sarabadu@gmail.com>
2025-08-22 15:16:31 -05:00
James Andrew Vaughn 56382c80de
Remove `rollbar-backend` legacy backend (#4778)
* fix(rollbar-backend): use correct plugin id

The rollbar backend plugin did not have the id expected by
the frontend plugin and all rollbar to backend requests
failed.

Signed-off-by: James Andrew Vaughn <jamesvaughn@modethirteen.com>

* refactor(rollbar-backend)!: remove legacy backend support

BREAKING CHANGE: This removes the deprecated createRouter
interface. Going forward the rollbar backend plugin must be
registered using createBackendPlugin

Signed-off-by: James Andrew Vaughn <jamesvaughn@modethirteen.com>

---------

Signed-off-by: James Andrew Vaughn <jamesvaughn@modethirteen.com>
2025-08-22 14:39:55 -05:00
Backstage Service Account 7043b8bbb6
Version Packages (#5006)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-22 15:54:03 +00:00
Fredrik Adelöw 8460759070
explore: remove the alpha export of the collator (#5034)
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
2025-08-22 09:45:36 -05:00
Fredrik Adelöw 02546adb32
explore: use the yarn plugin (#5033)
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
2025-08-22 09:29:45 -05:00
Fredrik Adelöw c4f95b6e1e
move over `@backstage/plugin-search-backend-module-explore` (#4993)
* move over @backstage/plugin-search-backend-module-explore

Signed-off-by: Fredrik Adelöw <freben@gmail.com>

* add note in the README about where it was moved from

Signed-off-by: Fredrik Adelöw <freben@gmail.com>

---------

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
2025-08-22 07:31:31 -05:00
Backstage Service Account fe6d855d73
tech-radar - version:bump to v1.42.3 (#5022)
* v1.42.3 version bump

* tech-radar: align to the latest nfs specs

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>

* tech-radar: api-reports

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>

---------

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
2025-08-22 13:27:29 +02:00
Camila Belo e46f2a55c2
refactor: simplify home redirect (#5007)
Signed-off-by: Camila Belo <camilaibs@gmail.com>
2025-08-22 12:59:50 +02:00
github-actions[bot] 46e1cefbac
chore: update issue templates with latest workspaces (#5028)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-22 12:30:41 +02:00
Backstage Service Account deb952366a
v1.42.3 version bump (#5029)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-22 11:50:16 +02:00
Backstage Service Account 593523a511
Version Packages (#5027) 2025-08-22 00:50:37 +02:00
Debsmita Santra 4d645136fd
(npm): export translation files (#5020)
Signed-off-by: Debsmita Santra <debsmita.santra@gmail.com>
2025-08-21 22:25:30 +00:00
dependabot[bot] 496b5ea76f
build(deps): bump cipher-base in /workspaces/topology (#5024)
Bumps [cipher-base](https://github.com/crypto-browserify/cipher-base) from 1.0.4 to 1.0.6.
- [Changelog](https://github.com/browserify/cipher-base/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/cipher-base/compare/v1.0.4...v1.0.6)

---
updated-dependencies:
- dependency-name: cipher-base
  dependency-version: 1.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-21 15:51:13 -04:00
Backstage Service Account 2e3afacf8b
Version Packages (#5026)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 15:34:45 -04:00
logonoff 80b21b20f0
feat: add bookmarks plugin (#4931)
Signed-off-by: logonoff <git@logonoff.co>
2025-08-21 15:27:47 -04:00
Backstage Service Account 3cd6bac697
Version Packages (#5023)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 15:11:26 -04:00
Backstage Service Account 707c5f303c
v1.41.1 version bump (#4971)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-21 14:40:13 -04:00
Backstage Service Account 7df058feae
Version Packages (#5015)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 13:53:11 -04:00
Backstage Service Account 00731dafe6
Version Packages (#5016)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 13:50:18 -04:00
Backstage Service Account 51814e6f58
Version Packages (#4996)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 10:47:17 +01:00
Backstage Service Account 91f611bccb
Version Packages (#4982)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 15:04:18 +05:30
Backstage Service Account 74876f0d13
Version Packages (#4880)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 10:37:01 +02:00
Backstage Service Account fb6cbc705d
Version Packages (#5017)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-21 10:20:06 +02:00
Debsmita Santra 56b4264d5f
use useKubernetesObjects hook from k8s-react package (#4851)
Signed-off-by: Debsmita Santra <debsmita.santra@gmail.com>
2025-08-21 01:24:37 -04:00
Sri Aradhyula b0e72b5547
chore(CODEOWNERS): add maintainers for agent-forge plugin (#5011)
We are the maintainer of the agent-forge plugin

Stefan Braicu - @sbraicu 
Sri Aradhyula - @sriaradhyula
Shubham Bakshi - @subbaksh
Sunny Whang - @suwhang-cisco

Signed-off-by: Sri Aradhyula <sraradhy@cisco.com>
2025-08-20 12:15:41 -05:00
dependabot[bot] 2c0ec89454
build(deps): bump @backstage/plugin-scaffolder-backend from 2.0.0 to 2.1.1 in /workspaces/acs (#4979)
* build(deps): bump @backstage/plugin-scaffolder-backend

Bumps [@backstage/plugin-scaffolder-backend](https://github.com/backstage/backstage/tree/HEAD/plugins/scaffolder-backend) from 2.0.0 to 2.1.1.
- [Release notes](https://github.com/backstage/backstage/releases)
- [Changelog](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/CHANGELOG.md)
- [Commits](https://github.com/backstage/backstage/commits/HEAD/plugins/scaffolder-backend)

---
updated-dependencies:
- dependency-name: "@backstage/plugin-scaffolder-backend"
  dependency-version: 2.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): deduplicate yarn lockfile dependencies

Signed-off-by: Saif Chaudhry <schaudhr@redhat.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Saif Chaudhry <schaudhr@redhat.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saif Chaudhry <schaudhr@redhat.com>
2025-08-20 09:11:58 -07:00
Christoph Jerolimov f5fff6fc37
chore(npm): add another codeowner (#5008)
Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com>
2025-08-20 10:44:43 -04:00
Christoph Jerolimov bff3611105
chore(npm): enable knip-report verification and cleanup dependencies (#4991)
Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com>
2025-08-20 17:28:57 +05:30
Camila Belo 13c0def6ea
[NFS] Complete Explore Plugin Migration (#5005)
* feat: complete migrating explore plugin to NFS

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* Update workspaces/explore/.changeset/afraid-baboons-develop.md

Co-authored-by: Ben Lambert <ben@blam.sh>
Signed-off-by: Camila Belo <camilaibs@gmail.com>

---------

Signed-off-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Ben Lambert <ben@blam.sh>
2025-08-20 09:52:40 +02:00
Backstage Service Account fa109420d0
Version Packages (#5002)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-19 16:21:49 -04:00
Backstage Service Account d1b75248ed
Version Packages (#5001)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-19 16:21:31 -04:00
backstage-goalie[bot] 45236345e3
chore(deps): update dependency @testing-library/jest-dom to v6.7.0 (#4988)
Signed-off-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: backstage-goalie[bot] <97962292+backstage-goalie[bot]@users.noreply.github.com>
2025-08-19 22:03:29 +02:00
Christoph Jerolimov 2c67eaaa4a
chore: add .auto-version-bump to npm, ocm and rbac workspaces (#4990)
Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com>
2025-08-19 13:32:47 +02:00
Backstage Service Account 4b2153a833
scaffolder-backend-module-annotator - version:bump to v1.41.1 (#4744)
* v1.41.1 version bump

* fix(scaffolder-backend-module-annotator): fix backstage bump (#4914)

* fix: createAnnotatorAction method for the scaffolder-backend-module-annotator

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

* docs: update the report.api.md

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

* Update record zod schema

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

* fix(scaffolder-backend-module-annotator): warnings in the `report.api.md`

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

* fix(scaffolder-backend-module-annotator): schema for the `createAnnotatorAction` method

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

---------

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

---------

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ihor Mykhno <imykhno@redhat.com>
2025-08-19 12:44:40 +05:30
backstage-goalie[bot] e172f18500
chore(deps): update dependency @openapitools/openapi-generator-cli to v2.22.0 (#4986)
Signed-off-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: backstage-goalie[bot] <97962292+backstage-goalie[bot]@users.noreply.github.com>
2025-08-18 15:25:09 +00:00
Oleksandr Andriienko 10978474b7
fix(servicenow): remove freezed backend deps in the frontend (#4857)
* fix(servicenow): remove freezed backend deps in the frontend

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>

* fix(servicenow): remove more freezed deps

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>

---------

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2025-08-18 17:22:39 +03:00
backstage-goalie[bot] 63407d09df
chore(deps): update dependency fs-extra to v11.3.1 (#4894)
Signed-off-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: backstage-goalie[bot] <97962292+backstage-goalie[bot]@users.noreply.github.com>
2025-08-18 14:57:55 +01:00
mbruhin 43064bf46c
Update default non-dense behavior (#4877)
* Update default non-dense behavior

Signed-off-by: Matthew Bruhin <mbruhin@athenahealth.com>

* add API report

Signed-off-by: Matthew Bruhin <mbruhin@athenahealth.com>

---------

Signed-off-by: Matthew Bruhin <mbruhin@athenahealth.com>
Co-authored-by: Matthew Bruhin <mbruhin@athenahealth.com>
2025-08-18 15:05:51 +02:00
Backstage Service Account f9a8eec66d
scaffolder-backend-module-kubernetes - version:bump to v1.41.1 (#4738)
* v1.41.1 version bump

* fix(scaffolder-backend-module-kubernetes): using zod for `createKubernetesNamespaceAction` method (#4927)

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>

---------

Signed-off-by: Ihor Mykhno <imykhno@redhat.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ihor Mykhno <imykhno@redhat.com>
2025-08-18 17:05:35 +05:30
Backstage Service Account 1d51b5654e
Version Packages (#4978)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-15 16:17:39 +02:00
Gustaf Räntilä 7593e172ce
Fixed erroneous repository.url in some manage plugin packages (#4977)
* Fixed erroneous repository.url in some manage plugin packages

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>

* Also fix repository.directory

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>

---------

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
2025-08-15 16:05:55 +02:00
Backstage Service Account 9a143944f3
Version Packages (#4889)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-15 14:20:07 +01:00
Backstage Service Account a04f9dbbfe
Version Packages (#4881)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-08-15 09:10:37 +02:00
407 changed files with 42816 additions and 25555 deletions

4
.github/CODEOWNERS vendored
View File

@ -13,6 +13,7 @@ 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
@ -31,6 +32,7 @@ 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
@ -79,7 +81,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
/workspaces/npm @backstage/community-plugins-maintainers @christoph-jerolimov @ciiay @karthikjeeyar @logonoff
/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,6 +33,7 @@ body:
- bazaar
- bitrise
- blackduck
- bookmarks
- cicd-statistics
- cloudbuild
- code-climate

View File

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

View File

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

3
.github/labeler.yml vendored
View File

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

View File

@ -78,6 +78,13 @@ 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"
@ -97,7 +104,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.0",
"fs-extra": "11.3.1",
"husky": "^9.0.11",
"js-yaml": "^4.1.0",
"lint-staged": "^15.2.2",

View File

@ -0,0 +1,5 @@
---
'@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.6.4",
"@testing-library/jest-dom": "6.7.0",
"@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.6.4"
"@testing-library/jest-dom": "npm:6.7.0"
"@testing-library/react": "npm:14.3.1"
"@testing-library/react-hooks": "npm:8.0.1"
"@testing-library/user-event": "npm:14.6.1"
@ -11690,18 +11690,17 @@ __metadata:
languageName: node
linkType: hard
"@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"
"@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"
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/5e67112c789f884fb75b279c2cddfdd0995a012a7847a03c474e4134f0d213934ee70c97433bca26b45e3a5ffa56faafe6499c8e57841179c4f2bd80eef429cd
checksum: 10/c994f028b6f2d49c18c9fd6050af7f3316fb0afd03d0ba15d03b177f0f046a0308302dd52ab289fad8794e16a88e4d724b5f23caa007cf343a4b5e435efb84d9
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.0.0",
"@backstage/plugin-scaffolder-backend": "^2.1.1",
"@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.0.0",
"@backstage/plugin-scaffolder-backend": "^2.1.1",
"@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

View File

@ -0,0 +1,6 @@
---
'@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.6.4",
"@testing-library/jest-dom": "6.7.0",
"@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.6.4",
"@testing-library/jest-dom": "6.7.0",
"@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.6.4"
"@testing-library/jest-dom": "npm:6.7.0"
"@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.6.4"
"@testing-library/jest-dom": "npm:6.7.0"
"@testing-library/react": "npm:14.3.1"
"@testing-library/user-event": "npm:14.6.1"
"@types/node": "npm:22.15.29"
@ -6491,18 +6491,17 @@ __metadata:
languageName: node
linkType: hard
"@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"
"@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"
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/5e67112c789f884fb75b279c2cddfdd0995a012a7847a03c474e4134f0d213934ee70c97433bca26b45e3a5ffa56faafe6499c8e57841179c4f2bd80eef429cd
checksum: 10/c994f028b6f2d49c18c9fd6050af7f3316fb0afd03d0ba15d03b177f0f046a0308302dd52ab289fad8794e16a88e4d724b5f23caa007cf343a4b5e435efb84d9
languageName: node
linkType: hard

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
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,6 +1,7 @@
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.41.1"
"version": "1.42.3"
}

View File

@ -36,9 +36,9 @@
"directory": "workspaces/azure-devops"
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/repo-tools": "^0.15.0",
"@backstage/cli": "backstage:^",
"@backstage/e2e-test-utils": "backstage:^",
"@backstage/repo-tools": "backstage:^",
"@changesets/cli": "^2.27.1",
"knip": "^5.27.4",
"node-gyp": "^10.0.0",
@ -58,9 +58,5 @@
"*.{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": "^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": "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:^",
"@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": "^1.7.10",
"@backstage/test-utils": "backstage:^",
"@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": "^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",
"@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:^",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"dockerode": "^3.3.1",
@ -53,7 +53,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.33.1"
"@backstage/cli": "backstage:^"
},
"files": [
"dist"

View File

@ -1,5 +1,16 @@
# @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.19.0",
"version": "0.20.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": "^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",
"@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:^",
"@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": "^1.7.0",
"@backstage/cli": "^0.33.1",
"@backstage/backend-test-utils": "backstage:^",
"@backstage/cli": "backstage:^",
"@types/lodash": "^4.14.151",
"@types/mime-types": "^2.1.0",
"@types/supertest": "^6.0.0",

View File

@ -1,5 +1,11 @@
# @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.13.0",
"version": "0.14.0",
"backstage": {
"role": "common-library",
"pluginId": "azure-devops",
@ -41,11 +41,11 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/catalog-model": "^1.7.5",
"@backstage/plugin-catalog-common": "^1.1.5",
"@backstage/plugin-permission-common": "^0.9.1"
"@backstage/catalog-model": "backstage:^",
"@backstage/plugin-catalog-common": "backstage:^",
"@backstage/plugin-permission-common": "backstage:^"
},
"devDependencies": {
"@backstage/cli": "^0.33.1"
"@backstage/cli": "backstage:^"
}
}

View File

@ -1,5 +1,16 @@
# @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,3 +412,43 @@ 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.18.0",
"version": "0.19.0",
"backstage": {
"role": "frontend-plugin",
"pluginId": "azure-devops",
@ -52,14 +52,14 @@
},
"dependencies": {
"@backstage-community/plugin-azure-devops-common": "workspace:^",
"@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",
"@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:^",
"@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": "^0.33.1",
"@backstage/dev-utils": "^1.1.12",
"@backstage/test-utils": "^1.7.10",
"@backstage/cli": "backstage:^",
"@backstage/dev-utils": "backstage:^",
"@backstage/test-utils": "backstage:^",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",

View File

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

View File

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

View File

@ -1,5 +1,16 @@
# @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.10.0",
"version": "0.11.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": "^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",
"@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:^",
"lodash": "^4.17.21"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.7.0",
"@backstage/cli": "^0.33.1"
"@backstage/backend-test-utils": "backstage:^",
"@backstage/cli": "backstage:^"
},
"files": [
"dist"

View File

@ -1,5 +1,11 @@
# @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.12.0",
"version": "0.13.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": "^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",
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/config": "backstage:^",
"@backstage/errors": "backstage:^",
"@backstage/integration": "backstage:^",
"@backstage/plugin-scaffolder-node": "backstage:^",
"azure-devops-node-api": "^14.1.0",
"yaml": "^2.6.0"
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.1"
"@backstage/cli": "backstage:^",
"@backstage/plugin-scaffolder-node-test-utils": "backstage:^"
},
"files": [
"dist"

View File

@ -1,5 +1,11 @@
# @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.5.0",
"version": "0.6.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": "^1.4.1",
"@backstage/plugin-scaffolder-node": "^0.10.0",
"@backstage/backend-plugin-api": "backstage:^",
"@backstage/plugin-scaffolder-node": "backstage:^",
"fs-extra": "^11.3.0",
"yaml": "^2.6.0",
"zod": "^3.24.3"
},
"devDependencies": {
"@backstage/cli": "^0.33.1",
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.1",
"@backstage/cli": "backstage:^",
"@backstage/plugin-scaffolder-node-test-utils": "backstage:^",
"@types/fs-extra": "^11"
},
"files": [

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
---
'@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.40.2"
"version": "1.41.1"
}

View File

@ -36,9 +36,9 @@
"directory": "workspaces/azure-storage-explorer"
},
"devDependencies": {
"@backstage/cli": "^0.33.0",
"@backstage/cli": "^0.33.1",
"@backstage/e2e-test-utils": "^0.1.1",
"@backstage/repo-tools": "^0.14.0",
"@backstage/repo-tools": "^0.15.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.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",
"@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",
@ -51,7 +51,7 @@
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@backstage/test-utils": "^1.7.9",
"@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

@ -22,27 +22,27 @@
},
"dependencies": {
"@backstage-community/plugin-azure-storage-explorer-backend": "workspace:^",
"@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",
"@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",
"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.0"
"@backstage/cli": "^0.33.1"
},
"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.0",
"@backstage/backend-plugin-api": "^1.4.0",
"@backstage/config": "^1.3.2",
"@backstage/backend-defaults": "^0.11.1",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/config": "^1.3.3",
"@backstage/errors": "^1.2.7",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0"
},
"devDependencies": {
"@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",
"@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",
"@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.3",
"@backstage/core-plugin-api": "^1.10.8",
"@backstage/core-components": "^0.17.4",
"@backstage/core-plugin-api": "^1.10.9",
"@backstage/errors": "^1.2.7",
"@backstage/theme": "^0.6.6",
"@backstage/theme": "^0.6.7",
"@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.0",
"@backstage/core-app-api": "^1.17.1",
"@backstage/dev-utils": "^1.1.11",
"@backstage/test-utils": "^1.7.9",
"@backstage/cli": "^0.33.1",
"@backstage/core-app-api": "^1.18.0",
"@backstage/dev-utils": "^1.1.12",
"@backstage/test-utils": "^1.7.10",
"@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

View File

View File

View File

@ -0,0 +1,8 @@
# 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

@ -0,0 +1,11 @@
{
"$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

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

View File

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

View File

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

56
workspaces/bookmarks/.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# 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

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

View File

@ -0,0 +1,13 @@
# [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

@ -0,0 +1,64 @@
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

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

View File

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

View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,66 @@
{
"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

@ -0,0 +1,12 @@
# 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

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

View File

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

View File

@ -0,0 +1,58 @@
# 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

@ -0,0 +1,65 @@
/*
* 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

@ -0,0 +1,16 @@
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

@ -0,0 +1,23 @@
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

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

View File

@ -0,0 +1,74 @@
{
"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

@ -0,0 +1,48 @@
## 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

@ -0,0 +1,187 @@
/*
* 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

@ -0,0 +1,114 @@
/*
* 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

@ -0,0 +1,43 @@
/*
* 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

@ -0,0 +1,49 @@
/*
* 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

@ -0,0 +1,67 @@
/*
* 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

@ -0,0 +1,91 @@
/*
* 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

@ -0,0 +1,64 @@
/*
* 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

@ -0,0 +1,78 @@
/*
* 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

@ -0,0 +1,59 @@
/*
* 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

@ -0,0 +1,77 @@
/*
* 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

@ -0,0 +1,98 @@
/*
* 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

@ -0,0 +1,113 @@
/*
* 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

@ -0,0 +1,77 @@
/*
* 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

@ -0,0 +1,48 @@
/*
* 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

@ -0,0 +1,21 @@
/*
* 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

@ -0,0 +1,41 @@
/*
* 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

@ -0,0 +1,121 @@
/*
* 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

@ -0,0 +1,52 @@
/*
* 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

@ -0,0 +1,37 @@
/*
* 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

@ -0,0 +1,24 @@
/*
* 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