Compare commits

..
99 Commits
Author SHA1 Message Date
Zhihe Li b8ea558f14 ci: 🎡 Update when e2e tests run (#3320) 2026-06-16 14:25:06 -04:00
Priya Patel 4fc061ec48 chore: 🤖 update desktop client changelog (#3316)
* chore: 🤖 update desktop client changelog and version

* fix: 🐛 update PR number

* docs: ✏️ updated log

* fix: 🐛 revert cli version change
2026-06-16 10:06:41 -07:00
Zhihe Li 3313cd5316 feat: 🎸 Add new grants and update grant templates (#3319)
* feat: 🎸 Add new grants and update grant templates

* feat: 🎸 Use wildcard for remaining grants as well
2026-06-15 18:52:50 -04:00
Zhihe Li af36d75252 feat: 🎸 Update deps for newest shell-quote version (#3318) 2026-06-12 17:25:19 -04:00
72a69fbe8d chore(deps): bump shell-quote in /ui/desktop/electron-app (#3313)
Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.8.2 to 1.8.4.
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/shell-quote/compare/v1.8.2...v1.8.4)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-version: 1.8.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2026-06-12 13:35:04 -04:00
Michael Li e77ed3ae53 test(e2e): Address flake when loading project page (#3315) 2026-06-12 12:06:36 -04:00
Lisbet Alvarez 677a3c925b fix: 🐛 color of search cancel button icon (#3314) 2026-06-10 16:26:55 -07:00
Priya Patel 4cbe08276a chore: 🤖 updates libraries for webkit to resolve e2e test fais (#3317) 2026-06-10 10:14:40 -07:00
Lisbet Alvarez 20699d62dc chore: 🤖 Add override to pin markdown-it to resolve cve (#3311) 2026-06-09 15:14:23 -07:00
Zhihe Li 048339982e feat: 🎸 Use new path for grants schema (#3309) 2026-06-09 15:35:10 -04:00
Priya Patel 2709c2e081 chore: 🤖 updates chaining using pipe operator (#3312) 2026-06-09 08:19:02 -07:00
Susmitha GirumalaandLisbet Alvarez 5e3addcc41 chore: 🤖 Upgrade lint-staged to resolve yaml secvuln (#3310)
✅ Closes: https://github.com/hashicorp/boundary-ui/security/code-scanning/254

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
2026-06-08 15:47:51 -07:00
Zhihe Li 576fef9f8a feat: 🎸 Update scope dropdown in login to show current scope (#3308)
* feat: 🎸 Update scope dropdown in login to show current scope

* test: 💍 Update e2e tests
2026-06-08 17:08:23 -04:00
dillanb-hashi b9517e0233 test: 💍 Allow webkit and firefox to tests to run on enterprise (#3305)
* test: 💍 Allow webkit and firefox to tests to run on enterprise
✅ Closes: ICU-18952
2026-06-05 08:45:28 -07:00
Lisbet Alvarez 4d7ce40469 fix: 🐛 newline being removed for existing import & fix transform errors for a11y ignores codemod (#3183)
* fix: 🐛 newline being removed for existing import & fix errors

* fix: 🐛 avoid testName being undefined
2026-06-04 17:00:56 -07:00
Dharini fa51973e6d chore: make sub claim mapping readonly after creation (#3306) 2026-06-04 15:40:24 -07:00
Lisbet Alvarez f80d7ebeb3 chore: 🤖 add override for tmp in electron-app to resolve cve (#3307) 2026-06-04 15:24:25 -07:00
Dharini 9aea1a8fd8 test: 💍 a11y (#3302) 2026-06-03 16:36:29 -07:00
Dharini 35377e8466 chore: 🤖 remove listwrapper usage (#3294) 2026-06-03 16:36:29 -07:00
Dharini 0e580e3c9f chore: migrate listwrapper Target and worker fields to keyvalue component (#3284) 2026-06-03 16:36:29 -07:00
Dharini e38d3afa58 Chore migrate credential library storage bucket (#3282) 2026-06-03 16:36:29 -07:00
Dharini 0e223ded8f chore: 🤖 migrate host catalog and host set list wrapper fields to key value (#3276) 2026-06-03 16:36:29 -07:00
Dharini 4e2b779d96 migrate ldap list wrapper fields (#3162) 2026-06-03 16:36:29 -07:00
Dharini 71ec7b465a chore: 🤖 migrate oidc fields to keyvalue component (#3160) 2026-06-03 16:36:29 -07:00
Dharini 8b870556a6 chore: 🤖 use hds keyvalue component (#3027) 2026-06-03 16:36:29 -07:00
Lisbet Alvarez ecefa39b98 fix: 🐛 actions not displaying for templates in grants editor (#3296)
* fix: 🐛 actions not displaying for templates in grants editor

* fix: 🐛 invalid ids check

* fix: 🐛 update check for ids type

* test: 💍 fix broken strings

* test: 💍 fix test case

* test: 💍 remove log
2026-06-03 12:46:34 -07:00
Susmitha Girumala 4a206c82dc chore: 🤖 Update openapi-generator-cli to resolve basic-ftp cve (#3303)
✅ Closes: https://github.com/hashicorp/boundary-ui-enterprise/security/code-scanning/287
2026-06-03 12:22:17 -07:00
Michael Li 2d0ed53202 test(e2e): Fix flakiness in host catalog navigation (#3291) 2026-06-02 15:51:42 -04:00
dependabot[bot]andPriya Patel 5000d98e23 chore(deps): bump hashicorp/setup-terraform from 4.0.0 to 4.0.1 (#3299)
Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/hashicorp/setup-terraform/releases)
- [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/setup-terraform/compare/5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85...dfe3c3f87815947d99a8997f908cb6525fc44e9e)

---
updated-dependencies:
- dependency-name: hashicorp/setup-terraform
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Priya Patel <priya.patel@hashicorp.com>
2026-06-01 17:29:20 -07:00
dependabot[bot] 4afd7c2d0b chore(deps): bump browser-actions/setup-chrome from 2.1.1 to 2.1.2 (#3300)
Bumps [browser-actions/setup-chrome](https://github.com/browser-actions/setup-chrome) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/browser-actions/setup-chrome/releases)
- [Changelog](https://github.com/browser-actions/setup-chrome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/browser-actions/setup-chrome/compare/4f8e94349a351df0f048634f25fec36c3c91eded...2e1d749697dd1612b833dba4a722266286fbefcd)

---
updated-dependencies:
- dependency-name: browser-actions/setup-chrome
  dependency-version: 2.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-01 16:55:28 -07:00
dependabot[bot]andPriya Patel 207b1e8a7c chore(deps): bump actions/labeler from 6.0.1 to 6.1.0 (#3298)
Bumps [actions/labeler](https://github.com/actions/labeler) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/634933edcd8ababfe52f92936142cc22ac488b1b...f27b608878404679385c85cfa523b85ccb86e213)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Priya Patel <priya.patel@hashicorp.com>
2026-06-01 16:41:29 -07:00
dependabot[bot]andPriya Patel 8b90db1ff5 chore(deps): bump slackapi/slack-github-action from 3.0.2 to 3.0.3 (#3297)
Bumps [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slackapi/slack-github-action/compare/03ea5433c137af7c0495bc0cad1af10403fc800c...45a88b9581bfab2566dc881e2cd66d334e621e2c)

---
updated-dependencies:
- dependency-name: slackapi/slack-github-action
  dependency-version: 3.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Priya Patel <priya.patel@hashicorp.com>
2026-06-01 16:06:40 -07:00
Zhihe Li 7fc10855d9 feat: 🎸 Add override to fix node 24 builds (#3301) 2026-06-01 18:39:45 -04:00
Lisbet Alvarez 7c3be7c9a0 chore: 🤖 upgrade @babel/preset-env to resolve cve (#3292) 2026-06-01 11:36:59 -07:00
Lisbet Alvarez 7cddf7cb91 Refactor utils to share components (#3279)
* refactor: 💡 initial refactor for utils to share components

* refactor: 💡 actions logic

* Add missing copyright headers

* refactor: 💡 simplify actions getter logic

* refactor: 💡 convert output fields logic into a separate getter

* test: 💍 fix unit test

* fix: 🐛 bug with unknown fields lint error

* refactor: 💡 pr feedback - simplify template check

* refactor: 💡 remove unnecessary unit test file
2026-05-29 16:52:00 -07:00
Zhihe Li 878bd241f3 feat: 🎸 Update playwright to newest version (#3290)
* feat: 🎸 Update playwright to newest version

* fix: 🐛 Fix cache dependency key
2026-05-29 19:23:04 -04:00
Susmitha Girumala 4aec0d3acc chore: 🤖 Upgrade fast-uri for cve (#3289)
✅ Closes: https://github.com/hashicorp/boundary-ui/security/code-scanning/333,
https://github.com/hashicorp/boundary-ui/security/code-scanning/334,
https://github.com/hashicorp/boundary-ui/security/code-scanning/335,
https://github.com/hashicorp/boundary-ui/security/code-scanning/336
2026-05-29 15:27:18 -04:00
Susmitha GirumalaandZhihe Li e43625f76c chore: 🤖 Upgrade tmp module for secvuln resolution (#3287)
* chore: 🤖 Upgrade tmp module for secvul cve-2026-44705

Resolves
https://github.com/hashicorp/boundary-ui/security/code-scanning/342

* chore: 🤖 Upgrade tmp to resolve desktop client secvuln

✅ Closes: https://github.com/hashicorp/boundary-ui/security/code-scanning/342

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2026-05-29 14:26:59 -04:00
Priya Patel 5b51edd17f fix: 🐛 updates node and pnpm setup order (#3286) 2026-05-29 10:36:17 -07:00
Dharini 6eb21d3e13 Display session playback error (#3280) 2026-05-29 09:54:55 -07:00
Susmitha Girumala 9b43819865 chore: 🤖 Upgrade xmldom for secvul resolution (#3288)
✅ Closes: https://github.com/hashicorp/boundary-ui/security/code-scanning/313,
https://github.com/hashicorp/boundary-ui/security/code-scanning/311,
https://github.com/hashicorp/boundary-ui/security/code-scanning/310,
https://github.com/hashicorp/boundary-ui/security/code-scanning/309
2026-05-29 12:07:55 -04:00
Zhihe Li 562b371db3 feat: 🎸 Update grant template names (#3283) 2026-05-29 10:18:42 -04:00
Zhihe Li 89d492042e feat: 🎸 Fix contrast issue with count badge in dropdown (#3285)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-19106
2026-05-28 09:59:48 -04:00
Lisbet Alvarez f9197d5a99 chore: 🤖 Upgrade electron to v42.0.1 (latest) (#3278) 2026-05-21 16:59:13 -07:00
Zhihe Li ebd7f64fe3 fix: 🐛 Add missing code block description (#3277)
* fix: 🐛 Add missing code block description

* feat: 🎸 Add autocomplete on empty lines
2026-05-20 18:29:40 -04:00
Zhihe Li 7dadc53fe1 Remove renderer url (#3194)
* feat: 🎸 Refactor how we store cluster URL
2026-05-20 16:36:16 -04:00
Zhihe Li 3d257afd49 refactor: 💡 Update transaction style and add busy timeout PRAGMA (#3271)
* refactor: 💡 Update transaction style and add busy PRAGMA

* refactor: 💡 Add return to insert statement for consistency

* refactor: 💡 Also add check for db

* test: 💍 Fix flaky test
2026-05-18 19:05:29 -04:00
Lisbet Alvarez 95918cb997 fix: 🐛 scrollbar color in dark mode + layout bugs (#3226)
* fix: 🐛 header scrollbar overlap in desktop

* refactor: 💡 undo desktop scrollbar changes

* refactor: 💡 fix whitespace issues when viewport is small

* fix: 🐛 scrollbar color issue in dark mode

* fix: 🐛 move style inside of media query
2026-05-15 15:01:19 -07:00
Lisbet Alvarez 077200d792 test: 💍 fix a11y violations and ignore existing issues (#3275)
* test: 💍 fix a11y violations and ignore existing issues

* test: 💍 remove ignores for fixed violation

* test: 💍 more color contrast ignores for new tests
2026-05-15 13:05:19 -07:00
Zhihe Li f34a1df298 refactor: 💡 Update e2e test for finding code editor (#3273)
* refactor: 💡 Update e2e test for finding code editor

* test: 💍 Added more acceptance tests
2026-05-15 12:40:37 -04:00
Zhihe Li 7fa59bfd5d feat: 🎸 Add support for output fields (#3268)
* feat: 🎸 Add support for output fields

* refactor: 💡 PR feedback
2026-05-14 15:21:30 -04:00
e0b80b1176 refactor: 💡 update pinned IDs logic for actions table (#3269)
* chore: 🤖 Add initial page layout for code editor + sidebar (#3180)

* chore: 🤖 Add initial page layout for code editor + sidebar

* test: 💍 add missing a11y ignores for exisitng color issue

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18460

* Replace `Rose::CodeEditor` with `Hds::CodeEditor` (#2679)

* chore: 🤖 replace Rose::CodeEditor with Hds::CodeEditor

- Remove CodeEditor 5 dependency

* chore: 🤖 Replace CodeMirror5 dep

remove CodeMirror5 and replace existing editors with HDS::CodeEditor

* chore: 🤖 deleting old codeeditor styles

* feat: 🎸 changing code editor

changing code editor

✅ Closes: 0

* removed unused export

* pr cleanup

* updating tests for new code editor

* responding to PR feedback

* added whitespace

* fixing linting violations

* chore: 🤖 self-review PR

self-review PR in the course of switching to new HDS code editor

* fix: 🐛 fix linting bug

* fix: 🐛 unneeded dependency

remove jslint dependency as this is handled in the new code editor from
HDS

* fix: 🐛 Fixing import bug

Fix import bug caused by importing scss modules that no longer exist

* Fix and refactor code editor acceptance test

* Fix and refactor code editor acceptance test

* chore: 🤖 remove codemirror dependency

* refactor: 💡 fix merge conflict issues

* test: 💍 update e2e test selectors

* test: 💍 fix target spec click event

* refactor: 💡 clean up test selectors for code editor

* refactor: 💡 clean up storage bucket test selectors

* chore: 🤖 remove code mirror modifiers

---------

Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>

* feat: 🎸 Create string formats component (#3191)

* feat: 🎸 Create string formats component

* Add missing copyright headers

* fix: 🐛 change to readonly

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18458

* feat: 🎸 add grant schema to mirage (#3196)

* feat: 🎸 add grant schema to mirage

* refactor: 💡 update grant schema mirage data

* refactor: 💡 fix route name in mirage

* feat: 🎸 add export options flyout (#3198)

* feat: 🎸 add export options flyout

* fix: 🐛 update id to be ids

* test: 💍 add a11y ignores

* test: 💍 fix tests for grant string format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18524

* feat: 🎸 Add autocompletion to code editor (#3207)

* feat: 🎸 Add CSP nonce to code editors (#3230)

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* feat: 🎸 Add linting to code editor (#3211)

* feat: 🎸 Add linting to code editor

* refactor: 💡 add missing case for wildcard type & ids field

* refactor: 💡 simplify condition + add more tests

* refactor: 💡 cleanup ids validation logic and add more lint erro

* refactor: 💡 simplify logic and add actions linting cases

* feat: 🎸 Add autocompletion to code editor (#3207)

* refactor: 💡 apply PR feedback

* refactor: 💡 PR feedback

* refactor: 💡 add noop action

* fix: 🐛 noop should not be suggested with resource type format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18462

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* Icu 18506 load resource IDs for autocomplete (#3247)

* feat: 🎸 Add id auto completion
* style: 💄 Fix code editor styling for dark mode
* feat: 🎸 Add optimization for caching sqlite data

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18506

* refactor: 💡 Update string formats to use select + add options (#3256)

* refactor: 💡 Update string formats to use select + add options

* refactor: 💡 PR feedback

* test: 💍 fix tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18983

* refactor: 💡 role grants tab to use code block (#3261)

* refactor: 💡 grants tab to use code block

* refactor: 💡 update getter comment

* test: 💍 fix tests

* refactor: 💡 remove language arg for codeblock

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18757

* Grant actions card (#3246)

* chore: 🤖 Add initial page layout for code editor + sidebar (#3180)

* chore: 🤖 Add initial page layout for code editor + sidebar

* test: 💍 add missing a11y ignores for exisitng color issue

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18460

* Replace `Rose::CodeEditor` with `Hds::CodeEditor` (#2679)

* chore: 🤖 replace Rose::CodeEditor with Hds::CodeEditor

- Remove CodeEditor 5 dependency

* chore: 🤖 Replace CodeMirror5 dep

remove CodeMirror5 and replace existing editors with HDS::CodeEditor

* chore: 🤖 deleting old codeeditor styles

* feat: 🎸 changing code editor

changing code editor

✅ Closes: 0

* removed unused export

* pr cleanup

* updating tests for new code editor

* responding to PR feedback

* added whitespace

* fixing linting violations

* chore: 🤖 self-review PR

self-review PR in the course of switching to new HDS code editor

* fix: 🐛 fix linting bug

* fix: 🐛 unneeded dependency

remove jslint dependency as this is handled in the new code editor from
HDS

* fix: 🐛 Fixing import bug

Fix import bug caused by importing scss modules that no longer exist

* Fix and refactor code editor acceptance test

* Fix and refactor code editor acceptance test

* chore: 🤖 remove codemirror dependency

* refactor: 💡 fix merge conflict issues

* test: 💍 update e2e test selectors

* test: 💍 fix target spec click event

* refactor: 💡 clean up test selectors for code editor

* refactor: 💡 clean up storage bucket test selectors

* chore: 🤖 remove code mirror modifiers

---------

Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>

* feat: 🎸 Create string formats component (#3191)

* feat: 🎸 Create string formats component

* Add missing copyright headers

* fix: 🐛 change to readonly

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18458

* feat: 🎸 add grant schema to mirage (#3196)

* feat: 🎸 add grant schema to mirage

* refactor: 💡 update grant schema mirage data

* refactor: 💡 fix route name in mirage

* feat: 🎸 add export options flyout (#3198)

* feat: 🎸 add export options flyout

* fix: 🐛 update id to be ids

* test: 💍 add a11y ignores

* test: 💍 fix tests for grant string format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18524

* feat: 🎸 Add autocompletion to code editor (#3207)

* feat: 🎸 create grant actions component

* feat: 🎸 add description to actions

* feat: 🎸 update default logic to not show all actions

* feat: 🎸 show invalid id and type message

* test: 💍 create const for test selectors

* feat: 🎸 trigger action suggestion when cursor moves

* refactor: 💡 add createGrantLineHelpers func

* refactor: 💡 small refactors

* feat: 🎸 Add CSP nonce to code editors (#3230)

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* refactor: 💡 small refactor

* test: 💍 add more tests

* refactor: 💡 fix merge conflicts issues

* refactor: 💡 updates from PR feedback

* refactor: 💡 updates for completions and linting

* test: 💍 fix test issue with translations

* test: 💍 fix test issue that was already fixed once

* refactor: 💡 address PR feedback

---------

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Zack Moore <zack.moore@hashicorp.com>
Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
Co-authored-by: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com>

* refactor: 💡 update pinned IDs logic for actions table

* refactor: 💡 use if/else in place of complex ternary

* refactor: 💡 user intl.formatList

---------

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Zack Moore <zack.moore@hashicorp.com>
Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
Co-authored-by: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com>
2026-05-14 15:21:30 -04:00
Zhihe Li 0479c3c8fc test: 💍 Fix broken tests after new code editor (#3213) 2026-05-14 15:21:30 -04:00
08b8b03fed Grant actions card (#3246)
* chore: 🤖 Add initial page layout for code editor + sidebar (#3180)

* chore: 🤖 Add initial page layout for code editor + sidebar

* test: 💍 add missing a11y ignores for exisitng color issue

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18460

* Replace `Rose::CodeEditor` with `Hds::CodeEditor` (#2679)

* chore: 🤖 replace Rose::CodeEditor with Hds::CodeEditor

- Remove CodeEditor 5 dependency

* chore: 🤖 Replace CodeMirror5 dep

remove CodeMirror5 and replace existing editors with HDS::CodeEditor

* chore: 🤖 deleting old codeeditor styles

* feat: 🎸 changing code editor

changing code editor

✅ Closes: 0

* removed unused export

* pr cleanup

* updating tests for new code editor

* responding to PR feedback

* added whitespace

* fixing linting violations

* chore: 🤖 self-review PR

self-review PR in the course of switching to new HDS code editor

* fix: 🐛 fix linting bug

* fix: 🐛 unneeded dependency

remove jslint dependency as this is handled in the new code editor from
HDS

* fix: 🐛 Fixing import bug

Fix import bug caused by importing scss modules that no longer exist

* Fix and refactor code editor acceptance test

* Fix and refactor code editor acceptance test

* chore: 🤖 remove codemirror dependency

* refactor: 💡 fix merge conflict issues

* test: 💍 update e2e test selectors

* test: 💍 fix target spec click event

* refactor: 💡 clean up test selectors for code editor

* refactor: 💡 clean up storage bucket test selectors

* chore: 🤖 remove code mirror modifiers

---------

Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>

* feat: 🎸 Create string formats component (#3191)

* feat: 🎸 Create string formats component

* Add missing copyright headers

* fix: 🐛 change to readonly

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18458

* feat: 🎸 add grant schema to mirage (#3196)

* feat: 🎸 add grant schema to mirage

* refactor: 💡 update grant schema mirage data

* refactor: 💡 fix route name in mirage

* feat: 🎸 add export options flyout (#3198)

* feat: 🎸 add export options flyout

* fix: 🐛 update id to be ids

* test: 💍 add a11y ignores

* test: 💍 fix tests for grant string format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18524

* feat: 🎸 Add autocompletion to code editor (#3207)

* feat: 🎸 create grant actions component

* feat: 🎸 add description to actions

* feat: 🎸 update default logic to not show all actions

* feat: 🎸 show invalid id and type message

* test: 💍 create const for test selectors

* feat: 🎸 trigger action suggestion when cursor moves

* refactor: 💡 add createGrantLineHelpers func

* refactor: 💡 small refactors

* feat: 🎸 Add CSP nonce to code editors (#3230)

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* refactor: 💡 small refactor

* test: 💍 add more tests

* refactor: 💡 fix merge conflicts issues

* refactor: 💡 updates from PR feedback

* refactor: 💡 updates for completions and linting

* test: 💍 fix test issue with translations

* test: 💍 fix test issue that was already fixed once

* refactor: 💡 address PR feedback

---------

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Zack Moore <zack.moore@hashicorp.com>
Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
Co-authored-by: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com>
2026-05-14 15:21:29 -04:00
Lisbet Alvarez 1a8c895ea9 refactor: 💡 role grants tab to use code block (#3261)
* refactor: 💡 grants tab to use code block

* refactor: 💡 update getter comment

* test: 💍 fix tests

* refactor: 💡 remove language arg for codeblock

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18757
2026-05-14 15:21:29 -04:00
Lisbet Alvarez 990146fa34 refactor: 💡 Update string formats to use select + add options (#3256)
* refactor: 💡 Update string formats to use select + add options

* refactor: 💡 PR feedback

* test: 💍 fix tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18983
2026-05-14 15:21:29 -04:00
Zhihe Li e5657a8d4c Icu 18506 load resource IDs for autocomplete (#3247)
* feat: 🎸 Add id auto completion
* style: 💄 Fix code editor styling for dark mode
* feat: 🎸 Add optimization for caching sqlite data

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18506
2026-05-14 15:21:29 -04:00
Lisbet AlvarezandZhihe Li 1d126dc074 feat: 🎸 Add linting to code editor (#3211)
* feat: 🎸 Add linting to code editor

* refactor: 💡 add missing case for wildcard type & ids field

* refactor: 💡 simplify condition + add more tests

* refactor: 💡 cleanup ids validation logic and add more lint erro

* refactor: 💡 simplify logic and add actions linting cases

* feat: 🎸 Add autocompletion to code editor (#3207)

* refactor: 💡 apply PR feedback

* refactor: 💡 PR feedback

* refactor: 💡 add noop action

* fix: 🐛 noop should not be suggested with resource type format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18462

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2026-05-14 15:21:29 -04:00
Bharath GajjalaandZhihe Li dc224268f5 feat: 🎸 Add CSP nonce to code editors (#3230)
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
2026-05-14 15:21:28 -04:00
Zhihe Li 4c15d72321 feat: 🎸 Add autocompletion to code editor (#3207) 2026-05-14 15:21:28 -04:00
Lisbet Alvarez add2a9d6c4 feat: 🎸 add export options flyout (#3198)
* feat: 🎸 add export options flyout

* fix: 🐛 update id to be ids

* test: 💍 add a11y ignores

* test: 💍 fix tests for grant string format

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18524
2026-05-14 15:21:28 -04:00
Cameron Perera 759c1f5555 feat: 🎸 add grant schema to mirage (#3196)
* feat: 🎸 add grant schema to mirage

* refactor: 💡 update grant schema mirage data

* refactor: 💡 fix route name in mirage
2026-05-14 15:21:28 -04:00
Lisbet Alvarez 37de7e3480 feat: 🎸 Create string formats component (#3191)
* feat: 🎸 Create string formats component

* Add missing copyright headers

* fix: 🐛 change to readonly

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18458
2026-05-14 15:21:28 -04:00
d469f305de Replace Rose::CodeEditor with Hds::CodeEditor (#2679)
* chore: 🤖 replace Rose::CodeEditor with Hds::CodeEditor

- Remove CodeEditor 5 dependency

* chore: 🤖 Replace CodeMirror5 dep

remove CodeMirror5 and replace existing editors with HDS::CodeEditor

* chore: 🤖 deleting old codeeditor styles

* feat: 🎸 changing code editor

changing code editor

✅ Closes: 0

* removed unused export

* pr cleanup

* updating tests for new code editor

* responding to PR feedback

* added whitespace

* fixing linting violations

* chore: 🤖 self-review PR

self-review PR in the course of switching to new HDS code editor

* fix: 🐛 fix linting bug

* fix: 🐛 unneeded dependency

remove jslint dependency as this is handled in the new code editor from
HDS

* fix: 🐛 Fixing import bug

Fix import bug caused by importing scss modules that no longer exist

* Fix and refactor code editor acceptance test

* Fix and refactor code editor acceptance test

* chore: 🤖 remove codemirror dependency

* refactor: 💡 fix merge conflict issues

* test: 💍 update e2e test selectors

* test: 💍 fix target spec click event

* refactor: 💡 clean up test selectors for code editor

* refactor: 💡 clean up storage bucket test selectors

* chore: 🤖 remove code mirror modifiers

---------

Co-authored-by: calcaide <ccorvo@hashicorp.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>
2026-05-14 15:21:27 -04:00
Lisbet Alvarez fd1eba617b chore: 🤖 Add initial page layout for code editor + sidebar (#3180)
* chore: 🤖 Add initial page layout for code editor + sidebar

* test: 💍 add missing a11y ignores for exisitng color issue

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18460
2026-05-14 15:21:27 -04:00
Zhihe Li 5048fd478a feat: 🎸 Handle rate limiting (#3257)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12035
2026-05-14 15:21:07 -04:00
Susmitha Girumala de907284ff chore: 🤖 Upgrade path-to-regexp to latest (#3260)
* chore: 🤖 Upgrade path-to-regexp to latest

Fix multiple path-to-regexp sec vulns up upgrading to latest possible
versions. This fixes:
- https://github.com/hashicorp/boundary-ui/security/code-scanning/272
- https://github.com/hashicorp/boundary-ui/security/code-scanning/269
- https://github.com/hashicorp/boundary-ui/security/code-scanning/266

* chore: 🤖 Resolve broken pnpm lockfile after merge with install
2026-05-14 11:32:21 -04:00
Zhihe Li 6e9fe1c675 docs: ✏️ Add documentation for pnpm overrides (#3263) 2026-05-13 12:54:34 -04:00
Cameron Perera 6b29a875f3 test: 💍 update json credential flaky tests (#3267)
* test: 💍 update json credential flaky tests

* test: 💍 add commonSelector for code mirror editor
2026-05-13 11:26:41 -05:00
Michael Li 1fd638618b doc: No more enos variables (#3262) 2026-05-13 09:51:13 -04:00
dependabot[bot] 6704a251e0 chore(deps): bump pnpm/action-setup from 5.0.0 to 6.0.8 (#3249)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 5.0.0 to 6.0.8.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/fc06bc1257f339d1d5d8b3a19a8cae5388b55320...0e279bb959325dab635dd2c09392533439d90093)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 16:47:41 -04:00
Zhihe Li 46ed965360 chore: 🤖 Update node version (#3265) 2026-05-12 16:02:21 -04:00
Susmitha Girumala 283e128139 chore: 🤖 Fix axios cve-2026-42038 (#3259)
* chore: 🤖 Fix axios cve-2026-42038 by adding override to latest

✅ Closes: https://github.com/hashicorp/boundary-ui/security/code-scanning/327

* chore: 🤖 Undo axios override in favour of updating parent mod

Update openapi-generator-cli instead of dependent axios
2026-05-11 15:05:08 -04:00
Zhihe Li 047db02a65 fix: 🐛 Add extra check for SQLite errors (#3258) 2026-05-08 10:30:05 -04:00
Zhihe Li bd837a6383 fix: 🐛 Fix race condition from sqlite initialization (#3253) 2026-05-05 10:25:40 -04:00
dependabot[bot] 2a6fac66fc chore(deps-dev): bump postcss from 8.5.3 to 8.5.10 (#3255) 2026-05-04 16:17:13 -07:00
dependabot[bot] 4bdcd4ce69 chore(deps): bump actions/setup-node from 6.3.0 to 6.4.0 (#3252) 2026-05-04 15:21:37 -07:00
dependabot[bot] 9f76650dfb chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#3251) 2026-05-04 14:51:54 -07:00
dependabot[bot] c33d1718a2 chore(deps): bump slackapi/slack-github-action from 3.0.1 to 3.0.2 (#3250) 2026-05-04 14:07:44 -07:00
Priya Patel 923dedc9bb fix: 🐛 hides preferred clients section for linux (#3241) 2026-04-24 10:31:42 -07:00
dependabot[bot] 2d9b9d9659 chore(deps): bump uuid from 11.1.0 to 14.0.0 (#3245)
Bumps [uuid](https://github.com/uuidjs/uuid) from 11.1.0 to 14.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.1.0...v14.0.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 09:41:13 -04:00
Michael Li 6d7e24fdc5 chore(e2e): Log in via CLI during global setup to update cache (#3235)
* chore(e2e): log in via CLI

* CR

* CR: Rename ci option
2026-04-23 11:57:24 -04:00
Dharini 4f9086642b fix: 🐛 refresh action in session recording (#3229) 2026-04-22 10:54:50 -07:00
Susmitha Girumala 337963bff8 chore: 🤖 Upgrade socket.io-parser to fix sec vuln (#3244)
Fixes CVE 2026 33151

✅ Closes: https://github.com/hashicorp/boundary-ui/security/dependabot/269
2026-04-22 12:25:35 -04:00
Cameron Perera b87ed9f3db chore: 🤖 bump xmldom in electron-app (#3243) 2026-04-21 15:40:03 -05:00
dependabot[bot]andcameronperera 95b78299f9 chore(deps): bump brace-expansion from 1.1.12 to 1.1.13 in /ui/desktop/electron-app (#3210)
* chore(deps): bump brace-expansion in /ui/desktop/electron-app

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

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

* chore: 🤖 bump brace-expansion package

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>
2026-04-21 13:39:20 -05:00
Cameron Perera cf0df900e0 chore: 🤖 bump lodash to 4.18.1 (#3242) 2026-04-21 11:36:01 -05:00
dependabot[bot]andcameronperera 214304dc8c chore(deps): bump lodash from 4.17.23 to 4.18.1 in /ui/desktop/electron-app (#3236)
* chore(deps): bump lodash in /ui/desktop/electron-app

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

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

* chore: 🤖 bump lodash version in electron-app

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: cameronperera <cameron.perera@hashicorp.com>
2026-04-21 10:55:03 -05:00
Susmitha Girumala ed8075d200 chore: 🤖 Update flatted dep to latest (#3240)
Update flatted to latest >=3.4.0 version to fix DoS vuln CVE-2026-32141
2026-04-17 10:00:59 -04:00
Susmitha Girumala 07089e97e2 chore: 🤖 Update immutable to fix CVE 2026 29063 (#3237)
* chore: 🤖 Update immutable to fix CVE 2026 29063

Tag immutable to latest 5.1.5 version to fix prototype pollution vuln

✅ Closes: https://github.com/hashicorp/boundary-ui/security/code-scanning/239

* chore: 🤖 Undo immutable override in favor of updating only deps
2026-04-15 11:59:27 -04:00
Dharini 14b3c8c6d0 fix: 🐛 @xmldom/xmldom version update (#3238) 2026-04-14 14:21:57 -07:00
Zhihe Li 6580350dcf feat: 🎸 Update AWS host catalog fields to be required (#3214) 2026-04-13 16:33:41 -04:00
Michael Li 51be681758 chore: Always run end-to-end tests (#3234)
* chore: Always run e2e tests

* chore: Parallelize browsers
2026-04-13 15:08:21 -04:00
Susmitha Girumala dab50e352d chore: 🤖 Update underscore to fix vulnerability (#3231)
* chore: 🤖 Update underscore to fix vulnerability

Code scanning identified a potential DoS attach for underscore versions
not using latest 1.13.8 release

✅ Closes: https://github.com/jashkenas/underscore/issues/3011

* chore: 🤖 Remove nomnom module underscore override
2026-04-09 16:14:45 -04:00
dependabot[bot] f3af41dea4 chore(deps): bump hashicorp/action-setup-enos from 1.50 to 1.52 (#3221) 2026-04-08 09:51:01 -07:00
dependabot[bot] fb3f88a77d chore(deps): bump slackapi/slack-github-action from 2.1.1 to 3.0.1 (#3223) 2026-04-08 09:08:54 -07:00
dependabot[bot] 7345c79b20 chore(deps): bump actions/setup-go from 6.0.0 to 6.4.0 (#3220) 2026-04-07 16:18:41 -07:00
dependabot[bot] 9ed79be3ef chore(deps): bump actions/setup-node from 6.2.0 to 6.3.0 (#3222) 2026-04-07 15:37:22 -07:00
dependabot[bot] 7614f17efb chore(deps): bump pnpm/action-setup from 4.2.0 to 5.0.0 (#3219) 2026-04-07 11:59:18 -07:00
250 changed files with 12488 additions and 7208 deletions

No files matched your search

+10 -10
View File
@@ -13,18 +13,18 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
timeout-minutes: 40
steps:
- uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2.1.1
- uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
with:
install-dependencies: true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install
- name: Run a11y tests
env:
@@ -37,11 +37,11 @@ jobs:
jq -s 'map(.[])' ./ui/admin/ember-a11y-report/*.json > ember-a11y-reports/admin.json
jq -s 'map(.[])' ./ui/desktop/ember-a11y-report/*.json > ember-a11y-reports/desktop.json
- name: Output ember a11y test report summary (admin)
run: 'cat ember-a11y-reports/admin.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
run: 'cat ember-a11y-reports/admin.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[] | .[]? | .relatedNodes? | .[]? | .target? })}) })"'
- name: Output ember a11y test report summary (desktop)
run: 'cat ember-a11y-reports/desktop.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
run: 'cat ember-a11y-reports/desktop.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[] | .[]? | .relatedNodes? | .[]? | .target? })}) })"'
- name: Upload ember a11y reports
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ember-a11y-reports
path: ./ember-a11y-reports
+31 -31
View File
@@ -12,14 +12,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --frozen-lockfile
build-admin-ui-oss:
name: Build Admin UI OSS
@@ -27,24 +27,24 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- name: Build
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
run_install: false
- name: Build
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
- run: pnpm install --offline
- run: pnpm run build:ui:admin:oss
- name: Upload artifact admin-ui
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: admin-ui
path: ./ui/admin/dist/
- name: Upload artifact OSS admin UI
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: admin-ui-oss
path: ./ui/admin/dist/
@@ -54,19 +54,19 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- name: Build
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
run_install: false
- name: Build
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
- run: pnpm install --offline
- run: pnpm run build:ui:admin:enterprise
- name: Upload artifact enterprise admin UI
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: admin-ui-ent
path: ./ui/admin/dist/
@@ -76,19 +76,19 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- name: Build
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
run_install: false
- name: Build
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
- run: pnpm install --offline
- run: pnpm run build:ui:admin:hcp
- name: Upload artifact HCP admin UI
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: admin-ui-hcp
path: ./ui/admin/dist/
+2 -2
View File
@@ -33,9 +33,9 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
node-version: 24
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1 -1
View File
@@ -6,6 +6,6 @@ jobs:
triage:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+25 -25
View File
@@ -7,14 +7,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --frozen-lockfile
compliance:
@@ -23,14 +23,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
- run: pnpm install --offline
- run: pnpm run compliance:licenses
@@ -41,14 +41,14 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
- run: pnpm install --offline
- run: pnpm lint
@@ -58,17 +58,17 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }}
timeout-minutes: 15
steps:
- uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2.1.1
- uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
with:
install-dependencies: true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
- run: pnpm install --offline
- run: pnpm test
+142 -54
View File
@@ -3,13 +3,6 @@ name: Test E2E Admin UI
on:
pull_request:
types: [labeled, opened, synchronize, reopened]
paths:
- 'ui/admin/**'
- 'e2e-tests/*'
- 'e2e-tests/api-client/**'
- 'e2e-tests/helpers/**'
- 'e2e-tests/admin/**'
- '.github/workflows/test-e2e-admin-ui.yaml'
workflow_dispatch:
inputs:
boundary-enterprise-branch:
@@ -24,30 +17,57 @@ on:
type: string
jobs:
test:
name: test
setup:
name: setup matrix
# don't need to run e2e tests on ce -> ent merges since tests were already
# run in the ce branch, and there are currently no code diffferences between
# run in the ce branch, and there are currently no code differences between
# the two repos
if: github.actor != 'hc-github-team-secure-boundary'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Set matrix
id: set-matrix
run: |
BOUNDARY_BRANCH="${{ github.event.inputs.boundary-branch || 'main' }}"
ENT_BRANCH="${{ github.event.inputs.boundary-enterprise-branch || 'main' }}"
if [[ "${{ github.repository }}" == "hashicorp/boundary-ui" ]]; then
matrix=$(jq -cn --arg branch "$BOUNDARY_BRANCH" '{
browser: ["chromium", "firefox", "webkit"],
include: [{
boundary_edition: "community",
github_repo: "hashicorp/boundary",
folder_name: "boundary",
boundary_edition_short: "oss",
enos_scenario: "e2e_ui_docker",
test_command: "admin:ce:docker",
boundary_branch: $branch
}]
}')
else
matrix=$(jq -cn --arg branch "$ENT_BRANCH" '{
browser: ["chromium", "firefox", "webkit"],
include: [{
boundary_edition: "enterprise",
github_repo: "hashicorp/boundary-enterprise",
folder_name: "boundary-enterprise",
boundary_edition_short: "ent",
enos_scenario: "e2e_ui_docker_ent",
test_command: "admin:ent:docker",
boundary_branch: $branch
}]
}')
fi
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
test:
name: test
needs: setup
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
strategy:
matrix:
include:
- boundary_edition: community
github_repo: hashicorp/boundary
folder_name: boundary
boundary_edition_short: oss
enos_scenario: e2e_ui_docker
test_command: admin:ce:docker
boundary_branch: ${{ github.event.inputs.boundary-branch || 'main' }}
- boundary_edition: enterprise
github_repo: hashicorp/boundary-enterprise
folder_name: boundary-enterprise
boundary_edition_short: ent
enos_scenario: e2e_ui_docker_ent
test_command: admin:ent:docker
boundary_branch: ${{ github.event.inputs.boundary-enterprise-branch || 'main' }}
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
env:
ENOS_VAR_e2e_debug_no_run: true
@@ -56,25 +76,25 @@ jobs:
with:
fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: |
node-version: 24
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: |
pnpm-lock.yaml
ui/desktop/electron-app/pnpm-lock.yaml
run_install: false
- name: Set up Terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e
- name: Set up Enos
uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a
uses: hashicorp/action-setup-enos@6ec106c8f809fe645162d73bea565c65f3269907
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -159,15 +179,89 @@ jobs:
run: |
git config --global url."https://oauth2:${{ secrets.ELEVATED_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com"
- name: Install node dependencies
- name: Install Dependencies and Browsers
run: |
pnpm install --frozen-lockfile
# Block Playwright from doing anything during the pnpm install
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 pnpm install --frozen-lockfile
cd e2e-tests
# Install browsers and dependencies based on the repository
if [[ "${{ github.repository }}" == "hashicorp/boundary-ui" ]]; then
pnpm playwright install --with-deps
elif [[ "${{ github.repository }}" == "hashicorp/boundary-ui-enterprise" ]]; then
# playwright dependencies can't be installed on a self-hosted runner
# due to permission issues
# Use raw apt-get to install system libraries without npx
sudo apt-get install -y \
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libdbus-1-3 \
libxcb1 \
libxkbcommon0 \
libx11-6 \
libxcomposite1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libpango-1.0-0 \
libcairo2 \
libasound2 \
libgtk-4-1 \
libgraphene-1.0-0 \
libevent-2.1-7 \
libopus0 \
libgstreamer-plugins-base1.0-0 \
libgstreamer-plugins-good1.0-0 \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-bad1.0-0 \
libflite1 \
libwebpdemux2 \
libavif13 \
libwebpmux3 \
libmanette-0.2-0 \
libenchant-2-2 \
libhyphen0 \
libsecret-1-0 \
libwoff1 \
libx264-163 \
libsoup-3.0-0 \
gstreamer1.0-libav \
libicu70 \
libegl1 \
libepoxy0 \
libevdev2 \
libffi7 \
libfontconfig1 \
libfreetype6 \
libgdk-pixbuf-2.0-0 \
libgles2 \
libglib2.0-0 \
libglx0 \
libgstreamer1.0-0 \
libgudev-1.0-0 \
libharfbuzz-icu0 \
libharfbuzz0b \
libjpeg-turbo8 \
liblcms2-2 \
libnotify4 \
libopengl0 \
libopenjp2-7 \
libpng16-16 \
libproxy1v5 \
libwayland-client0 \
libwayland-egl1 \
libwayland-server0 \
libxml2 \
libxslt1.1 \
libatomic1
# Install the browser executables
pnpm playwright install
fi
@@ -178,7 +272,7 @@ jobs:
echo "go-version=$(cat ./support/src/${{matrix.folder_name}}/.go-version)" >> "$GITHUB_OUTPUT"
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ steps.get-go-version.outputs.go-version }}
cache: false
@@ -217,19 +311,13 @@ jobs:
run: |
source <(bash ./support/src/${{matrix.folder_name}}/enos/scripts/test_e2e_env.sh)
cd e2e-tests
if [[ "${{ github.repository }}" == "hashicorp/boundary-ui" ]]; then
pnpm run ${{ matrix.test_command }} --reporter=html
elif [[ "${{ github.repository }}" == "hashicorp/boundary-ui-enterprise" ]]; then
# only run tests on chromium because playwright dependencies can't
# be installed to support other browsers
pnpm run ${{ matrix.test_command }} --project=chromium --reporter=html
fi
pnpm run ${{ matrix.test_command }} --project=${{ matrix.browser}} --reporter=html
- name: Upload Playwright report
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-${{ matrix.boundary_edition }}
name: playwright-report-${{ matrix.boundary_edition }}-${{ matrix.browser }}
path: e2e-tests/playwright-report
- name: Clean up test infra
@@ -242,11 +330,11 @@ jobs:
enos scenario destroy --timeout 60m0s --chdir ./support/src/${{matrix.folder_name}}/enos ${{matrix.enos_scenario}} builder:local
- name: Send Slack message on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui-enterprise' }}
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: ":x: admin ui e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref }}"
text: ":x: admin ui e2e tests (${{ matrix.boundary_edition }}:${{ matrix.browser }}) failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref || github.ref_name }}"
+19 -25
View File
@@ -3,13 +3,6 @@ name: Test E2E Desktop Client
on:
pull_request:
types: [labeled, opened, synchronize, reopened]
paths:
- 'ui/desktop/**'
- 'e2e-tests/*'
- 'e2e-tests/api-client/**'
- 'e2e-tests/helpers/**'
- 'e2e-tests/desktop/**'
- '.github/workflows/test-e2e-desktop.yaml'
workflow_dispatch:
inputs:
boundary-enterprise-branch:
@@ -27,9 +20,10 @@ jobs:
test:
name: test
# don't need to run e2e tests on ce -> ent merges since tests were already
# run in the ce branch, and there are currently no code diffferences between
# run in the ce branch, and there are currently no code differences between
# the two repos
if: github.actor != 'hc-github-team-secure-boundary'
# desktop client e2e tests only run enterprise tests, so only run in boundary-ui-enterprise
if: github.actor != 'hc-github-team-secure-boundary' && github.repository == 'hashicorp/boundary-ui-enterprise'
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
env:
ENOS_VAR_boundary_edition: "ent"
@@ -39,25 +33,25 @@ jobs:
with:
fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: |
node-version: 24
- name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: |
pnpm-lock.yaml
ui/desktop/electron-app/pnpm-lock.yaml
run_install: false
- name: Set up Terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e
- name: Set up Enos
uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a
uses: hashicorp/action-setup-enos@6ec106c8f809fe645162d73bea565c65f3269907
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -113,7 +107,7 @@ jobs:
echo "go-version=$(cat ./support/src/boundary-enterprise/.go-version)" >> "$GITHUB_OUTPUT"
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ steps.get-go-version.outputs.go-version }}
cache: false
@@ -166,7 +160,7 @@ jobs:
- name: Upload Playwright report
if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: e2e-tests/playwright-report
@@ -180,11 +174,11 @@ jobs:
enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:local
- name: Send Slack message on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }}
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui-enterprise' }}
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: ":x: desktop client e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref }}"
text: ":x: desktop client e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref || github.ref_name }}"
+36 -4
View File
@@ -131,11 +131,34 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
// which indicates that there are more items in the list
// or the batch size has not been reached
do {
result = await super.query(store, schema, queryObject);
//add the result items to a data array
try {
result = await super.query(store, schema, queryObject);
} catch (e) {
// If we hit a rate limit mid-pagination, return whatever
// data has been accumulated so far rather than throwing and leaving
// the user stuck with nothing to show.
// Without a list_token this is the very first request with nothing to
// show, so rethrow and let normal error handling apply.
if (e?.errors?.[0]?.isRateLimited && queryObject.list_token) {
result ??= {};
result.items = data;
result.response_type = 'complete';
result.meta = {
...result.meta,
rateLimitWarning: { retryAfter: e.errors[0].retryAfter },
};
return prenormalizeArrayResponse(result);
}
// For all other errors (or a 429 on the very first request with
// no data yet), rethrow so normal error handling applies.
throw e;
}
// Add the result items to a data array
if (result && result.items) {
data.push(...result.items);
//pass in the list token for subsequent calls to fetch the remaining list items
// Pass in the list token for subsequent calls to fetch the remaining list items
queryObject.list_token = result.list_token;
}
} while (
@@ -237,6 +260,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* - `403 isForbidden`: the session is authenticated but does not have
* permission to perform the requested action
* - `404 isNotFound`: the requested resource could not be found
* - `429 isRateLimited`: the client has exceeded the rate limit
* - `500 isServer`: an internal server error occurred
* - `isUnknown`: an error occurred, but we don't know which or
* we don't distinguish it yet
@@ -248,7 +272,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* @param {Object} payload
* @return {Array} errors payload
*/
normalizeErrorResponse(/*status, headers, payload*/) {
normalizeErrorResponse(status, headers /*, payload*/) {
const errors = super.normalizeErrorResponse(...arguments);
if (isArray(errors)) {
errors.forEach((error) => {
@@ -262,6 +286,14 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
case 404:
error.isNotFound = true;
break;
case 429:
error.isRateLimited = true;
// Attach rate-limit metadata from response headers so downstream
// error handlers can inform the user when they may retry.
error.retryAfter = Number(
headers['Retry-After'] ?? headers['retry-after'],
);
break;
case 500:
error.isServer = true;
break;
@@ -96,19 +96,19 @@ export default class GeneratedSessionRecordingModel extends BaseModel {
})
duration;
@attr('string', {
@attr({
description:
'The current state of the session recording. One of "started", "available" and "unknown".',
'The detailed state of the session recording. This contains storage_state, syncing_error_details, and verification_error_details.',
readOnly: true,
})
state;
recording_state;
@attr('string', {
description:
'Any error seen during the closing of the session recording. Currently only set if state is "unknown".',
'The availability of the session recording. One of "available" or "not available".',
readOnly: true,
})
error_details;
availability;
@attr('date', {
description: 'The time until a session recording is required to be stored.',
@@ -122,4 +122,19 @@ export default class GeneratedSessionRecordingModel extends BaseModel {
readOnly: true,
})
delete_after;
// Deprecated: These two fields should be removed once support for RDP is added.
@attr('string', {
description:
'The current state of the session recording. One of "started", "available" and "unknown".',
readOnly: true,
})
state;
@attr('string', {
description:
'Any error seen during the closing of the session recording. Currently only set if state is "unknown".',
readOnly: true,
})
error_details;
}
+43 -9
View File
@@ -59,17 +59,39 @@ export default class SqliteHandler {
let payload,
listToken,
writeToDbPromise,
totalInsert = 0;
totalInsert = 0,
rateLimitWarning;
if (!peekDb) {
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
let tokenKey = `${type}-${hashCode(remainingQuery)}`;
// The global token key represents a recursive query from global scope,
// which pulls back every resource of this type.
const globalTokenKey = `${type}-${hashCode({ scope_id: 'global', recursive: true })}`;
if (storeToken) {
const [tokenObj] = await this.sqlite.fetchResource({
...generateSQLExpressions('token', {
filters: { id: [{ equals: tokenKey }] },
}),
});
listToken = tokenObj?.token;
// Always prefer the global token when it exists for
// any more fine-grained query. The global query already fetched every
// item for this resource type, so we should just re-use it.
if (tokenKey !== globalTokenKey) {
const [globalTokenObj] = await this.sqlite.fetchResource({
...generateSQLExpressions('token', {
filters: { id: [{ equals: globalTokenKey }] },
}),
});
if (globalTokenObj?.token) {
tokenKey = globalTokenKey;
listToken = globalTokenObj.token;
}
}
// Only look up the specific token if no global token was found.
if (!listToken) {
const [tokenObj] = await this.sqlite.fetchResource({
...generateSQLExpressions('token', {
filters: { id: [{ equals: tokenKey }] },
}),
});
listToken = tokenObj?.token;
}
} else {
// This is a temporary fix of clearing the DB (specifically for auth-methods)
// since we are not storing the token we do not get back a list of removed_ids
@@ -86,6 +108,11 @@ export default class SqliteHandler {
});
totalInsert += payload.items?.length ?? 0;
// Propagate any rate-limit warning the adapter attached
if (payload.meta?.rateLimitWarning) {
rateLimitWarning = payload.meta.rateLimitWarning;
}
// await the previous writeToDbPromise before writing to db again
if (writeToDbPromise) {
await writeToDbPromise;
@@ -163,7 +190,14 @@ export default class SqliteHandler {
// This isn't conventional but is better than returning an ArrayProxy
// or EmberArray since the ember store query method asserts it has to be an array
// so we can't just return an object.
records.meta = { totalItems: count[0].total };
records.meta = {
totalItems: count[0].total,
};
if (rateLimitWarning) {
records.meta.rateLimitWarning = rateLimitWarning;
}
return records;
}
default:
@@ -48,6 +48,18 @@ export default class AuthMethodSerializer extends ApplicationSerializer {
} else {
delete serialized.attributes.state;
}
// Don't send account_claim_maps on update if there's a saved 'sub' mapping
// The API doesn't allow updating when 'sub' claim mappings exist
if (!snapshot.record.isNew && snapshot.record.account_claim_maps) {
const hasSubClaimMap = snapshot.record.account_claim_maps.some(
(item) => item?.value === 'sub',
);
if (hasSubClaimMap) {
delete serialized.attributes.account_claim_maps;
}
}
return serialized;
}
+58 -16
View File
@@ -46,6 +46,15 @@ import {
// See https://sqlite.org/rescode.html#corrupt
const SQLITE_CORRUPT = 11;
/**
* Throws if the database has not been initialized or has been closed.
*/
const requireDb = () => {
if (!db) {
throw new Error('Database is not available');
}
};
const methods = {
initializeSQLite: async () => {
sqlite3 = await sqlite3InitModule({
@@ -57,6 +66,9 @@ import {
});
function setupDb() {
// Allow SQLite to wait up to 5 s when it encounters a busy lock
db.exec('PRAGMA busy_timeout = 5000;');
const [row] = db.exec('PRAGMA user_version;', { rowMode: 'object' });
// Check if we're on a newer schema version and clear the database if so
if (SCHEMA_VERSION > row.user_version) {
@@ -97,30 +109,40 @@ import {
if (poolUtil.getCapacity() > 20) {
await poolUtil.removeVfs();
}
} else {
} else if (
err?.resultCode === SQLITE_CORRUPT ||
err instanceof WebAssembly.RuntimeError
) {
// We might have gotten ourselves into a bad state which corrupted the DB so clear it out and retry.
console.error('SQLite initialization error:', err);
// We might have gotten ourselves into a bad state which corrupted the DB so clear it out and retry
if (err?.resultCode === SQLITE_CORRUPT) {
try {
if (db) {
db.close();
db = null;
}
poolUtil.unlink(`/${dbName}`);
} else {
throw err;
} catch (cleanupErr) {
// If cleanup fails, wipe the entire VFS and reinstall to start fresh
console.error('SQLite cleanup error:', cleanupErr);
await poolUtil.removeVfs();
}
} else {
throw err;
}
}
}
},
analyzeDatabase: () => {
requireDb();
db.exec('PRAGMA optimize');
},
clearDatabase: () => {
requireDb();
db.exec(CLEAR_DB);
db.exec(CREATE_TABLES(SCHEMA_VERSION));
},
deleteDatabase: () => {
if (!db) {
throw new Error('No database was initialized');
}
requireDb();
const name = db.dbFilename();
db.close();
@@ -128,9 +150,11 @@ import {
poolUtil.unlink(`/${name}`);
},
downloadDatabase: () => {
requireDb();
return sqlite3.capi.sqlite3_js_db_export(db);
},
fetchResource: ({ sql, parameters = [] }) => {
requireDb();
try {
return db.exec({
sql,
@@ -147,6 +171,7 @@ import {
if (items.length === 0) {
return [];
}
requireDb();
const numberOfParameters = items.reduce((acc, item) => {
return acc + item.length;
@@ -155,11 +180,15 @@ import {
// We want to use as many VALUES as part of the insert statement as it's quicker
// than doing multiple inserts. If we have too many rows to insert, we'll manually
// chunk them up to avoid hitting the maximum number of host parameters
// and execute these separate insert statements in a transaction
// and execute these separate insert statements in a transaction.
if (numberOfParameters > MAX_HOST_PARAMETERS) {
const chunkSize = Math.floor(MAX_HOST_PARAMETERS / items[0].length);
return db.transaction(() => {
// Note: We use explicit BEGIN/COMMIT instead of db.transaction() because
// db.transaction() uses SAVEPOINT internally, which fails with SQLITE_BUSY
// when any statement is active on the connection.
db.exec('BEGIN');
try {
const results = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
@@ -170,20 +199,29 @@ import {
});
results.push(result);
}
db.exec('COMMIT');
return results;
});
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
}
db.exec({
return db.exec({
sql: INSERT_STATEMENTS(resource, items, modelMapping),
bind: items.flat(),
rowMode: 'object',
});
},
deleteResource: ({ resource, ids }) => {
// Check if we have too many parameters for the deletion and chunk if so
requireDb();
// Check if we have too many parameters for the deletion and chunk if so.
if (ids?.length > MAX_HOST_PARAMETERS) {
return db.transaction(() => {
// Note: We use explicit BEGIN/COMMIT instead of db.transaction() because
// db.transaction() uses SAVEPOINT internally, which fails with SQLITE_BUSY
// when any statement is active on the connection.
db.exec('BEGIN');
try {
const results = [];
for (let i = 0; i < ids.length; i += MAX_HOST_PARAMETERS) {
const chunk = ids.slice(i, i + MAX_HOST_PARAMETERS);
@@ -194,8 +232,12 @@ import {
});
results.push(result);
}
db.exec('COMMIT');
return results;
});
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
}
return db.exec({
@@ -169,6 +169,12 @@ const onBecomeProvider = async ({
target,
status,
}) => {
// Create a gate that prevents request handling until the provider is fully initialized.
// This prevents a race condition where a client registers and sends requests before
// the provider has finished initialization.
const { promise: providerReady, resolve: providerReadyResolve } =
Promise.withResolvers();
// Set an event listener for other tabs to register with the provider tab
commonChannel.addEventListener('message', async (event) => {
const { clientId, type } = event.data;
@@ -194,6 +200,9 @@ const onBecomeProvider = async ({
}
const { method, args, id } = data;
// Wait for the provider to finish initialization before handling requests
await providerReady;
let result, error;
try {
result = await target[method](...args);
@@ -221,6 +230,7 @@ const onBecomeProvider = async ({
});
await onProviderChange?.(status.isServiceProvider);
providerReadyResolve();
commonChannel.postMessage({ type: 'providerChange', serviceName });
// If we became the new provider and there were requests in flight, request it directly again.
+5
View File
@@ -11,6 +11,7 @@ import initializeMockDesktopContextBridgeAPI from './scenarios/ipc/desktop-conte
import makeBooleanFilter from './helpers/bexpr-filter';
import { faker } from '@faker-js/faker';
import { asciicasts } from './data/asciicasts';
import { grantsSchemaData } from './data/grants-schema';
import { TYPE_WORKER_PKI } from 'api/models/worker';
import environmentConfig from 'ember-get-config';
@@ -210,6 +211,10 @@ function routes() {
return metadata;
});
this.get('/internal/grants-schema.json', () => {
return grantsSchemaData;
});
// make this `/api`, for example, if your API is namespaced
this.namespace = environmentConfig.api?.namespace;
// delay for each request, automatically set to 0 during testing
+648
View File
@@ -0,0 +1,648 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export const grantsSchemaData = {
resource_types: [
{
type: 'account',
collection_actions: ['create', 'list'],
id_actions: [
'read',
'update',
'delete',
'set-password',
'change-password',
],
scopes: ['global', 'org'],
id_prefixes: ['apw', 'acctldap', 'acctpw', 'acctoidc'],
parent_type: 'auth-method',
output_fields: [
'id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'auth_method_id',
'attributes',
'password_account_attributes',
'oidc_account_attributes',
'ldap_account_attributes',
'managed_group_ids',
'authorized_actions',
],
},
{
type: 'alias',
collection_actions: ['create', 'list'],
id_actions: ['read', 'update', 'delete'],
scopes: ['global'],
id_prefixes: ['alt'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'value',
'destination_id',
'type',
'attributes',
'target_alias_attributes',
'authorized_actions',
],
},
{
type: 'auth-method',
collection_actions: ['create', 'list'],
id_actions: ['authenticate', 'read', 'update', 'delete', 'change-state'],
scopes: ['global', 'org'],
id_prefixes: ['amoidc', 'amldap', 'ampw'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'attributes',
'password_auth_method_attributes',
'oidc_auth_methods_attributes',
'ldap_auth_methods_attributes',
'is_primary',
'authorized_actions',
'authorized_collection_actions',
],
},
{
type: 'auth-token',
collection_actions: ['list'],
id_actions: ['read', 'read:self', 'delete', 'delete:self'],
scopes: ['global', 'org'],
id_prefixes: ['at'],
output_fields: [
'id',
'scope_id',
'scope',
'token',
'user_id',
'auth_method_id',
'account_id',
'created_time',
'updated_time',
'approximate_last_used_time',
'expiration_time',
'authorized_actions',
],
},
{
type: 'billing',
collection_actions: ['monthly-active-users'],
scopes: ['global'],
},
{
type: 'credential',
collection_actions: ['create', 'list'],
id_actions: ['delete', 'read', 'update'],
scopes: ['project'],
id_prefixes: [
'credp',
'cdvlt',
'credup',
'cred',
'credjson',
'credupd',
'credspk',
],
parent_type: 'credential-store',
output_fields: [
'id',
'credential_store_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'attributes',
'username_password_attributes',
'ssh_private_key_attributes',
'json_attributes',
'username_password_domain_attributes',
'password_attributes',
'authorized_actions',
],
},
{
type: 'credential-library',
collection_actions: ['create', 'list'],
id_actions: ['delete', 'read', 'update'],
scopes: ['project'],
id_prefixes: ['clvlt', 'clvllt', 'clvsclt'],
parent_type: 'credential-store',
output_fields: [
'id',
'credential_store_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'attributes',
'vault_credential_library_attributes',
'vault_ssh_certificate_credential_library_attributes',
'vault_generic_credential_library_attributes',
'vault_ldap_credential_library_attributes',
'authorized_actions',
'credential_type',
'credential_mapping_overrides',
],
},
{
type: 'credential-store',
collection_actions: ['create', 'list'],
id_actions: ['read', 'update', 'delete'],
scopes: ['project'],
id_prefixes: ['csvlt', 'csst', 'cs'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'attributes',
'vault_credential_store_attributes',
'authorized_actions',
'authorized_collection_actions',
],
},
{
type: 'group',
collection_actions: ['create', 'list'],
id_actions: [
'update',
'delete',
'add-members',
'set-members',
'remove-members',
'read',
],
scopes: ['global', 'org', 'project'],
id_prefixes: ['g'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'member_ids',
'members',
'authorized_actions',
],
},
{
type: 'host',
collection_actions: ['create', 'list'],
id_actions: ['read', 'update', 'delete'],
scopes: ['project'],
id_prefixes: ['hst', 'hplg', 'h'],
parent_type: 'host-catalog',
output_fields: [
'id',
'host_catalog_id',
'scope',
'plugin',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'host_set_ids',
'attributes',
'static_host_attributes',
'ip_addresses',
'dns_names',
'external_id',
'external_name',
'authorized_actions',
],
},
{
type: 'host-catalog',
collection_actions: ['create', 'list'],
id_actions: ['read', 'update', 'delete'],
scopes: ['project'],
id_prefixes: ['hc', 'hcplg', 'hcst'],
output_fields: [
'id',
'scope_id',
'scope',
'plugin_id',
'plugin',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'attributes',
'secrets',
'secrets_hmac',
'worker_filter',
'authorized_actions',
'authorized_collection_actions',
],
},
{
type: 'host-set',
collection_actions: ['create', 'list'],
id_actions: [
'read',
'update',
'delete',
'add-hosts',
'set-hosts',
'remove-hosts',
],
scopes: ['project'],
id_prefixes: ['hsst', 'hsplg', 'hs'],
parent_type: 'host-catalog',
output_fields: [
'id',
'host_catalog_id',
'scope',
'plugin',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'host_ids',
'preferred_endpoints',
'sync_interval_seconds',
'attributes',
'authorized_actions',
],
},
{
type: 'managed-group',
collection_actions: ['create', 'list'],
id_actions: ['read', 'update', 'delete'],
scopes: ['global', 'org'],
id_prefixes: ['mgoidc', 'mgldap'],
parent_type: 'auth-method',
output_fields: [
'id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'auth_method_id',
'attributes',
'oidc_managed_group_attributes',
'ldap_managed_group_attributes',
'member_ids',
'authorized_actions',
],
},
{
type: 'policy',
collection_actions: ['create', 'list'],
id_actions: ['read', 'update', 'delete'],
scopes: ['global', 'org'],
id_prefixes: ['pst'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'type',
'version',
'attributes',
'storage_policy_attributes',
'authorized_actions',
],
},
{
type: 'role',
collection_actions: ['create', 'list'],
id_actions: [
'remove-principals',
'add-grants',
'set-grants',
'remove-grants',
'delete',
'add-grant-scopes',
'set-grant-scopes',
'remove-grant-scopes',
'read',
'update',
'add-principals',
'set-principals',
],
scopes: ['global', 'org', 'project'],
id_prefixes: ['r'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'grant_scope_ids',
'principal_ids',
'principals',
'grant_strings',
'grants',
'authorized_actions',
],
},
{
type: 'scope',
collection_actions: [
'create',
'list',
'list-keys',
'rotate-keys',
'list-key-version-destruction-jobs',
'destroy-key-version',
],
id_actions: [
'read',
'update',
'delete',
'attach-storage-policy',
'detach-storage-policy',
'set-alias-suffix',
'remove-alias-suffix',
],
scopes: ['global', 'org', 'project'],
id_prefixes: ['global', 'p', 'o'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'primary_auth_method_id',
'authorized_actions',
'authorized_collection_actions',
'storage_policy_id',
],
},
{
type: 'session',
collection_actions: ['list'],
id_actions: ['cancel:self', 'read', 'read:self', 'cancel'],
scopes: ['project'],
id_prefixes: ['s'],
output_fields: [
'id',
'target_id',
'scope',
'created_time',
'updated_time',
'version',
'type',
'expiration_time',
'auth_token_id',
'user_id',
'host_set_id',
'host_id',
'scope_id',
'endpoint',
'states',
'status',
'certificate',
'termination_reason',
'authorized_actions',
'connections',
],
},
{
type: 'session-recording',
collection_actions: ['list', 'list-exports'],
id_actions: [
'reapply-storage-policy',
'export',
'export:cancel',
'read',
'download',
'delete',
],
scopes: ['global', 'org'],
id_prefixes: ['sr'],
output_fields: [
'id',
'scope',
'session_id',
'storage_bucket_id',
'bytes_up',
'bytes_down',
'created_time',
'updated_time',
'start_time',
'end_time',
'duration',
'type',
'state',
'error_details',
'mime_types',
'endpoint',
'connection_recordings',
'create_time_values',
'authorized_actions',
'retain_until',
'delete_after',
'availability',
'recording_state',
],
},
{
type: 'storage-bucket',
collection_actions: ['list', 'create'],
id_actions: ['delete', 'read', 'update'],
scopes: ['global', 'org'],
id_prefixes: ['sb'],
output_fields: [
'id',
'scope_id',
'scope',
'plugin_id',
'plugin',
'name',
'description',
'bucket_name',
'bucket_prefix',
'created_time',
'updated_time',
'version',
'type',
'attributes',
'secrets',
'secrets_hmac',
'worker_filter',
'storage_bucket_credential_id',
'authorized_actions',
],
},
{
type: 'target',
collection_actions: ['create', 'list'],
id_actions: [
'delete',
'add-host-sources',
'set-host-sources',
'remove-host-sources',
'add-credential-sources',
'read',
'update',
'set-credential-sources',
'remove-credential-sources',
'authorize-session',
],
scopes: ['project'],
id_prefixes: ['trdp', 'tssh', 'ttcp'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'type',
'host_source_ids',
'host_sources',
'session_max_seconds',
'session_connection_limit',
'worker_filter',
'egress_worker_filter',
'ingress_worker_filter',
'brokered_credential_source_ids',
'brokered_credential_sources',
'injected_application_credential_source_ids',
'injected_application_credential_sources',
'attributes',
'tcp_target_attributes',
'ssh_target_attributes',
'rdp_target_attributes',
'authorized_actions',
'address',
'aliases',
'with_aliases',
],
},
{
type: 'user',
collection_actions: ['create', 'list'],
id_actions: [
'delete',
'add-accounts',
'set-accounts',
'remove-accounts',
'list-resolvable-aliases',
'read',
'update',
],
scopes: ['global', 'org'],
id_prefixes: ['u'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'account_ids',
'accounts',
'authorized_actions',
'login_name',
'full_name',
'email',
'primary_account_id',
],
},
{
type: 'worker',
collection_actions: [
'create:worker-led',
'list',
'read-certificate-authority',
'reinitialize-certificate-authority',
'create:controller-led',
],
id_actions: [
'remove-worker-tags',
'read',
'update',
'delete',
'add-worker-tags',
'set-worker-tags',
],
scopes: ['global'],
id_prefixes: ['w'],
output_fields: [
'id',
'scope_id',
'scope',
'name',
'description',
'created_time',
'updated_time',
'version',
'address',
'canonical_tags',
'config_tags',
'last_status_time',
'worker_generated_auth_token',
'controller_generated_activation_token',
'active_connection_count',
'type',
'api_tags',
'release_version',
'directly_connected_downstream_workers',
'authorized_actions',
'local_storage_state',
'remote_storage_state',
],
},
],
};
@@ -45,6 +45,12 @@ export default factory.extend({
username: faker.internet.userName(),
key_bits: faker.number.int(999),
path: faker.system.directoryPath(),
critical_options: {
[faker.word.noun()]: faker.word.words(2),
},
extensions: {
[faker.word.noun()]: faker.word.words(2),
},
};
case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
return {
+1 -1
View File
@@ -26,7 +26,7 @@ export default factory.extend({
'set-grant-scopes',
],
grant_strings: () => [
'ids=*;action=*',
'ids=*;actions=*',
'ids=*;type=host-catalog;actions=create,read',
],
@@ -10,8 +10,8 @@ import { faker } from '@faker-js/faker';
import { trait } from 'miragejs';
import {
TYPES_SESSION_RECORDING as types,
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_AVAILABLE,
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_UNKNOWN,
} from 'api/models/session-recording';
@@ -27,6 +27,29 @@ export default factory.extend({
// Cycle through available types
type: (i) => types[i % types.length],
state: (i) => states[i % states.length],
recording_state(i) {
// Only return error details if state is 'unknown' (failed)
if (this.state !== STATE_SESSION_RECORDING_UNKNOWN) {
return {};
}
const errorScenarios = [
{
syncing_error_details:
'sync errors in container "sr_jovU2XLzBP.bsr": file "sr_jovU2XLzBP.bsr/session-recording.meta": storage.(syncingFile).sync: storage.parseStoragePluginError: plugin error, external system issue: error #3001: rpc error: \ncode = Internal desc = failed to put object into minio: Get "http://minio:9000/my-special-storage-bucket/?location=": dial tcp: lookup minio on 10.89.0.1:53: no such host',
verification_error_details:
'integrity check failed for session recording file "sr_jovU2XLzBP.bsr/channels/connection-1/channel-recording-data.bin": expected SHA-256 checksum "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6" but computed \nchecksum was "z6y5x4w3v2u1t0s9r8q7p6o5n4m3l2k1j0i9h8g7f6e5d4c3b2a1" indicating potential data corruption during transfer or storage',
},
{
syncing_error_details:
'sync errors in container "sr_jovU2XLzBP.bsr": file "sr_jovU2XLzBP.bsr/session-recording.meta": storage.(syncingFile).sync: storage.parseStoragePluginError: plugin error, external system issue: error #3001: rpc error: code = Internal desc = failed to put object into minio: Get "http://minio:9000/my-special-storage-bucket/?location=": dial tcp: lookup minio on 10.89.0.1:53: no such host',
verification_error_details: null,
},
];
return errorScenarios[i % 2];
},
start_time() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE
? faker.date.recent({ days: 3, refDate: this.end_time })
+5 -1
View File
@@ -9,7 +9,11 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
module.exports = function (defaults) {
const app = new EmberAddon(defaults, {
// Add options here
babel: {
plugins: [
require.resolve('ember-concurrency/async-arrow-task-transform'),
],
},
});
/*
+5
View File
@@ -18,6 +18,11 @@ module.exports = {
name: require('./package').name,
options: {
babel: {
plugins: [
require.resolve('ember-concurrency/async-arrow-task-transform'),
],
},
// Don't automatically import sqlite, we need to manually control how it gets used
autoImport: {
exclude: ['@sqlite.org/sqlite-wasm'],
+5 -4
View File
@@ -34,8 +34,9 @@
"broccoli-source": "^3.0.1",
"ember-auto-import": "^2.10.0",
"ember-can": "^8.0.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-babel": "^8.3.1",
"ember-cli-htmlbars": "^6.3.0",
"ember-concurrency": "^4.0.3",
"ember-data": "~5.3.12",
"ember-get-config": "^2.1.1",
"ember-inflector": "^6.0.0",
@@ -43,7 +44,7 @@
"promise-worker-bi": "^5.0.1",
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
"tracked-built-ins": "^4.0.0",
"uuid": "^11.0.3"
"uuid": "^14.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.25.1",
@@ -55,7 +56,7 @@
"@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0",
"concurrently": "^10.0.3",
"doctoc": "^2.2.0",
"ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0",
@@ -92,7 +93,7 @@
"sinon": "^19.0.2"
},
"engines": {
"node": "20.* || 22.*"
"node": "22.* || 24.*"
},
"ember": {
"edition": "octane"
+1 -1
View File
@@ -30,7 +30,7 @@ module.exports = function (environment) {
},
mirage: {
enabled: true
enabled: true,
},
};
@@ -175,11 +175,46 @@ module('Unit | Adapter | application', function (hooks) {
assert.ok(getResponse(401).errors[0].isUnauthenticated);
assert.ok(getResponse(403).errors[0].isForbidden);
assert.ok(getResponse(404).errors[0].isNotFound);
assert.ok(getResponse(429).errors[0].isRateLimited);
assert.ok(getResponse(500).errors[0].isServer);
assert.ok(getResponse(0).errors[0].isUnknown);
assert.ok(getResponse(false).errors[0].isUnknown);
});
test('it attaches retryAfter from Retry-After header on 429 responses', function (assert) {
const adapter = this.owner.lookup('adapter:application');
const headers = { 'Retry-After': '30' };
const response = adapter.handleResponse(429, headers, { status: 429 });
assert.ok(response.errors[0].isRateLimited);
assert.strictEqual(response.errors[0].retryAfter, 30);
});
test('it returns partial data when rate limited mid-pagination', async function (assert) {
const adapter = this.owner.lookup('adapter:application');
const store = this.owner.lookup('service:store');
let requestCount = 0;
this.server.get('/groups', () => {
requestCount++;
if (requestCount === 1) {
return {
items: [{ id: 'g_1', scope_id: 'p_1' }],
response_type: 'delta',
list_token: 'token123',
};
}
// Second request returns a 429
return new Response(429, { 'Retry-After': '10' }, { status: 429 });
});
const result = await adapter.query(
store,
{ modelName: 'group' },
{ scope_id: 'p_1' },
);
assert.strictEqual(result.items.length, 1);
assert.strictEqual(result.meta.rateLimitWarning.retryAfter, 10);
});
test('it returns field-level errors in InvalidError from handleResponse', function (assert) {
const adapter = this.owner.lookup('adapter:application');
const payload = {
@@ -470,6 +470,62 @@ module('Unit | Handler | sqlite-handler', function (hooks) {
assert.strictEqual(results[0].name, 'specific-target');
});
test('it reuses the global token for a more specific query when the global token exists', async function (assert) {
const targets = this.server.createList('target', 3, { scope });
let capturedListToken;
let responseToken = 'specific-list-token';
this.server.get('targets', (_schema, request) => {
capturedListToken = request.queryParams.list_token;
return {
items: targets.map((t) =>
this.server.serializerOrRegistry.serialize(t),
),
list_token: responseToken,
response_type: 'complete',
};
});
// Run the specific query first
await store.query('target', { scope_id: 'some-specific-scope' });
// Switch the response token and run the global query
responseToken = 'global-list-token';
await store.query('target', { scope_id: 'global', recursive: true });
capturedListToken = undefined;
// Re-run the specific query which has a cached token but should prefer the global token since it exists
await store.query('target', { scope_id: 'some-specific-scope' });
assert.strictEqual(capturedListToken, 'global-list-token');
});
test('it uses the specific token when no global token exists', async function (assert) {
const targets = this.server.createList('target', 3, { scope });
let capturedListToken;
this.server.get('targets', (_schema, request) => {
capturedListToken = request.queryParams.list_token;
return {
items: targets.map((t) =>
this.server.serializerOrRegistry.serialize(t),
),
list_token: 'specific-list-token',
response_type: 'complete',
};
});
await store.query('target', { scope_id: 'some-specific-scope' });
capturedListToken = undefined;
// Re-run the specific query which should just use the normal specific token since no global token exists
await store.query('target', { scope_id: 'some-specific-scope' });
assert.strictEqual(capturedListToken, 'specific-list-token');
});
test('it retries query when invalid list token error occurs', async function (assert) {
let targets = this.server.createList('target', 5, { scope });
@@ -506,4 +506,90 @@ module('Unit | Serializer | auth method', function (hooks) {
assert.strictEqual(record.client_certificate_key, '');
assert.strictEqual(record.bind_password, '');
});
test('it includes account_claim_maps when serializing new OIDC records', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('auth-method');
const record = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
name: 'Test OIDC',
account_claim_maps: [
{ key: 'email', value: 'email' },
{ key: 'name', value: 'sub' },
],
});
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.ok(
serializedRecord.attributes.account_claim_maps,
'account_claim_maps is included for new records',
);
assert.deepEqual(serializedRecord.attributes.account_claim_maps, [
'email=email',
'name=sub',
]);
});
test('it excludes account_claim_maps when serializing existing OIDC records with "sub" mapping', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('auth-method');
const record = store.push({
data: {
id: 'oidc123',
type: 'auth-method',
attributes: {
type: TYPE_AUTH_METHOD_OIDC,
name: 'Test OIDC',
account_claim_maps: [
{ key: 'email', value: 'email' },
{ key: 'name', value: 'sub' },
],
},
},
});
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.notOk(
serializedRecord.attributes.account_claim_maps,
'account_claim_maps is excluded when updating with "sub" mapping',
);
});
test('it includes account_claim_maps when serializing existing OIDC records without "sub" mapping', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('auth-method');
const record = store.push({
data: {
id: 'oidc456',
type: 'auth-method',
attributes: {
type: TYPE_AUTH_METHOD_OIDC,
name: 'Test OIDC',
account_claim_maps: [
{ key: 'email', value: 'email' },
{ key: 'name', value: 'name' },
],
},
},
});
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.ok(
serializedRecord.attributes.account_claim_maps,
'account_claim_maps is included when updating without "sub" mapping',
);
assert.deepEqual(serializedRecord.attributes.account_claim_maps, [
'email=email',
'name=name',
]);
});
});
+3 -3
View File
@@ -28,7 +28,7 @@
"@babel/core": "^7.26.10",
"@ember/test-waiters": "^4.1.0",
"ember-auto-import": "^2.10.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-babel": "^8.3.1",
"ember-cli-htmlbars": "^6.3.0",
"ember-simple-auth": "^6.1.0"
},
@@ -41,7 +41,7 @@
"@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0",
"concurrently": "^10.0.3",
"doctoc": "^2.2.0",
"ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0",
@@ -79,7 +79,7 @@
"ember-source": ">= 4.0.0"
},
"engines": {
"node": "20.* || 22.*"
"node": "22.* || 24.*"
},
"ember": {
"edition": "octane"
@@ -12,6 +12,7 @@ import Component from '@glimmer/component';
* - 403 - `error.isForbidden`: the session is authenticated but does not have
* permission to perform the requested action
* - 404 - `error.isNotFound`: the requested resource could not be found
* - 429 - `error.isRateLimited`: the client has exceeded the rate limit
* - 500 - `error.isServer`: an internal server error occurred
*
* For an unknown error state, i.e error state not matching to the above defined list:
@@ -19,7 +20,7 @@ import Component from '@glimmer/component';
* we don't distinguish it yet
*/
const statuses = ['401', '403', '404', '500'];
const statuses = ['401', '403', '404', '429', '500'];
export default class ErrorMessageComponent extends Component {
// =methods
+3 -3
View File
@@ -37,7 +37,7 @@
"auth": "workspace:*",
"ember-auto-import": "^2.10.0",
"ember-browser-services": "^4.0.3",
"ember-cli-babel": "^8.2.0",
"ember-cli-babel": "^8.3.1",
"ember-cli-htmlbars": "^6.3.0",
"ember-cli-sass": "^11.0.1",
"ember-concurrency": "^4.0.3",
@@ -59,7 +59,7 @@
"@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0",
"concurrently": "^10.0.3",
"doctoc": "^2.2.0",
"ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0",
@@ -95,7 +95,7 @@
"webpack": "^5.105.4"
},
"engines": {
"node": "20.* || 22.*"
"node": "22.* || 24.*"
},
"ember": {
"edition": "octane"
@@ -52,6 +52,16 @@ module('Integration | Component | error/message', function (hooks) {
.hasText(
'We could not find the requested resource. You can ask your administrator or try again later.',
);
await render(hbs`<Error::Message @status='429' />`);
assert.dom(ERROR_MSG_TITLE).hasText('Rate limit exceeded');
assert.dom(ERROR_MSG_CODE).hasText('ERROR 429');
assert
.dom(ERROR_MSG_BODY)
.hasText(
"You've exceeded the rate limit. Please wait a moment and try again.",
);
});
test('it renders default error for unknown error status', async function (assert) {
+3 -1
View File
@@ -23,13 +23,13 @@ retry: Retry
play: Play
pause: Pause
resume: Resume
export: Export
get-topic-help: Get help for this topic
copy-to-clipboard: Copy to Clipboard
copy-error-detail-to-clipboard: Copy error detail to clipboard
authenticate: Sign In
deauthenticate: Sign Out
choose-org: Choose Scope
choose-different-org: Choose a different scope
add-host-set: Create Host Set
add-host: Create Host
add-principals: Add Principals
@@ -69,3 +69,5 @@ hide-errors: Hide Errors
edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter
open: Open
download: Download
show-more: Show more
+1
View File
@@ -24,6 +24,7 @@ titles:
empty-set: No Items Yet
global: Global
actions: Actions
action: Action
documentation: Documentation
abandon-confirm: Discard unsaved changes?
welcome-to-boundary: Welcome to Boundary
@@ -13,6 +13,11 @@
500:
title: Server error
description: We ran into a problem and could not continue. You can ask your administrator or try again later.
429:
title: Rate limit exceeded
description: You've exceeded the rate limit. Please wait a moment and try again.
description-partial: Currently showing partial results. You can reload for more in {seconds} {seconds, plural, one {second} other {seconds}}.
description-ready: Currently showing partial results. You can reload now.
unknown:
title: Something went wrong
description: We're not sure what happened. Please contact your administrator or try again later.
+1
View File
@@ -177,6 +177,7 @@ sync-interval:
tags:
label: Tags
secret-editor:
label: Secret Editor
details: This secret is saved but won't be displayed
edit: Click to replace this secret
created:
+196 -5
View File
@@ -420,6 +420,13 @@ session-recording:
title: Channels by connection
messages:
no-channels: There are no channels for this connection
flyout:
title: Session playback errors
sync-error:
title: This recording is not available
flyout-section-title: Sync error
verification-error:
flyout-section-title: Verification error
started-no-connections:
title: Session Recording In Progress
description: The Session has started but no connections have been made.
@@ -611,6 +618,7 @@ role:
title: Grant
title_plural: Grants
description: Grants are permissions which allow roles to take actions and access resources.
codeblock-description: Select "{editGrantsTitle}" to add or modify grants. Select "{addGrantTemplatesTitle}" to add in templates.
actions:
create: New Grant
grant-templates:
@@ -627,12 +635,18 @@ role:
global-admin:
name: Global Admin
description: Full administrative access to all resources in Boundary
org-access:
name: Org Access
description: Administrative access to perform actions on an org scope, but not the resources the org contains
org-admin:
name: Org Admin
description: Administrative access to all resources within an organization
description: Administrative access to perform actions on the resources an org contains
project-access:
name: Project Access
description: Administrative access to perform actions on a project scope, but not the resources the project contains
project-admin:
name: Project Admin
description: Administrative access to all resources within a project
description: Administrative access to perform actions on the resources a project contains
session-manager:
name: Session Manager
description: Can view and cancel sessions
@@ -655,11 +669,188 @@ role:
name: Session Auditor
description: Can view session recordings
target-user:
name: Target User
name: User
description: Can connect to targets but cannot manage anything
read-only-user:
name: Read-Only User
description: Can view but not modify any resources
name: Resource Auditor
description: Can only view but not modify any resources
edit-grants:
title: Edit Grants
description: Modify existing grant strings or add new grant strings to this role.
completion-info:
no-suggestions: No suggestions
wildcard-types: Wildcard - matches all types
wildcard-ids: Wildcard - matches all IDs
wildcard-actions: Wildcard - matches all actions
template-value: Template value
all-fields: All fields
loading-ids: Still loading ID suggestions…
linting-errors:
general:
whitespace-not-allowed: Whitespace is not allowed
leading-semicolon-not-allowed: Leading semicolon is not allowed
trailing-semicolon-not-allowed: Trailing semicolon is not allowed
invalid-character: Invalid character "{character}"
invalid-format: 'Invalid format: expected key=value'
invalid-syntax: Invalid syntax
fields:
duplicate-field: Duplicate field "{field}"
unknown-field: Unknown field "{field}"
empty-value: '"{field}" value cannot be empty'
missing-ids-or-type: Missing "ids" or "type" fields
missing-actions: Missing "actions" field
missing-actions-or-output-fields: Missing "actions" or "output_fields" fields
type:
invalid-type: Invalid resource type "{type}"
not-top-level: Type "{type}" is not a top-level type
not-child-type: Type "{type}" is not a child type of the pinned ID
missing-for-wildcard-ids: Missing "type" field for wildcard ids
wildcard-requires-ids: Missing "ids" field for wildcard type
ids:
empty-list: '"ids" field does not contain at least one id'
wildcard-cannot-combine: Wildcard id "*" cannot be combined with other ids
templates-cannot-combine: Template ids cannot be combined with each other
duplicate-template: Duplicate id "{primary}" and "{secondary}"
duplicate-id: Duplicate id "{id}"
unknown-template: Unknown template "{id}"
invalid-id: Invalid id "{id}"
mismatched-types: Ids do not have the same type. Invalid id "{id}"
must-support-child-types: Pinned IDs must support child types. Invalid id "{id}"
must-match-parent-type: Pinned IDs must be "{parentType}" ids. Invalid id "{id}"
actions:
empty-list: '"actions" field does not contain at least one action'
wildcard-cannot-combine: Wildcard action "*" cannot be combined with other actions
duplicate-action: Duplicate action "{action}"
invalid-action: Invalid action "{action}"
id-only: Only id actions for "{type}" type are allowed. Invalid action "{action}"
collection-only: Only collection actions are allowed. Invalid action "{action}"
noop-requires-list: '"no-op" action should only be used with "list" action'
list-requires-noop: '"list" action should be used with "no-op" action to have an effect'
noop-unnecessary: '"no-op" action is unnecessary when other actions are specified'
editor:
remove: Remove
remove-comma: Remove comma
add-noop: Add "no-op" action
output-fields:
empty-list: '"output_fields" field does not contain at least one field'
wildcard-cannot-combine: Wildcard output field "*" cannot be combined with other fields
duplicate-field: Duplicate output field "{field}"
invalid-field: Invalid output field "{field}"
invalid-field-for-type: Invalid output field "{field}" for type "{type}"
editor:
title: Grant Editor
description: Add each new grant string on a different line. Press Ctrl+Space to get suggestions.
string-formats:
title_plural: String formats
syntax: Syntax
syntax-format: Syntax format
more-info: More info
resource-type:
label: Singular resource type
help: Applies collection actions to a specific resource type in a single grant.
resource:
label: Specific resource(s)
help: Applies ID actions to one or more individual resources in a single grant.
pinned-id:
label: Pinned ID
help: '"Pins" actions to a non-top-level type within a specific ID in a single grant.'
output-fields:
label: Output fields
help: Defines which fields from a request are returned to the user.
account-template:
label: Account template
help: Substitutes the account ID from the token used to perform the request.
user-template:
label: User template
help: Substitutes the user ID from the token used to perform the request.
insert-actions: insert actions
insert-resource-type: insert resource type
insert-ids: insert ids
insert-resource-ids: insert resource ids
insert-output-fields: insert output fields
export:
format: Format
formatted-export-aria-label: Formatted export
options:
terraform: Terraform
native-hcl: Native HCL
actions:
collection-actions: Collection Actions
id-actions: ID Actions
create: Create a new {resourceType}
list: List all {resourceType} resources
read: Read {article} {resourceType}
update: Update {article} {resourceType}
delete: Delete {article} {resourceType}
read:self: Read the {resourceType} associated with the current user
delete:self: Delete {article} {resourceType} associated with the current user
cancel: Cancel {article} {resourceType}
cancel:self: Cancel {article} {resourceType} associated with the current user
generic:
create: Creates resources
list: Lists resources
read: Reads resources
update: Updates resources
delete: Deletes resources
read:self: Read the resource associated with the current user
delete:self: Delete a resource associated with the current user
cancel: Cancels resources
cancel:self: Cancel a resource associated with the current user
add-members: Add members to a group
set-members: Set the full list of members on a group
remove-members: Remove members from a group
add-hosts: Add hosts to a host set
set-hosts: Set the full list of hosts on a host set
remove-hosts: Remove hosts from a host set
add-worker-tags: Add tags to a worker
set-worker-tags: Set the full list of tags on a worker
remove-worker-tags: Remove tags from a worker
reinitialize-certificate-authority: Reinitialize the certificate authority that is used to authorize Boundary workers
create:controller-led: Create a worker via controller-led workflow
create:worker-led: Create a worker via worker-led workflow
read-certificate-authority: Read the certificate authority that is used to authorize Boundary workers
set-password: Set a password on an account, without requiring the current password
change-password: Change the password on an account
authenticate: Authenticate to an auth method
change-state: Change the active and visibility state of an OIDC-type auth method
add-accounts: Add accounts to a user
set-accounts: Set the full list of accounts on a user
remove-accounts: Remove accounts from a user
list-resolvable-aliases: List all aliases that point to resources the user has permission to access
add-principals: Add principals to a role
set-principals: Set the full list of principals on a role
remove-principals: Remove principals from a role
set-grants: Set the full list of grants on a role
add-grants: Add grants to a role
remove-grants: Remove grants from a role
add-grant-scopes: Add grant scopes to a role
set-grant-scopes: Set the full list of grant scopes on a role
remove-grant-scopes: Remove grant scopes from a role
authorize-session: Authorize a session via a target
add-host-sources: Add host sources to a target
set-host-sources: Set the full list of host sources on a target
remove-host-sources: Remove host sources from a target
add-credential-sources: Add credential sources to a target
set-credential-sources: Set the full list of credential sources on a target
remove-credential-sources: Remove credential sources from a target
attach-storage-policy: Attach a storage policy to all session recordings in the scope
detach-storage-policy: Detach a storage policy from all session recordings in the scope
set-alias-suffix: Set the alias suffix on a scope
remove-alias-suffix: Remove the alias suffix from a scope
list-keys: List the keys within a given scope
rotate-keys: Replace a scope's current KEK and DEKs with a new set of keys
list-key-version-destruction-jobs: List all pending key version destruction jobs within a scope
destroy-key-version: Destroy a key version in the scope
download: Download a session recording
reapply-storage-policy: Reapply the storage policy to a session recording
list-exports: List all export jobs for session recordings
export: Export a session recording
export:cancel: Cancel an in-progress session recording export
monthly-active-users: Display the number of monthly active Boundary users to help predict billing
no-actions: No actions available for this resource type.
invalid-id-or-type: Invalid ID and type. No actions available.
no-type-detected: No resource type detected.
unavailable: Grant actions information not available.
scope:
title: Scope
title_plural: Scopes
@@ -1,11 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<div
data-test-code-editor-field-editor
...attributes
class='rose-codemirror-container'
{{code-mirror value=@value onInput=@onInput options=@options}}
></div>
@@ -1,13 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<div data-test-code-editor>
{{yield
(hash
fieldEditor=(component 'rose/code-editor/field-editor' value=@codeValue)
toolbar=(component 'rose/code-editor/toolbar' copyText=@codeValue)
)
}}
</div>
@@ -1,31 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<div class='rose-code-editor-toolbar' data-test-code-editor-toolbar>
<div data-test-code-editor-toolbar-action class='code-editor-toolbar__action'>
{{yield to='action'}}
</div>
<div class='rose-code-editor-toolbar__right-aligned'>
{{#if (has-block 'menu')}}
<div
data-test-code-editor-toolbar-menu
class='rose-code-editor-toolbar__menu'
>
{{yield to='menu'}}
</div>
<div
class='rose-code-editor-toolbar__menu-divider'
data-test-code-editor-toolbar-menu-divider
></div>
{{/if}}
<Hds::Copy::Button
@text='Copy code'
@textToCopy={{@copyText}}
@onSuccess={{this.copied}}
data-test-code-editor-toolbar-copy-button
/>
</div>
</div>
@@ -1,14 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class RoseCodeEditorToolbarComponent extends Component {
@action
copied() {
this.args.onCopy?.();
}
}
@@ -1,99 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Modifier from 'ember-modifier';
import codemirror from 'codemirror';
import '../utilities/register-codemirror-hcl';
import 'codemirror/mode/go/go';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/shell/shell';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/display/autorefresh';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/json-lint';
// Here we define default options for the editor.
// These should follow the codemirror configuration types
//
const _PRESET_DEFAULTS = {
theme: 'monokai',
lineNumbers: true,
cursorBlinkRate: 500,
matchBrackets: true,
autoCloseBrackets: true,
styleActiveLine: true,
mode: 'hcl',
};
/**
*
* `CodeMirror` implements a modifier that creates a CodeMirror instance on the provided element.
*
* The supported modes for the editor are currently: hcl, shell, go, javascript
* The supported themes are: monokai
*
* Sample usage:
* ```
* <div
* {{code-mirror value=@value onInput=@onInput options=@options}}
* />
* ```
*
* @class CodeMirror
*
*/
export default class CodeMirrorModifier extends Modifier {
_editor;
didSetup = false;
modify(element, _, named) {
if (!this.didSetup) {
this._setup(element, named);
this.didSetup = true;
}
if (this._editor?.getValue() !== named.value) {
this._editor?.setValue(named.value);
}
}
/**
* Called when editor contents change.
* @method CodeMirror#_onChange
* See documentation for the CodeMirror editor events, specifically 'change' event here https://codemirror.net/doc/manual.html#events.
*/
_onChange(editor, named) {
const { onInput } = named;
const newVal = editor.getValue();
onInput && onInput(newVal);
}
/**
* Initializes the editor with default configuration merged with consumer-provided configuration.
* Also registers a 'change' event on the editor.
* @method CodeMirror#_setup
*/
_setup(element, named) {
if (!element) {
throw new Error('CodeMirror modifier has no element');
}
const editor = codemirror(element, {
..._PRESET_DEFAULTS,
...(named.options || {}),
value: named.value,
});
editor.on('change', (editor) => {
this._onChange(editor, named);
});
this._editor = editor;
}
}
@@ -13,6 +13,7 @@
form {
[class*='hds-form-field--layout'],
[class*='hds-form-group--layout'],
[class='hds-form-key-value-inputs'],
.rose-form-actions.hds-button-set {
margin-bottom: 1.5rem;
}
@@ -121,6 +121,15 @@
}
}
@mixin dark-mode-badge-count-override {
// Override the neutral badge count to fix contrast in dark mode.
/* stylelint-disable selector-class-pattern */
.hds-badge-count--color-neutral.hds-badge-count--type-inverted {
color: var(--token-color-palette-neutral-0);
}
/* stylelint-enable selector-class-pattern */
}
@mixin dark-mode-ember-dropdown-override {
.ember-basic-dropdown-content {
background-color: var(--token-color-palette-neutral-0);
@@ -191,20 +200,95 @@
}
}
// Overrides for CodeMirror 6 autocomplete and lint tooltips/panels.
@mixin dark-mode-codemirror-override {
/* stylelint-disable selector-class-pattern */
.cm-editor {
// Autocomplete dropdown and lint hover tooltips
.cm-tooltip {
background-color: var(--token-color-palette-neutral-50);
border: 1px solid var(--token-color-palette-neutral-100);
color: var(--token-color-palette-neutral-400);
}
// Arrow pointer for tooltips shown above the cursor
.cm-tooltip-above::before {
border-top-color: var(--token-color-palette-neutral-50);
}
// Arrow pointer for tooltips shown below the cursor
.cm-tooltip-below::before {
border-bottom-color: var(--token-color-palette-neutral-50);
}
// Autocomplete list > selected item highlight
.cm-tooltip-autocomplete > ul > li[aria-selected] {
background-color: var(--token-color-palette-blue-300);
color: var(--token-color-palette-neutral-700);
}
// Content/gutter border
.cm-content {
border-left: 1px solid var(--token-color-palette-neutral-100);
}
// Lint diagnostics panel (shown at the bottom of the editor)
.cm-panel.cm-panel-lint {
border-top: 3px solid var(--token-color-palette-neutral-100);
ul {
background-color: var(--token-color-palette-neutral-100);
// Lint panel > selected diagnostic item highlight
&:focus li.cm-diagnostic[aria-selected] {
background-color: var(--token-color-palette-neutral-50);
color: var(--token-color-palette-neutral-400);
}
}
}
}
/* stylelint-enable selector-class-pattern */
}
// Override to improve visibility of search clear ("x") icon.
@mixin dark-mode-search-clear-override {
input[type='search']::-webkit-search-cancel-button {
filter: invert(1);
}
}
@media (prefers-color-scheme: dark) {
// Apply dark style overrides to scrollbar when system is in dark mode and app theme is not set to light
html:has(.ember-application:not(.rose-theme-light)) {
scrollbar-color: var(--token-color-palette-neutral-400)
var(--token-color-palette-neutral-600);
}
.ember-application:not(.rose-theme-light) {
@include dark-mode;
@include dark-mode-modal-override;
@include dark-mode-badge-count-override;
@include dark-mode-ember-dropdown-override;
@include dark-mode-app-header-override;
@include dark-mode-app-side-nav-override;
@include dark-mode-codemirror-override;
@include dark-mode-search-clear-override;
}
}
// Apply dark style overrides to scrollbar when app theme is set to dark regardless of system theme
html:has(.ember-application.rose-theme-dark) {
scrollbar-color: var(--token-color-palette-neutral-400)
var(--token-color-palette-neutral-600);
}
.ember-application.rose-theme-dark {
@include dark-mode;
@include dark-mode-modal-override;
@include dark-mode-badge-count-override;
@include dark-mode-ember-dropdown-override;
@include dark-mode-app-header-override;
@include dark-mode-app-side-nav-override;
@include dark-mode-codemirror-override;
@include dark-mode-search-clear-override;
}
@@ -1,43 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import codemirror from 'codemirror';
import 'codemirror/addon/mode/simple';
// This utility defines 'hcl' mode for a codemirror editor and enables correct syntax highlighting.
// Importing this file in the codemirror modifier will allow 'hcl' to be selected as a mode:
// import 'cloud-ui-core/utils/register-codemirror-hcl'
codemirror.defineSimpleMode('hcl', {
start: [
{ regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string' }, // strings
{ regex: /(\w+)(\s+)(=)/, token: ['keyword', 'null', 'null'] }, // assignment of variables
{
regex: /(build|deploy|release|hook|registry|type|runner|url)( )({)/,
token: ['keyword', 'null', 'null'],
}, // top level keywords
{ regex: /(variable)\b/, token: 'keyword' }, // input variable keyword
{ regex: /true|false|null|undefined/, token: 'atom' }, // bool keywords
{
regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
token: 'number',
}, // numbers
{ regex: /(#|\/\/)(\s*\S*)/, token: 'comment' }, // single line comments
{ regex: /(path)(.)/, token: ['variable-2', 'null'] }, // path variables
{ regex: /(workspace)(.)(\S*)/, token: ['string', 'null', 'string'] }, // workspace variables
{ regex: /\/\*/, token: 'comment', next: 'comment' }, // multi-line comment
{ regex: /[-+/*=<>!]+/, token: 'operator' }, // operators
{ regex: /[{[(]/, indent: true }, // for auto indent
{ regex: /[}\])]/, dedent: true },
],
// The multi-line comment state.
comment: [
{ regex: /.*?\*\//, token: 'comment', next: 'start' },
{ regex: /.*/, token: 'comment' },
],
meta: {
dontIndentStates: ['comment'],
lineComment: '#',
},
});
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/components/rose/code-editor';
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/components/rose/code-editor/field-editor';
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/components/rose/code-editor/toolbar';
-6
View File
@@ -1,6 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'rose/modifiers/code-mirror';
@@ -5,7 +5,6 @@
// This list is sorted in ascending order to improve readability
@use 'code-editor';
@use 'form';
@use 'frame';
@use 'layout';
@@ -1,70 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
/* stylelint-disable selector-class-pattern */
@use '../../variables/sizing';
// Codemirror overrides
.rose-codemirror-container {
width: 100%;
}
.CodeMirror {
max-width: 100%;
display: flex;
flex: 1;
font-family: var(--token-typography-font-stack-code);
}
.rose-autosize {
.CodeMirror {
height: auto;
}
}
.CodeMirror-scroll {
width: 100%;
}
.CodeMirror-sizer {
border-right-width: 0 !important;
}
// Toolbar
.rose-code-editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--ui-gray-subtler-3);
height: 2.75rem;
&__action {
margin-left: sizing.rems(s);
}
&__right-aligned {
display: flex;
height: 100%;
align-items: center;
}
&__menu {
margin-right: sizing.rems(s);
.pds-dropdownTrigger {
box-shadow: none;
&__layoutWrapper {
padding-left: sizing.rems(s);
padding-right: sizing.rems(s);
}
}
&-divider {
border-right: 1px solid var(--ui-gray-subtler-3);
height: sizing.rems(xl);
}
}
}
@@ -12,7 +12,7 @@
'header header header'
'navigation navigation navigation'
'body body body';
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, auto);
grid-template-rows: auto 1fr;
.rose-layout-page-header {
-5
View File
@@ -12,11 +12,6 @@ module.exports = {
included(app) {
this._super.included.apply(this, arguments);
this.import('node_modules/codemirror/lib/codemirror.css');
this.import('node_modules/codemirror/theme/monokai.css');
this.import('node_modules/codemirror/addon/lint/lint.css');
this.import('node_modules/jsonlint/lib/jsonlint.js');
this.includeHDSStyles(app);
this.includeFlightIcons(app);
this.includePublic(app);
+5 -7
View File
@@ -34,16 +34,14 @@
"@hashicorp/design-system-tokens": "^2.3.0",
"@hashicorp/flight-icons": "^3.10.0",
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
"codemirror": "5.65.7",
"ember-auto-import": "^2.10.0",
"ember-cli-babel": "^8.2.0",
"ember-cli-babel": "^8.3.1",
"ember-cli-htmlbars": "^6.3.0",
"ember-cli-sass": "^11.0.1",
"ember-focus-trap": "^1.0.1",
"ember-named-blocks-polyfill": "^0.2.5",
"jsonlint": "^1.6.3",
"sass": "^1.69.5",
"uuid": "^11.0.3"
"uuid": "^14.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.25.1",
@@ -54,7 +52,7 @@
"@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0",
"concurrently": "^10.0.3",
"doctoc": "^2.2.0",
"ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0",
@@ -84,7 +82,7 @@
"@eslint/js": "^9.18.0",
"globals": "^15.14.0",
"loader.js": "^4.7.0",
"postcss": "^8.4.31",
"postcss": "^8.5.10",
"prettier": "^3.3.3",
"qunit": "^2.22.0",
"qunit-dom": "^3.2.1",
@@ -98,7 +96,7 @@
"ember-source": ">= 4.0.0"
},
"engines": {
"node": "20.* || 22.*"
"node": "22.* || 24.*"
},
"ember": {
"edition": "octane"
@@ -1,32 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | rose/code-editor', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
const codeValue = 'export HCP_CLIENT_ID = {place your client_id here}';
this.set('codeValue', codeValue);
await render(hbs`
<Rose::CodeEditor
@codeValue={{this.codeValue}}
as |c|
>
<c.toolbar />
<c.fieldEditor />
</Rose::CodeEditor>
`);
assert.dom('[data-test-code-editor]').isVisible();
assert.dom('[data-test-code-editor-toolbar]').isVisible();
assert.dom('[data-test-code-editor-field-editor]').isVisible();
assert.dom('[data-test-code-editor-field-editor]').includesText(codeValue);
});
});
@@ -1,74 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { typeIn, render, waitUntil } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module(
'Integration | Component | rose/code-editor/field-editor',
function (hooks) {
setupRenderingTest(hooks);
const editorSelector = '[data-test-code-editor-field-editor]';
test('it renders', async function (assert) {
const codeValue = `export HCP_CLIENT_ID = {place your client_id here}`;
this.set('codeValue', codeValue);
await render(hbs`
<Rose::CodeEditor::FieldEditor @value={{this.codeValue}} />
`);
assert.dom(editorSelector).isVisible();
assert.dom(editorSelector).includesText(codeValue);
const myNewCode = 'NEW CODE';
await typeIn(`${editorSelector} textarea`, myNewCode);
assert.dom(editorSelector).includesText(myNewCode);
});
test('it renders readonly', async function (assert) {
const codeValue = `export HCP_CLIENT_ID = {place your client_id here}`;
this.set('codeValue', codeValue);
this.set('options', {
readOnly: true,
});
await render(hbs`
<Rose::CodeEditor::FieldEditor @value={{this.codeValue}} @options={{this.options}} />
`);
assert.dom(editorSelector).isVisible();
assert.dom(editorSelector).includesText(codeValue);
const myNewCode = 'NEW CODE';
waitUntil(async () => {
try {
// This should fail if the textarea is readonly
await typeIn(`${editorSelector} textarea`, myNewCode);
} catch {
return true;
}
});
assert.dom(editorSelector).doesNotIncludeText(myNewCode);
});
test('it calls onInput on change', async function (assert) {
const codeValue = `export HCP_CLIENT_ID = {place your client_id here}`;
const onInput = () => {
this.set('called', true);
};
this.set('codeValue', codeValue);
this.set('onInput', onInput);
await render(hbs`
<Rose::CodeEditor::FieldEditor @value={{this.codeValue}} @onInput={{this.onInput}} />
`);
assert.dom(editorSelector).isVisible();
assert.dom(editorSelector).includesText(codeValue);
const myNewCode = 'NEW CODE';
await typeIn(`${editorSelector} textarea`, myNewCode);
assert.dom(editorSelector).includesText(myNewCode);
assert.true(this.called);
});
},
);
@@ -1,65 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
module('Integration | Component | rose/code-editor/toolbar', function (hooks) {
setupRenderingTest(hooks);
const toolbarSelector = '[data-test-code-editor-toolbar]';
const copyButtonSelector = '[data-test-code-editor-toolbar-copy-button]';
const menuDividerSelector = '[data-test-code-editor-toolbar-menu-divider]';
hooks.beforeEach(function () {
const copyText = 'copyText';
this.set('copyText', copyText);
});
test('it renders', async function (assert) {
await render(hbs`
<Rose::CodeEditor::Toolbar @copyText={{this.copyText}} />
`);
assert.dom(toolbarSelector).isVisible();
assert.dom(copyButtonSelector).isVisible();
assert.dom(menuDividerSelector).doesNotExist();
});
test('it displays action', async function (assert) {
await render(hbs`
<Rose::CodeEditor::Toolbar @onCopy={{this.copyText}}>
<:action>
<div data-test-my-action>Hello</div>
</:action>
</Rose::CodeEditor::Toolbar>
`);
assert.dom(toolbarSelector).isVisible();
assert.dom(copyButtonSelector).isVisible();
assert.dom(menuDividerSelector).doesNotExist();
assert.dom('[data-test-my-action]').isVisible();
});
test('it calls onCopy callback', async function (assert) {
// `writeText` could fail if the document is not in focus which is common during test runs
sinon.stub(window.navigator.clipboard, 'writeText').resolves();
const onCopy = () => {
this.set('called', true);
};
this.set('onCopy', onCopy);
await render(hbs`
<Rose::CodeEditor::Toolbar @copyText={{this.copyText}} @onCopy={{this.onCopy}} />
`);
assert.dom(toolbarSelector).isVisible();
assert.dom(copyButtonSelector).isVisible();
assert.dom(menuDividerSelector).doesNotExist();
await click(copyButtonSelector);
assert.true(this.called);
sinon.restore();
});
});
+2 -24
View File
@@ -97,31 +97,8 @@ If you would like to do it through the command line:
```bash
doormat login
source <(doormat aws export --account boundary_team_acctest_dev)
# Feel free to use a custom name for your key pair if you don't want to use $USER, just make sure it follows the <name>_enos format so it can be identified. e.g. zed_enos
ssh-keygen -N '' -t ed25519 -f ~/.ssh/"$USER"_enos
aws ec2 import-key-pair \
--region us-east-1 \
--key-name "$USER"_enos \
--public-key-material fileb://~/.ssh/"$USER"_enos.pub
```
### Setup Enos:
Make sure you're in the correct boundary repository for the version of Boundary you plan on using.
The enos scenarios for CE need to be run in `boundary` and the enos scenarios for enterprise need to be run in `boundary-enterprise`.
Enos needs some configuration variables to run the scenario successfully. [See the configuration file](https://github.com/hashicorp/boundary/blob/main/enos/enos.vars.hcl). The file has comments per each variable, but some awareness:
- `boundary_edition`: The edition of Boundary you are using. Options are `oss` or `enterprise`.
- `aws_region`: The AWS region you are using. Very important as mentioned within the EC2 Setup.
- `aws_ssh_keypair_name`: The name of the AWS keypair.
- `aws_ssh_private_key_path`: The path to the private key associated with your keypair.
- `enos_user`: The user name to use for tagged resources in AWS.
- `e2e_debug_no_run`: Make sure this is set to true.
- `boundary_license_path` (enterprise only): Path to an enterprise license
More documentation about [scenario variables](https://github.com/hashicorp/boundary/tree/main/enos#scenarios-variables).
## Run tests:
Make sure you followed all the steps within the [Getting started section](#getting-started).
@@ -157,6 +134,7 @@ Using Terminal 1:
- `cd boundary/enos` or `cd boundary_enterprise/enos`.
- `doormat login`. Login with Doormat.
- `eval "$(doormat aws export --account boundary_team_acctest_dev)"`. Exporting AWS env variables from doormat to your terminal.
- `export ENOS_VAR_boundary_license={LICENSE_KEY}`. If running enterprise, you will need a license key
- `enos scenario launch e2e_ui_aws builder:local protocol:https` or `enos scenario launch e2e_ui_aws_ent builder:local protocol:https` if in enterprise.
- Launches enos scenario, this will take from 5 to 10 minutes. When its done, you will see a Enos Operations finished! within your terminal. Check out more scenarios [here](https://github.com/hashicorp/boundary/tree/main/enos).
- `bash scripts/test_e2e_env.sh`. Prints all the env variables within Enos scenario. Copy the output and paste it within your Terminal 2 (Boundary UI). These env variables are needed within Boundary UI to run the test against the enos scenario.
@@ -324,7 +302,7 @@ If working on an E2E test that is working against a different version of the bou
# in the following example $COMMITISH can be replaced with a branch name, commit, or tag
OPENAPI_SWAGGER_URL_OR_FILE=https://raw.githubusercontent.com/hashicorp/boundary/$COMMITISH/internal/gen/controller.swagger.json pnpm generate:api-client
# using a local absolute or relative path
# using a local absolute or relative path
OPENAPI_SWAGGER_URL_OR_FILE=../../boundary-enterprise/internal/gen/controller.swagger.json pnpm generate:api-client
```
+13 -4
View File
@@ -64,12 +64,21 @@ export class RolesPage extends BaseResourcePage {
*/
async addGrantsToRole(grants) {
await this.page.getByRole('link', { name: 'Grants', exact: true }).click();
await this.page.getByRole('textbox', { name: 'New Grant' }).fill(grants);
await this.page.getByRole('button', { name: 'Add', exact: true }).click();
await this.page
.getByRole('link', { name: 'Edit Grants', exact: true })
.click();
await this.page.locator('.hds-code-editor').getByRole('textbox').click();
await expect(
this.page.locator('.hds-code-editor').getByRole('textbox'),
).toBeFocused();
await this.page
.locator('.hds-code-editor')
.getByRole('textbox')
.fill(grants);
await this.page.getByRole('button', { name: 'Save', exact: true }).click();
await this.dismissSuccessAlert();
await expect(
this.page.getByRole('textbox', { name: 'Grant', exact: true }),
).toHaveValue(grants);
this.page.locator('.grant-codeblock .hds-code-block__code'),
).toHaveText(grants);
}
}
+4
View File
@@ -98,6 +98,10 @@ export class StorageBucketsPage extends BaseResourcePage {
await this.page.getByLabel('Region').fill(region);
await this.page.getByLabel('Access key ID').fill(accessKeyId);
await this.page.getByLabel('Secret access key').fill(secretAccessKey);
await this.page
.getByLabel('Worker filter')
.first()
.scrollIntoViewIfNeeded();
await this.page
.getByLabel('Worker filter')
.getByRole('textbox')
+9 -3
View File
@@ -58,7 +58,7 @@ export class TargetsPage extends BaseResourcePage {
if (alias) {
await this.page
.getByRole('group', { name: 'Aliases' })
.getByLabel('value')
.getByLabel('Aliases')
.last()
.fill(alias);
await this.page.getByRole('button', { name: 'Add' }).click();
@@ -145,7 +145,10 @@ export class TargetsPage extends BaseResourcePage {
.getByText('Edit Ingress Worker Filter'),
).toBeVisible();
await this.page.locator('.CodeMirror').getByRole('textbox').fill(filter);
await this.page
.locator('.hds-code-editor')
.getByRole('textbox')
.fill(filter);
await this.page.getByRole('button', { name: 'Save', exact: true }).click();
await this.dismissSuccessAlert();
@@ -174,7 +177,10 @@ export class TargetsPage extends BaseResourcePage {
.getByText('Edit Egress Worker Filter'),
).toBeVisible();
await this.page.locator('.CodeMirror').getByRole('textbox').fill(filter);
await this.page
.locator('.hds-code-editor')
.getByRole('textbox')
.fill(filter);
await this.page.getByRole('button', { name: 'Save', exact: true }).click();
await this.dismissSuccessAlert();
@@ -174,8 +174,8 @@ test(
// Log in using ldap account
await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: ldapAuthMethodName }).click();
await loginPage.login(ldapUserName, ldapUserPassword);
await expect(
@@ -193,6 +193,9 @@ test(
// View the LDAP account and verify account attributes
await page.getByRole('link', { name: orgName }).click();
await expect(
page.getByRole('heading', { name: 'Projects' }),
).toBeVisible();
await page
.getByRole('navigation', { name: 'Primary' })
.getByRole('link', { name: 'Auth Methods' })
@@ -129,8 +129,8 @@ test(
// Log in using oidc account
await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: oidcAuthMethodName }).click();
const pagePromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Sign In' }).click();
@@ -58,8 +58,8 @@ test(
// Log in using new account
await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: authMethodName }).click();
await loginPage.login(username, password);
@@ -97,8 +97,8 @@ test(
// Log in using new password
await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: orgName }).click();
await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: authMethodName }).click();
await loginPage.login(username, newPassword);
await expect(
@@ -52,8 +52,8 @@ test(
// Log in using new account
await page.reload();
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: org.name }).click();
await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('option', { name: org.name }).click();
await page.getByRole('link', { name: authMethod.name }).click();
const loginPage = new LoginPage(page);
await loginPage.login(username, password);
@@ -79,8 +79,8 @@ test(
// Confirm user cannot log in with old password
await loginPage.logout(username);
await page.getByText('Choose a different scope').click();
await page.getByRole('link', { name: org.name }).click();
await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('option', { name: org.name }).click();
await page.getByRole('link', { name: authMethod.name }).click();
await page.getByLabel('Login Name').fill(username);
await page.getByLabel('Password', { exact: true }).fill(password);
@@ -195,7 +195,9 @@ test(
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('combobox', { name: 'Type' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
await page.getByText('{}').click();
await page.locator('.hds-code-editor').getByRole('textbox').click();
// Move cursor to middle of '{}' to avoid typing outside of the braces
await page.keyboard.press('ArrowLeft');
const testName = 'name-json';
const testPassword = 'password-json';
const testId = 'id-json';
+6 -6
View File
@@ -52,6 +52,9 @@ test(
.getByRole('navigation', { name: 'Primary' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await expect(
page.getByRole('heading', { name: 'Host Catalogs' }),
).toBeVisible();
await page.getByRole('link', { name: hostCatalogName }).click();
const hostSetName2 = await hostCatalogsPage.createHostSet();
@@ -233,19 +236,16 @@ test(
).toBeVisible();
await expect(page.getByText('"dev" in "/tags/type"')).toBeVisible();
await page
.locator('.CodeMirror')
.getByRole('textbox')
.click({ force: true });
await page.locator('.hds-code-editor').getByRole('textbox').click();
await expect(
page.locator('.CodeMirror').getByRole('textbox'),
page.locator('.hds-code-editor').getByRole('textbox'),
).toBeFocused();
const selectAllShortcut =
process.platform === 'darwin' ? 'Meta+A' : 'Control+A';
await page.keyboard.press(selectAllShortcut);
await page.keyboard.press('Backspace');
await page
.locator('.CodeMirror')
.locator('.hds-code-editor')
.getByRole('textbox')
.fill('"prod" in "/tags/type"');
await page.getByRole('button', { name: 'Save' }).click();
@@ -133,6 +133,8 @@ export const electronTest = test.extend({
electronPage: async ({ electronApp }, use) => {
const page = await electronApp.firstWindow();
await page.evaluate(() => localStorage.clear());
// Reset the cluster URL stored by electron
await page.evaluate(() => window.desktop.cluster.resetClusterUrl());
await page.context().tracing.start({ screenshots: true, snapshots: true });
await use(page);
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import * as boundaryCli from '../helpers/boundary-cli.js';
import baseGlobalSetup from '../global-setup.js';
export default async function globalSetup() {
await baseGlobalSetup();
// when running locally, there's a chance that the cache could be running an
// older version.
// stop the cache if it's running, and the desktop client will automatically
// start the cache after logging in .
await boundaryCli.checkBoundaryCli();
await boundaryCli.stopCache();
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { defineConfig } from '@playwright/test';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
export default defineConfig({
globalSetup: '../global-setup',
globalSetup: './global-setup',
outputDir: './artifacts',
workers: 1, // Tests need to be run in serial, otherwise there may be conflicts when using the CLI
use: {
+1
View File
@@ -4,6 +4,7 @@
*/
import { chromium, test as baseTest, mergeTests } from '@playwright/test';
import { checkEnv } from './helpers/general.js';
import { boundaryApiClientTest } from './helpers/boundary-api-client.js';
import { LoginPage } from './admin/pages/login.js';
+1
View File
@@ -9,6 +9,7 @@ export * from './boundary-cli/accounts.js';
export * from './boundary-cli/aliases.js';
export * from './boundary-cli/auth-methods.js';
export * from './boundary-cli/authenticate.js';
export * from './boundary-cli/cache.js';
export * from './boundary-cli/connect.js';
export * from './boundary-cli/credential-stores.js';
export * from './boundary-cli/credentials.js';
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { execSync } from 'node:child_process';
/**
* Stops the boundary cache used by the desktop client
*/
export async function stopCache() {
try {
execSync('boundary cache stop');
} catch (e) {
console.log(`${e.stderr}`);
}
}
+5 -4
View File
@@ -25,15 +25,16 @@
"admin:dev:firefox": "pnpm run admin --project=firefox --headed",
"admin:dev:safari": "pnpm run admin --project=webkit --headed",
"desktop": "pnpm run --filter desktop build && pnpm run desktop:dev",
"desktop:local": "SETUP_CLI=true pnpm run desktop",
"desktop:dev": "pnpm playwright test --config desktop/playwright.config.js --reporter=html"
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/eslint-parser": "^7.21.3",
"@eslint/js": "^9.18.0",
"@openapitools/openapi-generator-cli": "^2.30.0",
"@playwright/test": "^1.56.1",
"concurrently": "^9.1.0",
"@openapitools/openapi-generator-cli": "^2.38.0",
"@playwright/test": "^1.60.0",
"concurrently": "^10.0.3",
"dotenv": "^16.4.5",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.1.8",
@@ -44,7 +45,7 @@
"tesseract.js": "^5.1.1"
},
"engines": {
"node": "20.* || 22.*"
"node": "22.* || 24.*"
},
"lint-staged": {
"*.js": [
+9 -9
View File
@@ -34,27 +34,27 @@
"prepare": "husky"
},
"devDependencies": {
"concurrently": "^9.1.0",
"concurrently": "^10.0.3",
"doctoc": "^2.2.0",
"esbuild": "^0.25.4",
"git-cz": "^4.9.0",
"husky": "^9.1.6",
"license-checker": "^25.0.1",
"lint-staged": "^15.2.10",
"esbuild": "^0.25.4"
"lint-staged": "^15.5.2"
},
"pnpm": {
"overrides": {
"nomnom>underscore": "^1.12.1",
"micromatch": "^4.0.8",
"css-select>nth-check": "^2.0.1",
"node-gyp": "^10.0.0",
"ansi-html": "^0.0.9",
"fireworm>async": "^2.6.4",
"ember-cli-babel>@babel/runtime": "7.27.6",
"ember-try-config>package-json": "^10.0.1",
"ember-loading>ember-concurrency": "^4.0.0",
"tmp": "^0.2.5",
"tar": "^7.5.10"
"tmp": "^0.2.7",
"tar": "^7.5.10",
"nomnom>underscore": "^1.13.8",
"ember-cli>markdown-it": "^14.2.0"
},
"ignoredBuiltDependencies": [
"@parcel/watcher",
@@ -75,7 +75,7 @@
}
},
"engines": {
"node": "20.* || 22.*"
"node": "22.* || 24.*"
},
"packageManager": "pnpm@10.8.1"
"packageManager": "pnpm@10.33.3"
}
+1372 -984
View File
File diff suppressed because it is too large. Load diff
+18
View File
@@ -0,0 +1,18 @@
# package.json Overrides
This file documents the rationale for each dependency override defined in `package.json` under `pnpm.overrides`.
These overrides force specific versions of transitive dependencies, primarily for security and compatibility reasons.
| Package | Version | Pulled in by | What to check before removing |
|---------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `micromatch` | `^4.0.8` | `anymatch` (`^3.1.4`), `common-tags` (`3.1.10`), `sane` (`^3.1.4`), `find-yarn-workspace-root` / `findup-sync` (`^4.0.2`), `ember-template-lint` / `stylelint` (`^4.0.5`) all declare ranges that would resolve below `4.0.8`. | Once all dependents declare a range of `^4.0.8` or higher, the override can be removed. |
| `css-select>nth-check` | `^2.0.1` | `css-select` pins an older `nth-check`. | Check whether a newer `css-select` release drops the pinned `nth-check` `<2.0.0` requirement. Used by `ember-inline-svg`. |
| `ansi-html` | `^0.0.9` | Both `broccoli-middleware` and `broccoli` pin `"ansi-html": "^0.0.7"`. | Check whether newer `broccoli` / `ember-cli` releases update these to drop the sub-`0.0.9` pin. |
| `fireworm>async` | `^2.6.4` | `fireworm` pins an older `async`. | Check whether `fireworm` has been updated or replaced; if `testem` updates `fireworm`, this override can be removed. |
| `ember-cli-babel>@babel/runtime` | `7.27.6` | `ember-cli-babel` restricts the version it resolves. | Review when upgrading `ember-cli-babel`; once its `@babel/runtime` range is broad enough, remove and verify the build. |
| `ember-try-config>package-json` | `^10.0.1` | `ember-try-config` pins an older `package-json`. | Check whether a newer `ember-try-config` release widens its `package-json` range to include v10+. |
| `ember-loading>ember-concurrency` | `^4.0.0` | `ember-loading` declares `"ember-concurrency": "^2.0.3"`. | The override and the patch (`patches/ember-loading.patch`) are a pair — the override forces v4 to be installed, the patch updates the source to use v4. Both must be removed together if `ember-loading` ever ships a version that natively supports v4. |
| `tmp` | `^0.2.7` | 10+ packages pin old versions of `tmp` (`broccoli`, `can-symlink`, `ember-cli-typescript`, `external-editor`, `fixturify-project`, `sane`, `testem`, and others) | Check whether the main Ember toolchain packages (`broccoli`, `ember-cli`, `testem`) have been updated to allow `tmp ^0.2.x`. |
| `tar` | `^7.5.10` | Both `cacache` (`^6.1.11`) and `@electron/rebuild` (`^6.0.5`) pin old ranges. | Check whether a newer `ember-electron` brings in versions of `@electron/rebuild` and `cacache` that allow `tar ^7.x`. |
| `nomnom>underscore` | `^1.13.8` | `nomnom` pins `"underscore": "~1.6.0"`. All other consumers already declare ranges that include 1.13.8. | `nomnom@1.8.1` is the last published version and the package is abandoned. `jsonlint` is the actual consumer, currently pinned to `^1.6.3` which still depends on `nomnom`. |
| `ember-cli>markdown-it` | `^14.2.0` | `ember-cli` (`~5.12.0`) pins old ranges. | Once `ember-cli` has been updated to at least `6.3.0` then this override can be removed. |
+20 -9
View File
@@ -90,10 +90,10 @@ export default function transformer(file, api) {
} else {
// if an array is provided and it's argument is a literal (number, boolean, string) then this is used for the test name
// otherwise if it's an object the index in the array is what is used for the test name
eachTestNames = testArguments[1].elements.map((element, i) => element.type.endsWith('Literal') ? element.value : i);
eachTestNames = testArguments[1].elements?.map((element, i) => element.type.endsWith('Literal') ? element.value : i);
}
testName = eachTestNames.map(eachTestName => `${testName} [${eachTestName}]`);
testName = eachTestNames?.map(eachTestName => `${testName} [${eachTestName}]`) ?? [];
} else {
testName = [testName];
}
@@ -270,16 +270,27 @@ export default function transformer(file, api) {
}
};
const existingEmberA11yImport = f.find(j.ImportDeclaration, {
source: {
value: 'ember-a11y-testing/test-support',
},
});
tests.forEach(testHandler);
testsEach.forEach(testHandler);
const code = f.toSource();
// the codemod will insert a newline between existing import statements,
// this regex removes the extra new line
//
return code.replace(
/(?<newline>\n)(import .*ember-a11y-testing\/test-support.*)/g,
'$2',
);
if (existingEmberA11yImport.length > 0) {
return code;
} else {
// the codemod will insert a newline between existing import statements,
// this regex removes the extra new line
return code.replace(
/(?<newline>\n)(import .*ember-a11y-testing\/test-support.*)/g,
'$2',
);
}
}
+2 -2
View File
@@ -24,11 +24,11 @@ module.exports = {
'at-rule-no-vendor-prefix': null,
// As a team we want to stick to using kebab-case as our selector class pattern, but there are a couple
// of exceptions in which we overrride CSS from addons like CodeMirror and HDS, in which they use
// of exceptions in which we overrride CSS from addons like HDS, in which they use
// pascal case and a double-kebab-case. This regex checks for all of those 3 patterns and won't fail
// lint check, but will fail if its any other pattern.
'selector-class-pattern': [
'(^([a-z][a-z0-9]*)(-{1,2}[a-z0-9]+)*$)|(^CodeMirror)',
'(^([a-z][a-z0-9]*)(-{1,2}[a-z0-9]+)*$)',
{
message: (selector) =>
`Expected class selector "${selector}" to be kebab-case`,
@@ -82,37 +82,30 @@
{{/if}}
</Hds::Form::TextInput::Field>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::Field::KeyValue
@name='certificates'
@model={{@model}}
@properties={{array 'value'}}
@data={{@model.certificates}}
@legend={{t 'resources.auth-method.form.certificates.label'}}
@helperText={{t 'resources.auth-method.form.certificates.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.auth-method.form.certificates.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.auth-method.form.certificates.help'}}
</F.HelperText>
{{#if @model.errors.certificates}}
<F.Error as |E|>
{{#each @model.errors.certificates as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
<:row as |R|>
<R.Field as |F|>
<F.Textarea
aria-label={{t 'resources.auth-method.form.certificates.label'}}
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.Textarea
@name='certificates'
@options={{@model.certificates}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
{{#if @model.isNew}}
<Hds::Form::TextInput::Field
@@ -406,45 +399,46 @@
}}</F.HelperText>
</Hds::Form::TextInput::Field>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::field::KeyValue
@name='account_attribute_maps'
@model={{@model}}
@properties={{array 'key' 'value'}}
@data={{@model.account_attribute_maps}}
@legend={{t 'resources.auth-method.form.account_attribute_maps.label'}}
@helperText={{t 'resources.auth-method.form.account_attribute_maps.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.auth-method.form.account_attribute_maps.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.auth-method.form.account_attribute_maps.help'}}
</F.HelperText>
{{#if @model.errors.account_attribute_maps}}
<F.Error as |E|>
{{#each @model.errors.account_attribute_maps as |error|}}
<E.Message>{{error.message}}</E.Message>
<:row as |R|>
<R.Field as |F|>
<F.Label>{{t 'resources.auth-method.titles.from-attr'}}</F.Label>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
<R.Field as |F|>
<F.Label>{{t 'resources.auth-method.titles.to-attr'}}</F.Label>
<F.Select
data-test-value-select
{{on 'change' (fn R.updateRow 'value')}}
>
<option disabled hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each this.toAttributes as |attr|}}
<option value={{attr}} selected={{eq R.rowData.value attr}}>
{{attr}}
</option>
{{/each}}
</F.Error>
</F.Select>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='account_attribute_maps'
@options={{@model.account_attribute_maps}}
@model={{@model}}
@keyLabel={{t 'resources.auth-method.titles.from-attr'}}
@valueLabel={{t 'resources.auth-method.titles.to-attr'}}
@width='100%'
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.select @selectOptions={{this.toAttributes}} />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::field::KeyValue>
<div class='ldap-section'>
<Hds::Text::Display
@@ -98,7 +98,6 @@
name='client_id'
@value={{@model.client_id}}
@isRequired={{true}}
@isInvalid={{@model.errors.client_id}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'client_id')}}
as |F|
@@ -146,179 +145,164 @@
</Hds::Form::TextInput::Field>
{{/unless}}
{{! Signing Algorithms }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
{{! Signing algorithms }}
<Form::Field::KeyValue
@name='signing_algorithms'
@model={{@model}}
@properties={{array 'value'}}
@data={{@model.signing_algorithms}}
@legend={{t 'form.signing_algorithms.label'}}
@helperText={{t 'form.signing_algorithms.help'}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.signing_algorithms.label'}}
</F.Legend>
<F.HelperText>
{{t 'form.signing_algorithms.help'}}
</F.HelperText>
{{#if @model.errors.signing_algorithms}}
<F.Error as |E|>
{{#each @model.errors.signing_algorithms as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.Select
@name='signing_algorithms'
@model={{@model}}
@selectOptions={{this.signingAlgorithms}}
@width='100%'
/>
</:field>
</Form::Field::ListWrapper>
{{! Allowed Audiences }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.allowed_audiences.label'}}
</F.Legend>
<F.HelperText>
{{t 'form.allowed_audiences.help'}}
</F.HelperText>
{{#if @model.errors.allowed_audiences}}
<F.Error as |E|>
{{#each @model.errors.allowed_audiences as |error|}}
<E.Message>{{error.message}}</E.Message>
<:row as |R|>
<R.Field as |F|>
<F.Select
aria-label={{t 'form.signing_algorithms.label'}}
data-test-value-select
{{on 'change' (fn R.updateRow 'value')}}
>
<option disabled hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each this.signingAlgorithms as |algorithm|}}
<option
value={{algorithm}}
selected={{eq R.rowData.value algorithm}}
>
{{algorithm}}
</option>
{{/each}}
</F.Error>
</F.Select>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
</:row>
</Form::Field::KeyValue>
<:field as |F|>
<F.TextInput
@name='allowed_audiences'
@options={{@model.allowed_audiences}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
{{! Allowed audiences }}
<Form::Field::KeyValue
@name='allowed_audiences'
@model={{@model}}
@properties={{array 'value'}}
@data={{@model.allowed_audiences}}
@legend={{t 'form.allowed_audiences.label'}}
@helperText={{t 'form.allowed_audiences.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:row as |R|>
<R.Field as |F|>
<F.TextInput
aria-label={{t 'form.allowed_audiences.label'}}
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
</Form::Field::KeyValue>
{{! Claims Scopes }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::Field::KeyValue
@name='claims_scopes'
@model={{@model}}
@properties={{array 'value'}}
@data={{@model.claims_scopes}}
@legend={{t 'form.claims_scopes.label'}}
@helperText={{t 'form.claims_scopes.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.claims_scopes.label'}}
</F.Legend>
<F.HelperText>
{{t 'form.claims_scopes.help'}}
</F.HelperText>
{{#if @model.errors.claims_scopes}}
<F.Error as |E|>
{{#each @model.errors.claims_scopes as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
<:row as |R|>
<R.Field as |F|>
<F.TextInput
aria-label={{t 'form.claims_scopes.label'}}
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
@name='claims_scopes'
@options={{@model.claims_scopes}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
{{! Account Claim Maps }}
<Form::Field::ListWrapper
@layout='horizontal'
<Form::Field::KeyValue
@name='account_claim_maps'
@model={{@model}}
@properties={{array 'key' 'value'}}
@data={{@model.account_claim_maps}}
@legend={{t 'form.account_claim_maps.label'}}
@helperText={{t 'form.account_claim_maps.help'}}
@disabled={{or form.disabled this.hasSavedSubClaimMap}}
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.account_claim_maps.label'}}
</F.Legend>
<F.HelperText>
{{t 'form.account_claim_maps.help'}}
</F.HelperText>
<:row as |R|>
<R.Field as |F|>
<F.Label>{{t 'form.from_claim.label'}}</F.Label>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
{{#if @model.errors.account_claim_maps}}
<F.Error as |E|>
{{#each @model.errors.account_claim_maps as |error|}}
<E.Message>{{error.message}}</E.Message>
<R.Field as |F|>
<F.Label>{{t 'form.to_claim.label'}}</F.Label>
<F.Select
data-test-value-select
{{on 'change' (fn R.updateRow 'value')}}
>
<option disabled hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each (this.toClaimOptions R.rowData) as |claim|}}
<option value={{claim}} selected={{eq R.rowData.value claim}}>
{{claim}}
</option>
{{/each}}
</F.Error>
</F.Select>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='account_claim_maps'
@options={{@model.account_claim_maps}}
@model={{@model}}
@keyLabel={{t 'form.from_claim.label'}}
@valueLabel={{t 'form.to_claim.label'}}
@width='100%'
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.select @selectOptions={{this.toClaims}} />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
{{! Certificates }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::Field::KeyValue
@name='idp_ca_certs'
@model={{@model}}
@properties={{array 'value'}}
@data={{@model.idp_ca_certs}}
@legend={{t 'form.idp_ca_certs.label'}}
@helperText={{t 'form.idp_ca_certs.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'form.idp_ca_certs.label'}}
</F.Legend>
<F.HelperText>
{{t 'form.idp_ca_certs.help'}}
</F.HelperText>
{{#if @model.errors.idp_ca_certs}}
<F.Error as |E|>
{{#each @model.errors.idp_ca_certs as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
<:row as |R|>
<R.Field as |F|>
<F.Textarea
aria-label={{t 'form.idp_ca_certs.label'}}
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.Textarea
@name='idp_ca_certs'
@options={{@model.idp_ca_certs}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
<Hds::Form::TextInput::Field
name='max_age'
@@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking';
export default class FormAuthMethodOidcComponent extends Component {
// =attributes
/**
* @type {object}
*/
@@ -38,6 +39,35 @@ export default class FormAuthMethodOidcComponent extends Component {
@tracked selectedPrompts = this.parsePromptsArray();
@tracked skipPromptsList = this.isToggleChecked();
/**
* @returns {boolean}
*/
get hasSavedSubClaimMap() {
return (
!this.args.model?.isNew &&
this.args.model.account_claim_maps?.some((item) => item?.value === 'sub')
);
}
/**
* Filter 'sub' from dropdown options when updating
* On creation, show all options including sub.
* On update, hide sub unless it's the current value of the row (to display it when disabled).
* @param {Object} rowData - The current row data
* @returns {string[]}
*/
toClaimOptions = (rowData) => {
// On creation, show all options
if (this.args.model?.isNew) {
return this.toClaims;
}
// On update, filter "sub" unless it's the current value
return this.toClaims.filter(
(claim) => claim !== 'sub' || rowData?.value === 'sub',
);
};
/**
* @returns {string}
*/
@@ -53,7 +83,6 @@ export default class FormAuthMethodOidcComponent extends Component {
}
//actions
/**
* @param {string} value
*/
@@ -225,83 +225,76 @@
{{/if}}
</Hds::Form::TextInput::Field>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::Field::KeyValue
@name='critical_options'
@model={{@model}}
@properties={{array 'key' 'value'}}
@data={{@model.critical_options}}
@legend={{t 'resources.credential-library.form.critical_options.label'}}
@helperText={{t 'resources.credential-library.form.critical_options.help'}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.credential-library.form.critical_options.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.credential-library.form.critical_options.help'}}
</F.HelperText>
{{#if @model.errors.critical_options}}
<F.Error as |E|>
{{#each @model.errors.critical_options as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='critical_options'
@options={{@model.critical_options}}
@model={{@model}}
@disabled={{form.disabled}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |K|>
<K.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.credential-library.form.extensions.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.credential-library.form.extensions.help'}}
</F.HelperText>
<:row as |R|>
<R.Field as |F|>
<F.Label>{{t 'form.key.label'}}</F.Label>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
{{#if @model.errors.extensions}}
<F.Error as |E|>
{{#each @model.errors.extensions as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
<R.Field as |F|>
<F.Label>{{t 'form.value.label'}}</F.Label>
<F.TextInput
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='extensions'
@options={{@model.extensions}}
@model={{@model}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</:row>
</Form::Field::ListWrapper>
</Form::Field::KeyValue>
<Form::Field::KeyValue
@name='extensions'
@model={{@model}}
@properties={{array 'key' 'value'}}
@data={{@model.extensions}}
@legend={{t 'resources.credential-library.form.extensions.label'}}
@helperText={{t 'resources.credential-library.form.extensions.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:row as |R|>
<R.Field as |F|>
<F.Label>{{t 'form.key.label'}}</F.Label>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
<R.Field as |F|>
<F.Label>{{t 'form.value.label'}}</F.Label>
<F.TextInput
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
</Form::Field::KeyValue>
{{#if (can 'save model' @model)}}
<form.actions
@@ -4,7 +4,6 @@
*/
import Component from '@glimmer/component';
import { options } from 'api/models/credential-library';
export default class FormCredentialLibraryVaultSshCertComponent extends Component {
@@ -20,17 +20,15 @@
/>
</Form::Field::JsonSecret::Skeleton>
{{else}}
<Rose::CodeEditor as |c|>
<c.fieldEditor
class='secret-editor-json'
@onInput={{@onInput}}
@value={{@value}}
@options={{hash
mode='application/json'
gutters=(array 'CodeMirror-lint-markers')
lint='true'
}}
/>
</Rose::CodeEditor>
<Hds::CodeEditor
class='secret-editor-json'
@ariaLabel={{t 'form.secret-editor.label'}}
@value={{@value}}
@isLintingEnabled={{true}}
@language='json'
@onInput={{@onInput}}
@cspNonce={{(csp-nonce)}}
data-test-code-editor
/>
{{/if}}
</div>
@@ -0,0 +1,46 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::KeyValueInputs
name={{@name}}
@data={{@data}}
@isRequired={{@isRequired}}
@isOptional={{@isOptional}}
disabled={{@disabled}}
>
<:header as |H|>
<H.Legend>{{@legend}}</H.Legend>
{{#if @helperText}}
<H.HelperText>{{@helperText}}</H.HelperText>
{{/if}}
</:header>
<:row as |R|>
{{yield
(hash
Field=R.Field
DeleteRowButton=R.DeleteRowButton
rowData=R.rowData
removeRow=(fn this.handleRemove R.rowData)
updateRow=(fn this.handleUpdate R.rowData)
canDelete=this.canDelete
)
to='row'
}}
</:row>
<:footer as |F|>
<F.AddRowButton
@text={{t 'actions.add'}}
data-test-add-button
@onClick={{this.handleAdd}}
/>
{{#if this.errors}}
<F.Error as |E|>
{{#each this.errors as |error|}}
<E.Message data-test-error-message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:footer>
</Hds::Form::KeyValueInputs>
@@ -0,0 +1,124 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { assert } from '@ember/debug';
export default class FormFieldKeyValueComponent extends Component {
/**
* Determines if the delete button should be shown.
*/
get canDelete() {
const rows = this.args.data || [];
// Always show delete for multiple rows
if (rows.length > 1) {
return true;
}
// For a single row, only show if it has any non-empty values
if (rows.length === 1) {
const row = rows[0];
for (const key in row) {
if (row[key]) {
return true;
}
}
}
// Return false if there are no rows
return false;
}
get errors() {
// If errors are passed directly as an argument, use those
if (this.args.errors) {
return this.args.errors;
}
const { model, name } = this.args;
if (model && name && model.errors) {
return model.errors[name];
}
return null;
}
/**
* Default handler for adding a row.
* Uses @model, @name, and @properties if provided, otherwise calls @onAdd.
*/
@action
handleAdd() {
// If an onAdd handler is provided, use it
if (this.args.onAdd) {
return this.args.onAdd();
}
const { model, name, properties } = this.args;
assert(
'FormFieldKeyValueComponent: model, name, and properties are required if no custom onAdd handler is provided.',
model && name && properties,
);
const emptyRow = Object.fromEntries(properties.map((prop) => [prop, '']));
model[name] = [...(model[name] ?? []), emptyRow];
}
/**
* Default handler for removing a row.
* Uses @model and @name if provided, otherwise calls @onRemove.
*/
@action
handleRemove(rowData) {
// If an onRemove handler is provided, use it
if (this.args.onRemove) {
return this.args.onRemove(rowData);
}
const { model, name, properties } = this.args;
assert(
'FormFieldKeyValueComponent: model, name, and properties are required if no custom onRemove handler is provided.',
model && name && properties,
);
const rows = (model[name] ?? []).filter((item) => item !== rowData);
// If removing the last row, add an empty row back
if (rows.length === 0) {
const emptyRow = Object.fromEntries(properties.map((prop) => [prop, '']));
model[name] = [emptyRow];
} else {
model[name] = rows;
}
}
/**
* Default handler for updating a row.
* Uses @model and @name if provided, otherwise calls @onUpdate.
*/
@action
handleUpdate(rowData, property, event) {
// If an onUpdate handler is provided, use it
if (this.args.onUpdate) {
return this.args.onUpdate(rowData, property, event);
}
const { model, name } = this.args;
assert(
'FormFieldKeyValueComponent: model and name are required if no custom onUpdate handler is provided.',
model && name,
);
const value = event?.target?.value ?? event;
rowData[property] = value;
model[name] = [...(model[name] ?? [])];
}
}
@@ -1,33 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::Fieldset
@layout={{@layout}}
@isRequired={{@isRequired}}
@isOptional={{@isOptional}}
as |F|
>
{{yield F to='fieldset'}}
<F.Control>
{{yield
(hash
KeyValue=(component
'form/field/list-wrapper/key-value' disabled=@disabled
)
TextInput=(component
'form/field/list-wrapper/text-input' disabled=@disabled
)
Textarea=(component
'form/field/list-wrapper/textarea' disabled=@disabled
)
Select=(component 'form/field/list-wrapper/select' disabled=@disabled)
)
to='field'
}}
</F.Control>
</Hds::Form::Fieldset>
@@ -1,202 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
{{#let (unique-id) (unique-id) as |keyHeaderID valueHeaderID|}}
<Hds::Table class='list-wrapper-field' name={{@name}}>
<:head as |H|>
<H.Tr>
<H.Th id={{keyHeaderID}}>
{{#if @keyLabel}}
{{@keyLabel}}
{{else}}
{{t 'form.key.label'}}
{{/if}}</H.Th>
<H.Th id={{valueHeaderID}}>
{{#if @valueLabel}}
{{@valueLabel}}
{{else}}
{{t 'form.value.label'}}
{{/if}}
</H.Th>
<H.Th>
{{t 'titles.actions'}}
</H.Th>
</H.Tr>
</:head>
<:body as |B|>
{{#each this.options as |option index|}}
<B.Tr>
<B.Td>
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=option.key
ariaLabelledBy=keyHeaderID
setContext=(set-from-event option 'key')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=option.key
ariaLabelledBy=keyHeaderID
setContext=(set-from-event option 'key')
width=@width
)
)
to='key'
}}
</B.Td>
<B.Td>
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=option.value
ariaLabelledBy=valueHeaderID
setContext=(set-from-event option 'value')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=option.value
ariaLabelledBy=valueHeaderID
width=@width
setContext=(set-from-event option 'value')
)
)
to='value'
}}
</B.Td>
<B.Td>
{{#if this.removeOptionByIndex}}
<Hds::Button
data-test-remove-button
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{@disabled}}
{{on 'click' (fn this.removeOptionByIndex index)}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each}}
{{#if this.showNewRow}}
<B.Tr>
<B.Td>
{{#if (has-block 'newKey')}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionKey
ariaLabelledBy=keyHeaderID
setContext=(set-from-event this 'newOptionKey')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionKey
ariaLabelledBy=keyHeaderID
width=@width
setContext=(set-from-event this 'newOptionKey')
)
)
to='newKey'
}}
{{else}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionKey
ariaLabelledBy=keyHeaderID
setContext=(set-from-event this 'newOptionKey')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionKey
ariaLabelledBy=keyHeaderID
width=@width
setContext=(set-from-event this 'newOptionKey')
)
)
to='key'
}}
{{/if}}
</B.Td>
<B.Td>
{{#if (has-block 'newValue')}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionValue
ariaLabelledBy=valueHeaderID
setContext=(set-from-event this 'newOptionValue')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionValue
ariaLabelledBy=valueHeaderID
width=@width
setContext=(set-from-event this 'newOptionValue')
)
)
to='newValue'
}}
{{else}}
{{yield
(hash
text=(component
'form/field/list-wrapper/key-value/text'
disabled=@disabled
value=this.newOptionValue
ariaLabelledBy=valueHeaderID
setContext=(set-from-event this 'newOptionValue')
)
select=(component
'form/field/list-wrapper/key-value/select'
disabled=@disabled
value=this.newOptionValue
ariaLabelledBy=valueHeaderID
width=@width
setContext=(set-from-event this 'newOptionValue')
)
)
to='value'
}}
{{/if}}
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionKey)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
{{/if}}
</:body>
</Hds::Table>
{{/let}}
@@ -1,94 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { set } from '@ember/object';
export default class MappingListComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newOptionKey = '';
/**
* @type {string}
*/
@tracked newOptionValue = '';
/**
* Returns an array of key/value pair that the user enters
* @type {object}
*/
get options() {
return this.args?.options || this.args?.model?.[this.args.name];
}
/**
* Determines if we need to show an empty row to the users to enter more key/value pairs based on removeDuplicates arg,
* by default it is true
* @type {object}
*/
get showNewRow() {
if (this.args.showNewRow) {
return this.args.showNewRow();
} else {
return true;
}
}
// =actions
/**
* If a new key value is entered and an addOption method was specified,
* calls addOption with the new key and value. Resets key and value.
* Otherwise use the model argument to create the array and update the model
*/
@action
addOption() {
if (this.args.addOption) {
if (this.newOptionKey) {
this.args.addOption({
key: this.newOptionKey,
value: this.newOptionValue,
});
}
} else {
const field = this.args.name;
const existingArray = this.args.model[field] ?? [];
const newArray = [
...existingArray,
{ key: this.newOptionKey, value: this.newOptionValue },
];
set(this.args.model, field, newArray);
}
this.newOptionKey = '';
this.newOptionValue = '';
}
/**
* If removeOptionByIndex method was passed, use that.
* Otherwise, use the model to remove an option by index. We recreate a new array after
* splicing out the item so that ember is aware that the array has been modified.
* @param index {number}
*/
@action
removeOptionByIndex(index) {
if (this.args.removeOptionByIndex) {
this.args.removeOptionByIndex(index);
} else {
const field = this.args.name;
const newArray = this.args.model[field].filter((_, i) => i !== index);
set(this.args.model, field, newArray);
}
}
}
@@ -1,24 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::Select::Base
@value={{@value}}
@width={{@width}}
disabled={{@disabled}}
aria-labelledby={{@ariaLabelledBy}}
{{on 'change' @setContext}}
as |F|
>
<F.Options>
<option hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each @selectOptions as |selectOption|}}
<option value={{selectOption}} selected={{eq selectOption @value}}>
{{selectOption}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Base>
@@ -1,11 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::TextInput::Field
@value={{@value}}
disabled={{@disabled}}
aria-labelledby={{@ariaLabelledBy}}
{{on 'input' @setContext}}
/>
@@ -1,116 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
{{#let (unique-id) (unique-id) as |keyHeaderID valueHeaderID|}}
<Hds::Table class='list-wrapper-field' name={{@name}}>
<:head as |H|>
<H.Tr>
<H.Th id={{keyHeaderID}}>{{t 'form.key.label'}}</H.Th>
<H.Th id={{valueHeaderID}}>{{t 'form.value.label'}}</H.Th>
<H.Th>
{{t 'titles.actions'}}
</H.Th>
</H.Tr>
</:head>
<:body as |B|>
{{#each-in this.options as |select val|}}
<B.Tr>
<B.Td>
<Hds::Form::Select::Base
@value={{select}}
disabled={{@disabled}}
@width={{@width}}
aria-labelledby={{keyHeaderID}}
{{on 'change' (fn this.selectChange select val)}}
as |F|
>
<F.Options>
{{#each-in @selectOptions as |key value|}}
<option value={{key}} selected={{eq select key}}>
{{value}}
</option>
{{/each-in}}
</F.Options>
</Hds::Form::Select::Base>
</B.Td>
<B.Td>
<Hds::Form::TextInput::Field
@value={{val}}
@type='text'
@width={{@width}}
disabled={{@disabled}}
aria-labelledby={{valueHeaderID}}
{{on 'change' (fn this.updateInput select)}}
/>
</B.Td>
<B.Td>
{{#if this.removeOptionByKey}}
<Hds::Button
data-test-remove-button={{select}}
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{@disabled}}
{{on 'click' (fn this.removeOptionByKey select)}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each-in}}
{{#if this.showNewRow}}
<B.Tr>
<B.Td>
<Hds::Form::Select::Base
@value={{this.newOptionKey}}
disabled={{@disabled}}
aria-labelledby={{keyHeaderID}}
@width={{@width}}
{{on 'change' (set-from-event this 'newOptionKey')}}
as |F|
>
<F.Options>
<option hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each-in this.selectOptions as |key value|}}
<option value={{key}} selected={{eq key this.newOptionKey}}>
{{value}}
</option>
{{/each-in}}
</F.Options>
</Hds::Form::Select::Base>
</B.Td>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newOptionValue}}
@type='text'
@width={{@width}}
disabled={{@disabled}}
aria-labelledby={{valueHeaderID}}
{{on 'input' (set-from-event this 'newOptionValue')}}
/>
</B.Td>
<B.Td>
<Hds::Button
data-test-select-text-add-btn
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionKey)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
{{/if}}
</:body>
</Hds::Table>
{{/let}}
@@ -1,137 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { set } from '@ember/object';
export default class MappingListComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newOptionKey = '';
/**
* @type {string}
*/
@tracked newOptionValue = '';
/**
* Returns an object of key/value pair that the user enters
* @type {object}
*/
get options() {
return this.args?.options || this.args?.model?.[this.args.name];
}
/**
* Determines if we need to show an empty row to the users to enter more key/value pairs based on removeDuplicates arg,
* by default it is true
* @type {object}
*/
get showNewRow() {
if (this.args.removeDuplicates) {
return (
Object.keys(this.options || {}).length !==
Object.keys(this.args.selectOptions || {}).length
);
} else {
return true;
}
}
/**
* Prevents users from selecting duplicate keys from the select list if there's the arg is set to true
* @type {object}
*/
get selectOptions() {
const previouslySelectedKeys = Object.keys(this.options || {});
if (this.args.removeDuplicates && previouslySelectedKeys.length) {
const newObj = { ...this.args.selectOptions };
for (const key of Object.keys(newObj)) {
if (previouslySelectedKeys.includes(key)) {
delete newObj[key];
}
}
return newObj;
} else {
return this.args.selectOptions;
}
}
// =actions
/**
* Handles when then user makes changes to the select list
* @param oldkey {string}
* @param oldVal {string}
*/
@action
selectChange(oldkey, oldVal, { target: { value: newKey } }) {
const field = this.args.name;
const newObj = { ...this.args.model[field], [newKey]: oldVal };
delete newObj[oldkey];
set(this.args.model, field, newObj);
}
/**
* Handles input changes
* @param key {string}
*/
@action
updateInput(key, { target: { value: newValue } }) {
const field = this.args.name;
this.args.model[field] = {
...this.args.model[field],
[key]: newValue,
};
}
/**
* If a new key value is entered and an addOption method was specified,
* calls addOption with the new key and value. Resets key and value.
* Otherwise use the model argument to create the array and update the model
*/
@action
addOption() {
if (this.args.addOption) {
if (this.newOptionKey) {
this.args.addOption({
key: this.newOptionKey,
value: this.newOptionValue,
});
}
} else {
const field = this.args.name;
this.args.model[field] = {
...this.args.model[field],
[this.newOptionKey]: this.newOptionValue,
};
}
this.newOptionKey = '';
this.newOptionValue = '';
}
/**
* If removeOptionByIndex method was passed, use that.
* Otherwise, use the model to remove an option by key
* @param index {number}
*/
@action
removeOptionByKey(selectedKey) {
if (this.args.removeOptionByKey) {
this.args.removeOptionByKey(selectedKey);
} else {
const field = this.args.name;
const newObj = { ...this.args.model[field] };
delete newObj[selectedKey];
set(this.args.model, field, newObj);
}
}
}
@@ -1,90 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Table class='list-wrapper-field' name={{@name}}>
<:body as |B|>
{{#each this.options as |select index|}}
<B.Tr>
<B.Td>
<Hds::Form::Select::Base
@value={{select.value}}
@width={{@width}}
disabled={{@disabled}}
aria-label={{t 'titles.value'}}
{{on 'change' (set-from-event select 'value')}}
as |F|
>
<F.Options>
{{#each @selectOptions as |selectOption|}}
<option
value={{selectOption}}
selected={{eq select.value selectOption}}
>
{{selectOption}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Base>
</B.Td>
<B.Td>
{{#if this.removeOptionByIndex}}
<Hds::Button
data-test-remove-option-button={{select.value}}
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{@disabled}}
{{on 'click' (fn this.removeOptionByIndex index)}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each}}
<B.Tr>
<B.Td>
<Hds::Form::Select::Base
@value={{this.newOptionValue}}
@width={{@width}}
disabled={{@disabled}}
aria-label={{t 'titles.value'}}
{{on 'change' (set-from-event this 'newOptionValue')}}
as |F|
>
<F.Options>
<option disabled={{@disabled}} hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each @selectOptions as |selectOption|}}
<option
value={{selectOption}}
selected={{eq selectOption this.newOptionValue}}
>
{{selectOption}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Base>
</B.Td>
<B.Td>
<Hds::Button
data-test-add-option-button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionValue)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
@@ -1,61 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { set } from '@ember/object';
export default class MappingListSelectComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newOptionValue = '';
get options() {
return this.args.options || this.args.model[this.args.name];
}
/**
* If a new input is entered and an addOption method was specified,
* calls addOption with the new input. Resets previous value.
* Otherwise use the model argument to create the array and update the model
*/
@action
addOption() {
if (this.args.addOption && this.newOptionValue) {
this.args.addOption({
value: this.newOptionValue,
});
} else {
const field = this.args.name;
const existingArray = this.args.model[field] ?? [];
const newArray = [...existingArray, { value: this.newOptionValue }];
set(this.args.model, field, newArray);
}
this.newOptionValue = '';
}
/**
* If removeOptionByIndex method was passed, use that.
* Otherwise, use the model to remove an option by index. We recreate a new array after
* splicing out the item so that ember is aware that the array has been modified.
* @param index {number}
*/
@action
removeOptionByIndex(index) {
if (this.args.removeOptionByIndex) {
this.args.removeOptionByIndex(index);
} else {
const field = this.args.name;
const newArray = this.args.model[field].filter((_, i) => i !== index);
set(this.args.model, field, newArray);
}
}
}
@@ -1,62 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Table class='list-wrapper-field' name={{@name}}>
<:body as |B|>
{{#each @options as |option index|}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{option.value}}
@type='text'
disabled={{@disabled}}
aria-label={{t 'titles.value'}}
{{on 'input' (set-from-event option 'value')}}
/>
</B.Td>
<B.Td>
{{#if this.removeOptionByIndex}}
<Hds::Button
data-test-remove-button
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{@disabled}}
{{on 'click' (fn this.removeOptionByIndex index)}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newOptionValue}}
@type='text'
disabled={{@disabled}}
aria-label={{t 'titles.value'}}
{{on 'input' (set-from-event this 'newOptionValue')}}
/>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionValue)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
@@ -1,61 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { set } from '@ember/object';
export default class MappingListTextInputComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newOptionValue = '';
// =actions
/**
* If a new input is entered and an addOption method was specified,
* calls addOption with the new input. Resets previous value.
* Otherwise use the model argument to create the array and update the model
*/
@action
addOption() {
if (this.args.addOption) {
if (this.newOptionValue) {
this.args.addOption({
value: this.newOptionValue,
});
}
} else {
const field = this.args.name;
const existingArray = this.args.model[field] ?? [];
const newArray = [...existingArray, { value: this.newOptionValue }];
set(this.args.model, field, newArray);
}
this.newOptionValue = '';
}
/**
* If removeOptionByIndex method was passed, use that.
* Otherwise, use the model to remove an option by index. We recreate a new array after
* splicing out the item so that ember is aware that the array has been modified.
* @param index {number}
*/
@action
removeOptionByIndex(index) {
if (this.args.removeOptionByIndex) {
this.args.removeOptionByIndex(index);
} else {
const field = this.args.name;
const newArray = this.args.model[field].filter((_, i) => i !== index);
set(this.args.model, field, newArray);
}
}
}
@@ -1,60 +0,0 @@
{{!
Copyright IBM Corp. 2021, 2026
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Table class='list-wrapper-field' name={{@name}}>
<:body as |B|>
{{#each @options as |option index|}}
<B.Tr>
<B.Td>
<Hds::Form::Textarea::Field
@value={{option.value}}
@type='text'
disabled={{@disabled}}
aria-label={{t 'titles.value'}}
/>
</B.Td>
<B.Td>
{{#if this.removeOptionByIndex}}
<Hds::Button
data-test-remove-button
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{@disabled}}
{{on 'click' (fn this.removeOptionByIndex index)}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each}}
<B.Tr>
<B.Td>
<Hds::Form::Textarea::Field
@value={{this.newOptionValue}}
@type='text'
disabled={{@disabled}}
aria-label={{t 'titles.value'}}
/>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionValue)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
@@ -1,61 +0,0 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { set } from '@ember/object';
export default class MappingListTextAreaComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newOptionValue = '';
// =actions
/**
* If a new input is entered and an addOption method was specified,
* calls addOption with the new input. Resets previous value.
* Otherwise use the model argument to create the array and update the model
*/
@action
addOption() {
if (this.args.addOption) {
if (this.newOptionValue) {
this.args.addOption({
value: this.newOptionValue,
});
}
} else {
const field = this.args.name;
const existingArray = this.args.model[field] ?? [];
const newArray = [...existingArray, { value: this.newOptionValue }];
set(this.args.model, field, newArray);
}
this.newOptionValue = '';
}
/**
* If removeOptionByIndex method was passed, use that.
* Otherwise, use the model to remove an option by index. We recreate a new array after
* splicing out the item so that ember is aware that the array has been modified.
* @param index {number}
*/
@action
removeOptionByIndex(index) {
if (this.args.removeOptionByIndex) {
this.args.removeOptionByIndex(index);
} else {
const field = this.args.name;
const newArray = this.args.model[field].filter((_, i) => i !== index);
set(this.args.model, field, newArray);
}
}
}
@@ -4,6 +4,7 @@
}}
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@@ -192,7 +193,7 @@
{{! role_arn }}
<Hds::Form::TextInput::Field
name='role_arn'
@isOptional={{true}}
@isRequired={{true}}
@value={{@model.role_arn}}
@isInvalid={{@model.errors.role_arn}}
disabled={{form.disabled}}
@@ -271,50 +272,44 @@
</Hds::Form::TextInput::Field>
{{! role_tags }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
<Form::Field::KeyValue
@name='role_tags'
@model={{@model}}
@properties={{array 'key' 'value'}}
@data={{@model.role_tags}}
@legend={{t 'resources.storage-bucket.form.role_tags.label'}}
@helperText={{t 'resources.storage-bucket.form.role_tags.help'}}
@disabled={{form.disabled}}
@isOptional={{true}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_tags.help'}}
</F.HelperText>
{{#if @model.errors.role_tags}}
<F.Error as |E|>
{{#each @model.errors.role_tags as |error|}}
<E.Message>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
<:row as |R|>
<R.Field as |F|>
<F.Label>{{t 'form.key.label'}}</F.Label>
<F.TextInput
data-test-key-input
@value={{R.rowData.key}}
{{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
<R.Field as |F|>
<F.Label>{{t 'form.value.label'}}</F.Label>
<F.TextInput
data-test-value-input
@value={{R.rowData.value}}
{{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:fieldset>
<:field as |F|>
<F.KeyValue
@name='role_tags'
@options={{@model.role_tags}}
@model={{@model}}
>
<:key as |K|>
<K.text />
</:key>
<:value as |V|>
<V.text />
</:value>
</F.KeyValue>
</:field>
</Form::Field::ListWrapper>
</:row>
</Form::Field::KeyValue>
{{else}}
<Hds::Form::TextInput::Field
name='access_key_id'
@value={{@model.access_key_id}}
@isOptional={{true}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
@@ -333,7 +328,7 @@
<Hds::Form::TextInput::Field
name='secret_access_key'
@value={{@model.secret_access_key}}
@isOptional={{true}}
@isRequired={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
@@ -4,6 +4,7 @@
}}
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@@ -4,6 +4,7 @@
}}
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff. Show more