Compare commits

...
106 Commits
Author SHA1 Message Date
Zhihe Li ed9371fab7 chore: 🤖 Update CLI version to correct one (#3336) 2026-07-10 12:17:57 -04:00
Zhihe Li 68a01cfa3a chore: 🤖 Update CLI version (#3335) 2026-07-10 10:17:51 -04:00
Priya Patel b7129e38a8 fix: 🐛 credential library update (#3328)
* fix: 🐛 update credential library serializer

* test: 💍 adds acceptance test

* test: 💍 fix test

* fix: 🐛 fix test

* test: 💍 adds a11y ignore

* chore: 🤖 pr feedback
2026-07-08 14:38:48 -07:00
Lisbet Alvarez 26e27fd24b fix: 🐛 code mirror lint tooltip format issues (#3325)
* fix: 🐛 code mirror lint tooltip format issues

* refactor: 💡 add fallback for parent config

* refactor: 💡 use getOwner to retrieve actual app root element
2026-06-19 12:52:48 -07:00
Zhihe Li d0612c0a46 feat: 🎸 No-op linting errors should be warnings (#3326) 2026-06-19 15:15:58 -04:00
Zhihe Li 347107c109 feat: 🎸 Add discard modal for edit grants page (#3324) 2026-06-18 13:35:49 -04:00
Lisbet Alvarez 0dac4ccf47 Use codemirror tooltip extension to set parent to fix positioning bug (#3323)
* fix: 🐛 absolute positioning of cm tooltip in firefox

* docs: ✏️ add comment

* fix: 🐛 tooltip hidden in editor fullscreen mode & dark mode

* fix: 🐛 tooltip color in dark mode

* docs: ✏️ add comment
2026-06-18 09:59:38 -07:00
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
253 changed files with 12715 additions and 7221 deletions

No files matched your search

+10 -10
View File
@@ -13,18 +13,18 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
timeout-minutes: 40 timeout-minutes: 40
steps: steps:
- uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2.1.1 - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
with: with:
install-dependencies: true install-dependencies: true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
run_install: false 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 - run: pnpm install
- name: Run a11y tests - name: Run a11y tests
env: env:
@@ -37,11 +37,11 @@ jobs:
jq -s 'map(.[])' ./ui/admin/ember-a11y-report/*.json > ember-a11y-reports/admin.json 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 jq -s 'map(.[])' ./ui/desktop/ember-a11y-report/*.json > ember-a11y-reports/desktop.json
- name: Output ember a11y test report summary (admin) - 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) - 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 - name: Upload ember a11y reports
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: ember-a11y-reports name: ember-a11y-reports
path: ./ember-a11y-reports path: ./ember-a11y-reports
+31 -31
View File
@@ -12,14 +12,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
run_install: false 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 - run: pnpm install --frozen-lockfile
build-admin-ui-oss: build-admin-ui-oss:
name: Build Admin UI OSS name: Build Admin UI OSS
@@ -27,24 +27,24 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: with:
run_install: false run_install: false
- name: Build cache: true
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 cache_dependency_path: '**/pnpm-lock.yaml'
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --offline - run: pnpm install --offline
- run: pnpm run build:ui:admin:oss - run: pnpm run build:ui:admin:oss
- name: Upload artifact admin-ui - name: Upload artifact admin-ui
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: admin-ui name: admin-ui
path: ./ui/admin/dist/ path: ./ui/admin/dist/
- name: Upload artifact OSS admin UI - name: Upload artifact OSS admin UI
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: admin-ui-oss name: admin-ui-oss
path: ./ui/admin/dist/ path: ./ui/admin/dist/
@@ -54,19 +54,19 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: with:
run_install: false run_install: false
- name: Build cache: true
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 cache_dependency_path: '**/pnpm-lock.yaml'
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --offline - run: pnpm install --offline
- run: pnpm run build:ui:admin:enterprise - run: pnpm run build:ui:admin:enterprise
- name: Upload artifact enterprise admin UI - name: Upload artifact enterprise admin UI
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: admin-ui-ent name: admin-ui-ent
path: ./ui/admin/dist/ path: ./ui/admin/dist/
@@ -76,19 +76,19 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: with:
run_install: false run_install: false
- name: Build cache: true
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 cache_dependency_path: '**/pnpm-lock.yaml'
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --offline - run: pnpm install --offline
- run: pnpm run build:ui:admin:hcp - run: pnpm run build:ui:admin:hcp
- name: Upload artifact HCP admin UI - name: Upload artifact HCP admin UI
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: admin-ui-hcp name: admin-ui-hcp
path: ./ui/admin/dist/ path: ./ui/admin/dist/
+2 -2
View File
@@ -33,9 +33,9 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node - name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 20 node-version: 24
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+1 -1
View File
@@ -6,6 +6,6 @@ jobs:
triage: triage:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with: with:
repo-token: "${{ secrets.GITHUB_TOKEN }}" repo-token: "${{ secrets.GITHUB_TOKEN }}"
+25 -25
View File
@@ -7,14 +7,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: '**/pnpm-lock.yaml'
run_install: false 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 - run: pnpm install --frozen-lockfile
compliance: compliance:
@@ -23,14 +23,14 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: with:
run_install: false run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 cache: true
with: cache_dependency_path: '**/pnpm-lock.yaml'
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --offline - run: pnpm install --offline
- run: pnpm run compliance:licenses - run: pnpm run compliance:licenses
@@ -41,14 +41,14 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: with:
run_install: false run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 cache: true
with: cache_dependency_path: '**/pnpm-lock.yaml'
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --offline - run: pnpm install --offline
- run: pnpm lint - run: pnpm lint
@@ -58,17 +58,17 @@ jobs:
runs-on: ${{ fromJSON(vars.RUNNER) }} runs-on: ${{ fromJSON(vars.RUNNER) }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2.1.1 - uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
with: with:
install-dependencies: true install-dependencies: true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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: with:
run_install: false run_install: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 cache: true
with: cache_dependency_path: '**/pnpm-lock.yaml'
node-version: 20
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- run: pnpm install --offline - run: pnpm install --offline
- run: pnpm test - run: pnpm test
+142 -54
View File
@@ -3,13 +3,6 @@ name: Test E2E Admin UI
on: on:
pull_request: pull_request:
types: [labeled, opened, synchronize, reopened] 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: workflow_dispatch:
inputs: inputs:
boundary-enterprise-branch: boundary-enterprise-branch:
@@ -24,30 +17,57 @@ on:
type: string type: string
jobs: jobs:
test: setup:
name: test name: setup matrix
# don't need to run e2e tests on ce -> ent merges since tests were already # 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 # the two repos
if: github.actor != 'hc-github-team-secure-boundary' 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) }} runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
strategy: strategy:
matrix: matrix: ${{ fromJSON(needs.setup.outputs.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' }}
fail-fast: false fail-fast: false
env: env:
ENOS_VAR_e2e_debug_no_run: true ENOS_VAR_e2e_debug_no_run: true
@@ -56,25 +76,25 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node - name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 20 node-version: 24
cache: "pnpm"
cache-dependency-path: | - name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: |
pnpm-lock.yaml pnpm-lock.yaml
ui/desktop/electron-app/pnpm-lock.yaml ui/desktop/electron-app/pnpm-lock.yaml
run_install: false
- name: Set up Terraform - name: Set up Terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e
- name: Set up Enos - name: Set up Enos
uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a uses: hashicorp/action-setup-enos@6ec106c8f809fe645162d73bea565c65f3269907
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -159,15 +179,89 @@ jobs:
run: | run: |
git config --global url."https://oauth2:${{ secrets.ELEVATED_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com" 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: | 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 cd e2e-tests
# Install browsers and dependencies based on the repository
if [[ "${{ github.repository }}" == "hashicorp/boundary-ui" ]]; then if [[ "${{ github.repository }}" == "hashicorp/boundary-ui" ]]; then
pnpm playwright install --with-deps pnpm playwright install --with-deps
elif [[ "${{ github.repository }}" == "hashicorp/boundary-ui-enterprise" ]]; then elif [[ "${{ github.repository }}" == "hashicorp/boundary-ui-enterprise" ]]; then
# playwright dependencies can't be installed on a self-hosted runner # Use raw apt-get to install system libraries without npx
# due to permission issues 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 pnpm playwright install
fi fi
@@ -178,7 +272,7 @@ jobs:
echo "go-version=$(cat ./support/src/${{matrix.folder_name}}/.go-version)" >> "$GITHUB_OUTPUT" echo "go-version=$(cat ./support/src/${{matrix.folder_name}}/.go-version)" >> "$GITHUB_OUTPUT"
- name: Set up Go - name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: ${{ steps.get-go-version.outputs.go-version }} go-version: ${{ steps.get-go-version.outputs.go-version }}
cache: false cache: false
@@ -217,19 +311,13 @@ jobs:
run: | run: |
source <(bash ./support/src/${{matrix.folder_name}}/enos/scripts/test_e2e_env.sh) source <(bash ./support/src/${{matrix.folder_name}}/enos/scripts/test_e2e_env.sh)
cd e2e-tests cd e2e-tests
if [[ "${{ github.repository }}" == "hashicorp/boundary-ui" ]]; then pnpm run ${{ matrix.test_command }} --project=${{ matrix.browser}} --reporter=html
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
- name: Upload Playwright report - name: Upload Playwright report
if: ${{ failure() }} if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: playwright-report-${{ matrix.boundary_edition }} name: playwright-report-${{ matrix.boundary_edition }}-${{ matrix.browser }}
path: e2e-tests/playwright-report path: e2e-tests/playwright-report
- name: Clean up test infra - 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 enos scenario destroy --timeout 60m0s --chdir ./support/src/${{matrix.folder_name}}/enos ${{matrix.enos_scenario}} builder:local
- name: Send Slack message on failure - name: Send Slack message on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }} if: ${{ failure() && github.repository == 'hashicorp/boundary-ui-enterprise' }}
with: with:
method: chat.postMessage method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }} token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: | payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }} 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: on:
pull_request: pull_request:
types: [labeled, opened, synchronize, reopened] 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: workflow_dispatch:
inputs: inputs:
boundary-enterprise-branch: boundary-enterprise-branch:
@@ -27,9 +20,10 @@ jobs:
test: test:
name: test name: test
# don't need to run e2e tests on ce -> ent merges since tests were already # 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 # 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) }} runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
env: env:
ENOS_VAR_boundary_edition: "ent" ENOS_VAR_boundary_edition: "ent"
@@ -39,25 +33,25 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
with:
run_install: false
- name: Set up Node - name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 20 node-version: 24
cache: "pnpm"
cache-dependency-path: | - name: Set up pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 #v6.0.8
with:
cache: true
cache_dependency_path: |
pnpm-lock.yaml pnpm-lock.yaml
ui/desktop/electron-app/pnpm-lock.yaml ui/desktop/electron-app/pnpm-lock.yaml
run_install: false
- name: Set up Terraform - name: Set up Terraform
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e
- name: Set up Enos - name: Set up Enos
uses: hashicorp/action-setup-enos@17b90fcf9591275b468a94aefb9dc6a93017de8a uses: hashicorp/action-setup-enos@6ec106c8f809fe645162d73bea565c65f3269907
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -113,7 +107,7 @@ jobs:
echo "go-version=$(cat ./support/src/boundary-enterprise/.go-version)" >> "$GITHUB_OUTPUT" echo "go-version=$(cat ./support/src/boundary-enterprise/.go-version)" >> "$GITHUB_OUTPUT"
- name: Set up Go - name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: ${{ steps.get-go-version.outputs.go-version }} go-version: ${{ steps.get-go-version.outputs.go-version }}
cache: false cache: false
@@ -166,7 +160,7 @@ jobs:
- name: Upload Playwright report - name: Upload Playwright report
if: ${{ failure() }} if: ${{ failure() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: playwright-report name: playwright-report
path: e2e-tests/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 enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:local
- name: Send Slack message on failure - name: Send Slack message on failure
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }} if: ${{ failure() && github.repository == 'hashicorp/boundary-ui-enterprise' }}
with: with:
method: chat.postMessage method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }} token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: | payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }} 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 // which indicates that there are more items in the list
// or the batch size has not been reached // or the batch size has not been reached
do { do {
result = await super.query(store, schema, queryObject); try {
//add the result items to a data array 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) { if (result && result.items) {
data.push(...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; queryObject.list_token = result.list_token;
} }
} while ( } while (
@@ -237,6 +260,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* - `403 isForbidden`: the session is authenticated but does not have * - `403 isForbidden`: the session is authenticated but does not have
* permission to perform the requested action * permission to perform the requested action
* - `404 isNotFound`: the requested resource could not be found * - `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 * - `500 isServer`: an internal server error occurred
* - `isUnknown`: an error occurred, but we don't know which or * - `isUnknown`: an error occurred, but we don't know which or
* we don't distinguish it yet * we don't distinguish it yet
@@ -248,7 +272,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
* @param {Object} payload * @param {Object} payload
* @return {Array} errors payload * @return {Array} errors payload
*/ */
normalizeErrorResponse(/*status, headers, payload*/) { normalizeErrorResponse(status, headers /*, payload*/) {
const errors = super.normalizeErrorResponse(...arguments); const errors = super.normalizeErrorResponse(...arguments);
if (isArray(errors)) { if (isArray(errors)) {
errors.forEach((error) => { errors.forEach((error) => {
@@ -262,6 +286,14 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
case 404: case 404:
error.isNotFound = true; error.isNotFound = true;
break; 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: case 500:
error.isServer = true; error.isServer = true;
break; break;
@@ -96,19 +96,19 @@ export default class GeneratedSessionRecordingModel extends BaseModel {
}) })
duration; duration;
@attr('string', { @attr({
description: 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, readOnly: true,
}) })
state; recording_state;
@attr('string', { @attr('string', {
description: 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, readOnly: true,
}) })
error_details; availability;
@attr('date', { @attr('date', {
description: 'The time until a session recording is required to be stored.', description: 'The time until a session recording is required to be stored.',
@@ -122,4 +122,19 @@ export default class GeneratedSessionRecordingModel extends BaseModel {
readOnly: true, readOnly: true,
}) })
delete_after; 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, let payload,
listToken, listToken,
writeToDbPromise, writeToDbPromise,
totalInsert = 0; totalInsert = 0,
rateLimitWarning;
if (!peekDb) { 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) { if (storeToken) {
const [tokenObj] = await this.sqlite.fetchResource({ // Always prefer the global token when it exists for
...generateSQLExpressions('token', { // any more fine-grained query. The global query already fetched every
filters: { id: [{ equals: tokenKey }] }, // item for this resource type, so we should just re-use it.
}), if (tokenKey !== globalTokenKey) {
}); const [globalTokenObj] = await this.sqlite.fetchResource({
listToken = tokenObj?.token; ...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 { } else {
// This is a temporary fix of clearing the DB (specifically for auth-methods) // 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 // 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; 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 // await the previous writeToDbPromise before writing to db again
if (writeToDbPromise) { if (writeToDbPromise) {
await writeToDbPromise; await writeToDbPromise;
@@ -163,7 +190,14 @@ export default class SqliteHandler {
// This isn't conventional but is better than returning an ArrayProxy // 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 // or EmberArray since the ember store query method asserts it has to be an array
// so we can't just return an object. // 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; return records;
} }
default: default:
@@ -48,6 +48,18 @@ export default class AuthMethodSerializer extends ApplicationSerializer {
} else { } else {
delete serialized.attributes.state; 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; return serialized;
} }
@@ -68,21 +68,25 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
*/ */
handleCredentialMappingOverrides(serialized, isNew) { handleCredentialMappingOverrides(serialized, isNew) {
const { credential_type, credential_mapping_overrides } = serialized; const { credential_type, credential_mapping_overrides } = serialized;
if (Object.keys(credential_mapping_overrides).length === 0) { if (Object.keys(credential_mapping_overrides).length === 0) {
serialized.credential_mapping_overrides = null; serialized.credential_mapping_overrides = null;
} }
// API expects to send null to fields if it is undefined or deleted
if (credential_mapping_overrides && !isNew) { if (!credential_mapping_overrides || isNew || !credential_type) {
serialized.credential_mapping_overrides = options.mapping_overrides[ return;
credential_type
].reduce((obj, key) => {
if (credential_mapping_overrides[key]) {
obj[key] = credential_mapping_overrides[key];
} else {
obj[key] = null;
}
return obj;
}, {});
} }
// API expects to send null to fields if it is undefined or deleted
serialized.credential_mapping_overrides = options.mapping_overrides[
credential_type
].reduce((obj, key) => {
if (credential_mapping_overrides[key]) {
obj[key] = credential_mapping_overrides[key];
} else {
obj[key] = null;
}
return obj;
}, {});
} }
} }
+58 -16
View File
@@ -46,6 +46,15 @@ import {
// See https://sqlite.org/rescode.html#corrupt // See https://sqlite.org/rescode.html#corrupt
const SQLITE_CORRUPT = 11; 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 = { const methods = {
initializeSQLite: async () => { initializeSQLite: async () => {
sqlite3 = await sqlite3InitModule({ sqlite3 = await sqlite3InitModule({
@@ -57,6 +66,9 @@ import {
}); });
function setupDb() { 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' }); const [row] = db.exec('PRAGMA user_version;', { rowMode: 'object' });
// Check if we're on a newer schema version and clear the database if so // Check if we're on a newer schema version and clear the database if so
if (SCHEMA_VERSION > row.user_version) { if (SCHEMA_VERSION > row.user_version) {
@@ -97,30 +109,40 @@ import {
if (poolUtil.getCapacity() > 20) { if (poolUtil.getCapacity() > 20) {
await poolUtil.removeVfs(); 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); console.error('SQLite initialization error:', err);
try {
// We might have gotten ourselves into a bad state which corrupted the DB so clear it out and retry if (db) {
if (err?.resultCode === SQLITE_CORRUPT) { db.close();
db = null;
}
poolUtil.unlink(`/${dbName}`); poolUtil.unlink(`/${dbName}`);
} else { } catch (cleanupErr) {
throw err; // 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: () => { analyzeDatabase: () => {
requireDb();
db.exec('PRAGMA optimize'); db.exec('PRAGMA optimize');
}, },
clearDatabase: () => { clearDatabase: () => {
requireDb();
db.exec(CLEAR_DB); db.exec(CLEAR_DB);
db.exec(CREATE_TABLES(SCHEMA_VERSION)); db.exec(CREATE_TABLES(SCHEMA_VERSION));
}, },
deleteDatabase: () => { deleteDatabase: () => {
if (!db) { requireDb();
throw new Error('No database was initialized');
}
const name = db.dbFilename(); const name = db.dbFilename();
db.close(); db.close();
@@ -128,9 +150,11 @@ import {
poolUtil.unlink(`/${name}`); poolUtil.unlink(`/${name}`);
}, },
downloadDatabase: () => { downloadDatabase: () => {
requireDb();
return sqlite3.capi.sqlite3_js_db_export(db); return sqlite3.capi.sqlite3_js_db_export(db);
}, },
fetchResource: ({ sql, parameters = [] }) => { fetchResource: ({ sql, parameters = [] }) => {
requireDb();
try { try {
return db.exec({ return db.exec({
sql, sql,
@@ -147,6 +171,7 @@ import {
if (items.length === 0) { if (items.length === 0) {
return []; return [];
} }
requireDb();
const numberOfParameters = items.reduce((acc, item) => { const numberOfParameters = items.reduce((acc, item) => {
return acc + item.length; 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 // 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 // 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 // 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) { if (numberOfParameters > MAX_HOST_PARAMETERS) {
const chunkSize = Math.floor(MAX_HOST_PARAMETERS / items[0].length); 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 = []; const results = [];
for (let i = 0; i < items.length; i += chunkSize) { for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize); const chunk = items.slice(i, i + chunkSize);
@@ -170,20 +199,29 @@ import {
}); });
results.push(result); results.push(result);
} }
db.exec('COMMIT');
return results; return results;
}); } catch (err) {
db.exec('ROLLBACK');
throw err;
}
} }
db.exec({ return db.exec({
sql: INSERT_STATEMENTS(resource, items, modelMapping), sql: INSERT_STATEMENTS(resource, items, modelMapping),
bind: items.flat(), bind: items.flat(),
rowMode: 'object', rowMode: 'object',
}); });
}, },
deleteResource: ({ resource, ids }) => { 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) { 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 = []; const results = [];
for (let i = 0; i < ids.length; i += MAX_HOST_PARAMETERS) { for (let i = 0; i < ids.length; i += MAX_HOST_PARAMETERS) {
const chunk = ids.slice(i, i + MAX_HOST_PARAMETERS); const chunk = ids.slice(i, i + MAX_HOST_PARAMETERS);
@@ -194,8 +232,12 @@ import {
}); });
results.push(result); results.push(result);
} }
db.exec('COMMIT');
return results; return results;
}); } catch (err) {
db.exec('ROLLBACK');
throw err;
}
} }
return db.exec({ return db.exec({
@@ -169,6 +169,12 @@ const onBecomeProvider = async ({
target, target,
status, 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 // Set an event listener for other tabs to register with the provider tab
commonChannel.addEventListener('message', async (event) => { commonChannel.addEventListener('message', async (event) => {
const { clientId, type } = event.data; const { clientId, type } = event.data;
@@ -194,6 +200,9 @@ const onBecomeProvider = async ({
} }
const { method, args, id } = data; const { method, args, id } = data;
// Wait for the provider to finish initialization before handling requests
await providerReady;
let result, error; let result, error;
try { try {
result = await target[method](...args); result = await target[method](...args);
@@ -221,6 +230,7 @@ const onBecomeProvider = async ({
}); });
await onProviderChange?.(status.isServiceProvider); await onProviderChange?.(status.isServiceProvider);
providerReadyResolve();
commonChannel.postMessage({ type: 'providerChange', serviceName }); commonChannel.postMessage({ type: 'providerChange', serviceName });
// If we became the new provider and there were requests in flight, request it directly again. // 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 makeBooleanFilter from './helpers/bexpr-filter';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { asciicasts } from './data/asciicasts'; import { asciicasts } from './data/asciicasts';
import { grantsSchemaData } from './data/grants-schema';
import { TYPE_WORKER_PKI } from 'api/models/worker'; import { TYPE_WORKER_PKI } from 'api/models/worker';
import environmentConfig from 'ember-get-config'; import environmentConfig from 'ember-get-config';
@@ -210,6 +211,10 @@ function routes() {
return metadata; return metadata;
}); });
this.get('/internal/grants-schema.json', () => {
return grantsSchemaData;
});
// make this `/api`, for example, if your API is namespaced // make this `/api`, for example, if your API is namespaced
this.namespace = environmentConfig.api?.namespace; this.namespace = environmentConfig.api?.namespace;
// delay for each request, automatically set to 0 during testing // 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(), username: faker.internet.userName(),
key_bits: faker.number.int(999), key_bits: faker.number.int(999),
path: faker.system.directoryPath(), 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: case TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP:
return { return {
+1 -1
View File
@@ -26,7 +26,7 @@ export default factory.extend({
'set-grant-scopes', 'set-grant-scopes',
], ],
grant_strings: () => [ grant_strings: () => [
'ids=*;action=*', 'ids=*;actions=*',
'ids=*;type=host-catalog;actions=create,read', 'ids=*;type=host-catalog;actions=create,read',
], ],
@@ -10,8 +10,8 @@ import { faker } from '@faker-js/faker';
import { trait } from 'miragejs'; import { trait } from 'miragejs';
import { import {
TYPES_SESSION_RECORDING as types, TYPES_SESSION_RECORDING as types,
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_AVAILABLE, STATE_SESSION_RECORDING_AVAILABLE,
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_UNKNOWN, STATE_SESSION_RECORDING_UNKNOWN,
} from 'api/models/session-recording'; } from 'api/models/session-recording';
@@ -27,6 +27,29 @@ export default factory.extend({
// Cycle through available types // Cycle through available types
type: (i) => types[i % types.length], type: (i) => types[i % types.length],
state: (i) => states[i % states.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() { start_time() {
return this.state === STATE_SESSION_RECORDING_AVAILABLE return this.state === STATE_SESSION_RECORDING_AVAILABLE
? faker.date.recent({ days: 3, refDate: this.end_time }) ? 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) { module.exports = function (defaults) {
const app = new EmberAddon(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, name: require('./package').name,
options: { 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 // Don't automatically import sqlite, we need to manually control how it gets used
autoImport: { autoImport: {
exclude: ['@sqlite.org/sqlite-wasm'], exclude: ['@sqlite.org/sqlite-wasm'],
+5 -4
View File
@@ -34,8 +34,9 @@
"broccoli-source": "^3.0.1", "broccoli-source": "^3.0.1",
"ember-auto-import": "^2.10.0", "ember-auto-import": "^2.10.0",
"ember-can": "^8.0.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-cli-htmlbars": "^6.3.0",
"ember-concurrency": "^4.0.3",
"ember-data": "~5.3.12", "ember-data": "~5.3.12",
"ember-get-config": "^2.1.1", "ember-get-config": "^2.1.1",
"ember-inflector": "^6.0.0", "ember-inflector": "^6.0.0",
@@ -43,7 +44,7 @@
"promise-worker-bi": "^5.0.1", "promise-worker-bi": "^5.0.1",
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1", "js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
"tracked-built-ins": "^4.0.0", "tracked-built-ins": "^4.0.0",
"uuid": "^11.0.3" "uuid": "^14.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.25.1", "@babel/eslint-parser": "^7.25.1",
@@ -55,7 +56,7 @@
"@glimmer/component": "^2.0.0", "@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0", "broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0", "concurrently": "^10.0.3",
"doctoc": "^2.2.0", "doctoc": "^2.2.0",
"ember-cli": "~5.12.0", "ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0", "ember-cli-clean-css": "^3.0.0",
@@ -92,7 +93,7 @@
"sinon": "^19.0.2" "sinon": "^19.0.2"
}, },
"engines": { "engines": {
"node": "20.* || 22.*" "node": "22.* || 24.*"
}, },
"ember": { "ember": {
"edition": "octane" "edition": "octane"
+1 -1
View File
@@ -30,7 +30,7 @@ module.exports = function (environment) {
}, },
mirage: { 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(401).errors[0].isUnauthenticated);
assert.ok(getResponse(403).errors[0].isForbidden); assert.ok(getResponse(403).errors[0].isForbidden);
assert.ok(getResponse(404).errors[0].isNotFound); assert.ok(getResponse(404).errors[0].isNotFound);
assert.ok(getResponse(429).errors[0].isRateLimited);
assert.ok(getResponse(500).errors[0].isServer); assert.ok(getResponse(500).errors[0].isServer);
assert.ok(getResponse(0).errors[0].isUnknown); assert.ok(getResponse(0).errors[0].isUnknown);
assert.ok(getResponse(false).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) { test('it returns field-level errors in InvalidError from handleResponse', function (assert) {
const adapter = this.owner.lookup('adapter:application'); const adapter = this.owner.lookup('adapter:application');
const payload = { const payload = {
@@ -470,6 +470,62 @@ module('Unit | Handler | sqlite-handler', function (hooks) {
assert.strictEqual(results[0].name, 'specific-target'); 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) { test('it retries query when invalid list token error occurs', async function (assert) {
let targets = this.server.createList('target', 5, { scope }); 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.client_certificate_key, '');
assert.strictEqual(record.bind_password, ''); 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',
]);
});
}); });
@@ -138,6 +138,45 @@ module('Unit | Serializer | credential library', function (hooks) {
}); });
}); });
test('it serializes credential library updates without credential_type', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('credential-library');
store.push({
data: {
id: '1',
type: 'credential-library',
attributes: {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
name: 'Name',
description: 'Description',
path: '/vault/path',
http_method: 'GET',
version: 1,
credential_type: null,
credential_mapping_overrides: {},
},
},
});
const record = store.peekRecord('credential-library', '1');
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_store_id: null,
name: 'Name',
description: 'Description',
credential_type: null,
credential_mapping_overrides: null,
attributes: {
path: '/vault/path',
http_method: 'GET',
http_request_body: null,
},
version: 1,
});
});
test('it does not serialize http_request_body unless http_method is set to POST', function (assert) { test('it does not serialize http_request_body unless http_method is set to POST', function (assert) {
const store = this.owner.lookup('service:store'); const store = this.owner.lookup('service:store');
const record = store.createRecord('credential-library', { const record = store.createRecord('credential-library', {
+3 -3
View File
@@ -28,7 +28,7 @@
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@ember/test-waiters": "^4.1.0", "@ember/test-waiters": "^4.1.0",
"ember-auto-import": "^2.10.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-cli-htmlbars": "^6.3.0",
"ember-simple-auth": "^6.1.0" "ember-simple-auth": "^6.1.0"
}, },
@@ -41,7 +41,7 @@
"@glimmer/component": "^2.0.0", "@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0", "broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0", "concurrently": "^10.0.3",
"doctoc": "^2.2.0", "doctoc": "^2.2.0",
"ember-cli": "~5.12.0", "ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0", "ember-cli-clean-css": "^3.0.0",
@@ -79,7 +79,7 @@
"ember-source": ">= 4.0.0" "ember-source": ">= 4.0.0"
}, },
"engines": { "engines": {
"node": "20.* || 22.*" "node": "22.* || 24.*"
}, },
"ember": { "ember": {
"edition": "octane" "edition": "octane"
@@ -12,6 +12,7 @@ import Component from '@glimmer/component';
* - 403 - `error.isForbidden`: the session is authenticated but does not have * - 403 - `error.isForbidden`: the session is authenticated but does not have
* permission to perform the requested action * permission to perform the requested action
* - 404 - `error.isNotFound`: the requested resource could not be found * - 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 * - 500 - `error.isServer`: an internal server error occurred
* *
* For an unknown error state, i.e error state not matching to the above defined list: * 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 * 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 { export default class ErrorMessageComponent extends Component {
// =methods // =methods
+3 -3
View File
@@ -37,7 +37,7 @@
"auth": "workspace:*", "auth": "workspace:*",
"ember-auto-import": "^2.10.0", "ember-auto-import": "^2.10.0",
"ember-browser-services": "^4.0.3", "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-htmlbars": "^6.3.0",
"ember-cli-sass": "^11.0.1", "ember-cli-sass": "^11.0.1",
"ember-concurrency": "^4.0.3", "ember-concurrency": "^4.0.3",
@@ -59,7 +59,7 @@
"@glimmer/component": "^2.0.0", "@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0", "broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0", "concurrently": "^10.0.3",
"doctoc": "^2.2.0", "doctoc": "^2.2.0",
"ember-cli": "~5.12.0", "ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0", "ember-cli-clean-css": "^3.0.0",
@@ -95,7 +95,7 @@
"webpack": "^5.105.4" "webpack": "^5.105.4"
}, },
"engines": { "engines": {
"node": "20.* || 22.*" "node": "22.* || 24.*"
}, },
"ember": { "ember": {
"edition": "octane" "edition": "octane"
@@ -52,6 +52,16 @@ module('Integration | Component | error/message', function (hooks) {
.hasText( .hasText(
'We could not find the requested resource. You can ask your administrator or try again later.', '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) { test('it renders default error for unknown error status', async function (assert) {
+3 -1
View File
@@ -23,13 +23,13 @@ retry: Retry
play: Play play: Play
pause: Pause pause: Pause
resume: Resume resume: Resume
export: Export
get-topic-help: Get help for this topic get-topic-help: Get help for this topic
copy-to-clipboard: Copy to Clipboard copy-to-clipboard: Copy to Clipboard
copy-error-detail-to-clipboard: Copy error detail to clipboard copy-error-detail-to-clipboard: Copy error detail to clipboard
authenticate: Sign In authenticate: Sign In
deauthenticate: Sign Out deauthenticate: Sign Out
choose-org: Choose Scope choose-org: Choose Scope
choose-different-org: Choose a different scope
add-host-set: Create Host Set add-host-set: Create Host Set
add-host: Create Host add-host: Create Host
add-principals: Add Principals add-principals: Add Principals
@@ -69,3 +69,5 @@ hide-errors: Hide Errors
edit-worker-filter: Edit Worker Filter edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter add-worker-filter: Add Worker Filter
open: Open open: Open
download: Download
show-more: Show more
+1
View File
@@ -24,6 +24,7 @@ titles:
empty-set: No Items Yet empty-set: No Items Yet
global: Global global: Global
actions: Actions actions: Actions
action: Action
documentation: Documentation documentation: Documentation
abandon-confirm: Discard unsaved changes? abandon-confirm: Discard unsaved changes?
welcome-to-boundary: Welcome to Boundary welcome-to-boundary: Welcome to Boundary
@@ -13,6 +13,11 @@
500: 500:
title: Server error title: Server error
description: We ran into a problem and could not continue. You can ask your administrator or try again later. 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: unknown:
title: Something went wrong title: Something went wrong
description: We're not sure what happened. Please contact your administrator or try again later. 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: tags:
label: Tags label: Tags
secret-editor: secret-editor:
label: Secret Editor
details: This secret is saved but won't be displayed details: This secret is saved but won't be displayed
edit: Click to replace this secret edit: Click to replace this secret
created: created:
+196 -5
View File
@@ -420,6 +420,13 @@ session-recording:
title: Channels by connection title: Channels by connection
messages: messages:
no-channels: There are no channels for this connection 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: started-no-connections:
title: Session Recording In Progress title: Session Recording In Progress
description: The Session has started but no connections have been made. description: The Session has started but no connections have been made.
@@ -611,6 +618,7 @@ role:
title: Grant title: Grant
title_plural: Grants title_plural: Grants
description: Grants are permissions which allow roles to take actions and access resources. 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: actions:
create: New Grant create: New Grant
grant-templates: grant-templates:
@@ -627,12 +635,18 @@ role:
global-admin: global-admin:
name: Global Admin name: Global Admin
description: Full administrative access to all resources in Boundary 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: org-admin:
name: 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: project-admin:
name: 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: session-manager:
name: Session Manager name: Session Manager
description: Can view and cancel sessions description: Can view and cancel sessions
@@ -655,11 +669,188 @@ role:
name: Session Auditor name: Session Auditor
description: Can view session recordings description: Can view session recordings
target-user: target-user:
name: Target User name: User
description: Can connect to targets but cannot manage anything description: Can connect to targets but cannot manage anything
read-only-user: read-only-user:
name: Read-Only User name: Resource Auditor
description: Can view but not modify any resources 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: scope:
title: Scope title: Scope
title_plural: Scopes 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 { form {
[class*='hds-form-field--layout'], [class*='hds-form-field--layout'],
[class*='hds-form-group--layout'], [class*='hds-form-group--layout'],
[class='hds-form-key-value-inputs'],
.rose-form-actions.hds-button-set { .rose-form-actions.hds-button-set {
margin-bottom: 1.5rem; 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 { @mixin dark-mode-ember-dropdown-override {
.ember-basic-dropdown-content { .ember-basic-dropdown-content {
background-color: var(--token-color-palette-neutral-0); 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 */
// 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);
}
.cm-editor {
// 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) { @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) { .ember-application:not(.rose-theme-light) {
@include dark-mode; @include dark-mode;
@include dark-mode-modal-override; @include dark-mode-modal-override;
@include dark-mode-badge-count-override;
@include dark-mode-ember-dropdown-override; @include dark-mode-ember-dropdown-override;
@include dark-mode-app-header-override; @include dark-mode-app-header-override;
@include dark-mode-app-side-nav-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 { .ember-application.rose-theme-dark {
@include dark-mode; @include dark-mode;
@include dark-mode-modal-override; @include dark-mode-modal-override;
@include dark-mode-badge-count-override;
@include dark-mode-ember-dropdown-override; @include dark-mode-ember-dropdown-override;
@include dark-mode-app-header-override; @include dark-mode-app-header-override;
@include dark-mode-app-side-nav-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 // This list is sorted in ascending order to improve readability
@use 'code-editor';
@use 'form'; @use 'form';
@use 'frame'; @use 'frame';
@use 'layout'; @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' 'header header header'
'navigation navigation navigation' 'navigation navigation navigation'
'body body body'; 'body body body';
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, auto);
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
.rose-layout-page-header { .rose-layout-page-header {
-5
View File
@@ -12,11 +12,6 @@ module.exports = {
included(app) { included(app) {
this._super.included.apply(this, arguments); 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.includeHDSStyles(app);
this.includeFlightIcons(app); this.includeFlightIcons(app);
this.includePublic(app); this.includePublic(app);
+5 -7
View File
@@ -34,16 +34,14 @@
"@hashicorp/design-system-tokens": "^2.3.0", "@hashicorp/design-system-tokens": "^2.3.0",
"@hashicorp/flight-icons": "^3.10.0", "@hashicorp/flight-icons": "^3.10.0",
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10", "@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
"codemirror": "5.65.7",
"ember-auto-import": "^2.10.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-cli-htmlbars": "^6.3.0",
"ember-cli-sass": "^11.0.1", "ember-cli-sass": "^11.0.1",
"ember-focus-trap": "^1.0.1", "ember-focus-trap": "^1.0.1",
"ember-named-blocks-polyfill": "^0.2.5", "ember-named-blocks-polyfill": "^0.2.5",
"jsonlint": "^1.6.3",
"sass": "^1.69.5", "sass": "^1.69.5",
"uuid": "^11.0.3" "uuid": "^14.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.25.1", "@babel/eslint-parser": "^7.25.1",
@@ -54,7 +52,7 @@
"@glimmer/component": "^2.0.0", "@glimmer/component": "^2.0.0",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"broccoli-asset-rev": "^3.0.0", "broccoli-asset-rev": "^3.0.0",
"concurrently": "^9.1.0", "concurrently": "^10.0.3",
"doctoc": "^2.2.0", "doctoc": "^2.2.0",
"ember-cli": "~5.12.0", "ember-cli": "~5.12.0",
"ember-cli-clean-css": "^3.0.0", "ember-cli-clean-css": "^3.0.0",
@@ -84,7 +82,7 @@
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"globals": "^15.14.0", "globals": "^15.14.0",
"loader.js": "^4.7.0", "loader.js": "^4.7.0",
"postcss": "^8.4.31", "postcss": "^8.5.10",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"qunit": "^2.22.0", "qunit": "^2.22.0",
"qunit-dom": "^3.2.1", "qunit-dom": "^3.2.1",
@@ -98,7 +96,7 @@
"ember-source": ">= 4.0.0" "ember-source": ">= 4.0.0"
}, },
"engines": { "engines": {
"node": "20.* || 22.*" "node": "22.* || 24.*"
}, },
"ember": { "ember": {
"edition": "octane" "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 ```bash
doormat login doormat login
source <(doormat aws export --account boundary_team_acctest_dev) 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: ## Run tests:
Make sure you followed all the steps within the [Getting started section](#getting-started). 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`. - `cd boundary/enos` or `cd boundary_enterprise/enos`.
- `doormat login`. Login with Doormat. - `doormat login`. Login with Doormat.
- `eval "$(doormat aws export --account boundary_team_acctest_dev)"`. Exporting AWS env variables from doormat to your terminal. - `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. - `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). - 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. - `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 # 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 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 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) { async addGrantsToRole(grants) {
await this.page.getByRole('link', { name: 'Grants', exact: true }).click(); await this.page.getByRole('link', { name: 'Grants', exact: true }).click();
await this.page.getByRole('textbox', { name: 'New Grant' }).fill(grants); await this.page
await this.page.getByRole('button', { name: 'Add', exact: true }).click(); .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.page.getByRole('button', { name: 'Save', exact: true }).click();
await this.dismissSuccessAlert(); await this.dismissSuccessAlert();
await expect( await expect(
this.page.getByRole('textbox', { name: 'Grant', exact: true }), this.page.locator('.grant-codeblock .hds-code-block__code'),
).toHaveValue(grants); ).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('Region').fill(region);
await this.page.getByLabel('Access key ID').fill(accessKeyId); await this.page.getByLabel('Access key ID').fill(accessKeyId);
await this.page.getByLabel('Secret access key').fill(secretAccessKey); await this.page.getByLabel('Secret access key').fill(secretAccessKey);
await this.page
.getByLabel('Worker filter')
.first()
.scrollIntoViewIfNeeded();
await this.page await this.page
.getByLabel('Worker filter') .getByLabel('Worker filter')
.getByRole('textbox') .getByRole('textbox')
+9 -3
View File
@@ -58,7 +58,7 @@ export class TargetsPage extends BaseResourcePage {
if (alias) { if (alias) {
await this.page await this.page
.getByRole('group', { name: 'Aliases' }) .getByRole('group', { name: 'Aliases' })
.getByLabel('value') .getByLabel('Aliases')
.last() .last()
.fill(alias); .fill(alias);
await this.page.getByRole('button', { name: 'Add' }).click(); await this.page.getByRole('button', { name: 'Add' }).click();
@@ -145,7 +145,10 @@ export class TargetsPage extends BaseResourcePage {
.getByText('Edit Ingress Worker Filter'), .getByText('Edit Ingress Worker Filter'),
).toBeVisible(); ).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.page.getByRole('button', { name: 'Save', exact: true }).click();
await this.dismissSuccessAlert(); await this.dismissSuccessAlert();
@@ -174,7 +177,10 @@ export class TargetsPage extends BaseResourcePage {
.getByText('Edit Egress Worker Filter'), .getByText('Edit Egress Worker Filter'),
).toBeVisible(); ).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.page.getByRole('button', { name: 'Save', exact: true }).click();
await this.dismissSuccessAlert(); await this.dismissSuccessAlert();
@@ -174,8 +174,8 @@ test(
// Log in using ldap account // Log in using ldap account
await loginPage.logout(adminLoginName); await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click(); await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('link', { name: orgName }).click(); await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: ldapAuthMethodName }).click(); await page.getByRole('link', { name: ldapAuthMethodName }).click();
await loginPage.login(ldapUserName, ldapUserPassword); await loginPage.login(ldapUserName, ldapUserPassword);
await expect( await expect(
@@ -193,6 +193,9 @@ test(
// View the LDAP account and verify account attributes // View the LDAP account and verify account attributes
await page.getByRole('link', { name: orgName }).click(); await page.getByRole('link', { name: orgName }).click();
await expect(
page.getByRole('heading', { name: 'Projects' }),
).toBeVisible();
await page await page
.getByRole('navigation', { name: 'Primary' }) .getByRole('navigation', { name: 'Primary' })
.getByRole('link', { name: 'Auth Methods' }) .getByRole('link', { name: 'Auth Methods' })
@@ -129,8 +129,8 @@ test(
// Log in using oidc account // Log in using oidc account
await loginPage.logout(adminLoginName); await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click(); await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('link', { name: orgName }).click(); await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: oidcAuthMethodName }).click(); await page.getByRole('link', { name: oidcAuthMethodName }).click();
const pagePromise = context.waitForEvent('page'); const pagePromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Sign In' }).click(); await page.getByRole('button', { name: 'Sign In' }).click();
@@ -58,8 +58,8 @@ test(
// Log in using new account // Log in using new account
await loginPage.logout(adminLoginName); await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click(); await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('link', { name: orgName }).click(); await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: authMethodName }).click(); await page.getByRole('link', { name: authMethodName }).click();
await loginPage.login(username, password); await loginPage.login(username, password);
@@ -97,8 +97,8 @@ test(
// Log in using new password // Log in using new password
await loginPage.logout(adminLoginName); await loginPage.logout(adminLoginName);
await page.getByText('Choose a different scope').click(); await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('link', { name: orgName }).click(); await page.getByRole('option', { name: orgName }).click();
await page.getByRole('link', { name: authMethodName }).click(); await page.getByRole('link', { name: authMethodName }).click();
await loginPage.login(username, newPassword); await loginPage.login(username, newPassword);
await expect( await expect(
@@ -52,8 +52,8 @@ test(
// Log in using new account // Log in using new account
await page.reload(); await page.reload();
await page.getByText('Choose a different scope').click(); await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('link', { name: org.name }).click(); await page.getByRole('option', { name: org.name }).click();
await page.getByRole('link', { name: authMethod.name }).click(); await page.getByRole('link', { name: authMethod.name }).click();
const loginPage = new LoginPage(page); const loginPage = new LoginPage(page);
await loginPage.login(username, password); await loginPage.login(username, password);
@@ -79,8 +79,8 @@ test(
// Confirm user cannot log in with old password // Confirm user cannot log in with old password
await loginPage.logout(username); await loginPage.logout(username);
await page.getByText('Choose a different scope').click(); await page.getByRole('button', { name: /global/i }).click();
await page.getByRole('link', { name: org.name }).click(); await page.getByRole('option', { name: org.name }).click();
await page.getByRole('link', { name: authMethod.name }).click(); await page.getByRole('link', { name: authMethod.name }).click();
await page.getByLabel('Login Name').fill(username); await page.getByLabel('Login Name').fill(username);
await page.getByLabel('Password', { exact: true }).fill(password); 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.getByLabel('Description').fill('This is an automated test');
await page.getByRole('combobox', { name: 'Type' }).click(); await page.getByRole('combobox', { name: 'Type' }).click();
await page.getByRole('option', { name: 'JSON' }).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 testName = 'name-json';
const testPassword = 'password-json'; const testPassword = 'password-json';
const testId = 'id-json'; const testId = 'id-json';
+6 -6
View File
@@ -52,6 +52,9 @@ test(
.getByRole('navigation', { name: 'Primary' }) .getByRole('navigation', { name: 'Primary' })
.getByRole('link', { name: 'Host Catalogs' }) .getByRole('link', { name: 'Host Catalogs' })
.click(); .click();
await expect(
page.getByRole('heading', { name: 'Host Catalogs' }),
).toBeVisible();
await page.getByRole('link', { name: hostCatalogName }).click(); await page.getByRole('link', { name: hostCatalogName }).click();
const hostSetName2 = await hostCatalogsPage.createHostSet(); const hostSetName2 = await hostCatalogsPage.createHostSet();
@@ -233,19 +236,16 @@ test(
).toBeVisible(); ).toBeVisible();
await expect(page.getByText('"dev" in "/tags/type"')).toBeVisible(); await expect(page.getByText('"dev" in "/tags/type"')).toBeVisible();
await page await page.locator('.hds-code-editor').getByRole('textbox').click();
.locator('.CodeMirror')
.getByRole('textbox')
.click({ force: true });
await expect( await expect(
page.locator('.CodeMirror').getByRole('textbox'), page.locator('.hds-code-editor').getByRole('textbox'),
).toBeFocused(); ).toBeFocused();
const selectAllShortcut = const selectAllShortcut =
process.platform === 'darwin' ? 'Meta+A' : 'Control+A'; process.platform === 'darwin' ? 'Meta+A' : 'Control+A';
await page.keyboard.press(selectAllShortcut); await page.keyboard.press(selectAllShortcut);
await page.keyboard.press('Backspace'); await page.keyboard.press('Backspace');
await page await page
.locator('.CodeMirror') .locator('.hds-code-editor')
.getByRole('textbox') .getByRole('textbox')
.fill('"prod" in "/tags/type"'); .fill('"prod" in "/tags/type"');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
@@ -133,6 +133,8 @@ export const electronTest = test.extend({
electronPage: async ({ electronApp }, use) => { electronPage: async ({ electronApp }, use) => {
const page = await electronApp.firstWindow(); const page = await electronApp.firstWindow();
await page.evaluate(() => localStorage.clear()); 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 page.context().tracing.start({ screenshots: true, snapshots: true });
await use(page); 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} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
export default defineConfig({ export default defineConfig({
globalSetup: '../global-setup', globalSetup: './global-setup',
outputDir: './artifacts', outputDir: './artifacts',
workers: 1, // Tests need to be run in serial, otherwise there may be conflicts when using the CLI workers: 1, // Tests need to be run in serial, otherwise there may be conflicts when using the CLI
use: { use: {
+1
View File
@@ -4,6 +4,7 @@
*/ */
import { chromium, test as baseTest, mergeTests } from '@playwright/test'; import { chromium, test as baseTest, mergeTests } from '@playwright/test';
import { checkEnv } from './helpers/general.js'; import { checkEnv } from './helpers/general.js';
import { boundaryApiClientTest } from './helpers/boundary-api-client.js'; import { boundaryApiClientTest } from './helpers/boundary-api-client.js';
import { LoginPage } from './admin/pages/login.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/aliases.js';
export * from './boundary-cli/auth-methods.js'; export * from './boundary-cli/auth-methods.js';
export * from './boundary-cli/authenticate.js'; export * from './boundary-cli/authenticate.js';
export * from './boundary-cli/cache.js';
export * from './boundary-cli/connect.js'; export * from './boundary-cli/connect.js';
export * from './boundary-cli/credential-stores.js'; export * from './boundary-cli/credential-stores.js';
export * from './boundary-cli/credentials.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:firefox": "pnpm run admin --project=firefox --headed",
"admin:dev:safari": "pnpm run admin --project=webkit --headed", "admin:dev:safari": "pnpm run admin --project=webkit --headed",
"desktop": "pnpm run --filter desktop build && pnpm run desktop:dev", "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" "desktop:dev": "pnpm playwright test --config desktop/playwright.config.js --reporter=html"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@babel/eslint-parser": "^7.21.3", "@babel/eslint-parser": "^7.21.3",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@openapitools/openapi-generator-cli": "^2.30.0", "@openapitools/openapi-generator-cli": "^2.38.0",
"@playwright/test": "^1.56.1", "@playwright/test": "^1.60.0",
"concurrently": "^9.1.0", "concurrently": "^10.0.3",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
@@ -44,7 +45,7 @@
"tesseract.js": "^5.1.1" "tesseract.js": "^5.1.1"
}, },
"engines": { "engines": {
"node": "20.* || 22.*" "node": "22.* || 24.*"
}, },
"lint-staged": { "lint-staged": {
"*.js": [ "*.js": [
+9 -9
View File
@@ -34,27 +34,27 @@
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.0", "concurrently": "^10.0.3",
"doctoc": "^2.2.0", "doctoc": "^2.2.0",
"esbuild": "^0.25.4",
"git-cz": "^4.9.0", "git-cz": "^4.9.0",
"husky": "^9.1.6", "husky": "^9.1.6",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lint-staged": "^15.2.10", "lint-staged": "^15.5.2"
"esbuild": "^0.25.4"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"nomnom>underscore": "^1.12.1",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"css-select>nth-check": "^2.0.1", "css-select>nth-check": "^2.0.1",
"node-gyp": "^10.0.0",
"ansi-html": "^0.0.9", "ansi-html": "^0.0.9",
"fireworm>async": "^2.6.4", "fireworm>async": "^2.6.4",
"ember-cli-babel>@babel/runtime": "7.27.6", "ember-cli-babel>@babel/runtime": "7.27.6",
"ember-try-config>package-json": "^10.0.1", "ember-try-config>package-json": "^10.0.1",
"ember-loading>ember-concurrency": "^4.0.0", "ember-loading>ember-concurrency": "^4.0.0",
"tmp": "^0.2.5", "tmp": "^0.2.7",
"tar": "^7.5.10" "tar": "^7.5.10",
"nomnom>underscore": "^1.13.8",
"ember-cli>markdown-it": "^14.2.0"
}, },
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
"@parcel/watcher", "@parcel/watcher",
@@ -75,7 +75,7 @@
} }
}, },
"engines": { "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 { } else {
// if an array is provided and it's argument is a literal (number, boolean, string) then this is used for the test name // 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 // 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 { } else {
testName = [testName]; 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); tests.forEach(testHandler);
testsEach.forEach(testHandler); testsEach.forEach(testHandler);
const code = f.toSource(); const code = f.toSource();
// the codemod will insert a newline between existing import statements,
// this regex removes the extra new line
// if (existingEmberA11yImport.length > 0) {
return code.replace( return code;
/(?<newline>\n)(import .*ember-a11y-testing\/test-support.*)/g, } else {
'$2', // 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, '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 // 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 // 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. // lint check, but will fail if its any other pattern.
'selector-class-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) => message: (selector) =>
`Expected class selector "${selector}" to be kebab-case`, `Expected class selector "${selector}" to be kebab-case`,
@@ -82,37 +82,30 @@
{{/if}} {{/if}}
</Hds::Form::TextInput::Field> </Hds::Form::TextInput::Field>
<Form::Field::ListWrapper <Form::Field::KeyValue
@layout='horizontal' @name='certificates'
@isOptional={{true}} @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}} @disabled={{form.disabled}}
@isOptional={{true}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'resources.auth-method.form.certificates.label'}} <F.Textarea
</F.Legend> aria-label={{t 'resources.auth-method.form.certificates.label'}}
<F.HelperText> data-test-value-input
{{t 'resources.auth-method.form.certificates.help'}} @value={{R.rowData.value}}
</F.HelperText> {{on 'input' (fn R.updateRow 'value')}}
/>
{{#if @model.errors.certificates}} </R.Field>
<F.Error as |E|> {{#if R.canDelete}}
{{#each @model.errors.certificates as |error|}} <R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}} {{/if}}
</:fieldset> </:row>
</Form::Field::KeyValue>
<:field as |F|>
<F.Textarea
@name='certificates'
@options={{@model.certificates}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
{{#if @model.isNew}} {{#if @model.isNew}}
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
@@ -406,45 +399,46 @@
}}</F.HelperText> }}</F.HelperText>
</Hds::Form::TextInput::Field> </Hds::Form::TextInput::Field>
<Form::Field::ListWrapper <Form::field::KeyValue
@layout='horizontal' @name='account_attribute_maps'
@isOptional={{true}} @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}} @disabled={{form.disabled}}
@isOptional={{true}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'resources.auth-method.form.account_attribute_maps.label'}} <F.Label>{{t 'resources.auth-method.titles.from-attr'}}</F.Label>
</F.Legend> <F.TextInput
<F.HelperText> data-test-key-input
{{t 'resources.auth-method.form.account_attribute_maps.help'}} @value={{R.rowData.key}}
</F.HelperText> {{on 'input' (fn R.updateRow 'key')}}
/>
{{#if @model.errors.account_attribute_maps}} </R.Field>
<F.Error as |E|> <R.Field as |F|>
{{#each @model.errors.account_attribute_maps as |error|}} <F.Label>{{t 'resources.auth-method.titles.to-attr'}}</F.Label>
<E.Message>{{error.message}}</E.Message> <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}} {{/each}}
</F.Error> </F.Select>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}} {{/if}}
</:fieldset> </:row>
<:field as |F|> </Form::field::KeyValue>
<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>
<div class='ldap-section'> <div class='ldap-section'>
<Hds::Text::Display <Hds::Text::Display
@@ -98,7 +98,6 @@
name='client_id' name='client_id'
@value={{@model.client_id}} @value={{@model.client_id}}
@isRequired={{true}} @isRequired={{true}}
@isInvalid={{@model.errors.client_id}}
disabled={{form.disabled}} disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'client_id')}} {{on 'input' (set-from-event @model 'client_id')}}
as |F| as |F|
@@ -146,179 +145,164 @@
</Hds::Form::TextInput::Field> </Hds::Form::TextInput::Field>
{{/unless}} {{/unless}}
{{! Signing Algorithms }} {{! Signing algorithms }}
<Form::Field::ListWrapper <Form::Field::KeyValue
@layout='horizontal' @name='signing_algorithms'
@isOptional={{true}} @model={{@model}}
@properties={{array 'value'}}
@data={{@model.signing_algorithms}}
@legend={{t 'form.signing_algorithms.label'}}
@helperText={{t 'form.signing_algorithms.help'}}
@disabled={{form.disabled}} @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}} @isOptional={{true}}
@disabled={{form.disabled}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'form.allowed_audiences.label'}} <F.Select
</F.Legend> aria-label={{t 'form.signing_algorithms.label'}}
<F.HelperText> data-test-value-select
{{t 'form.allowed_audiences.help'}} {{on 'change' (fn R.updateRow 'value')}}
</F.HelperText> >
<option disabled hidden selected value=''>
{{#if @model.errors.allowed_audiences}} {{t 'titles.choose-an-option'}}
<F.Error as |E|> </option>
{{#each @model.errors.allowed_audiences as |error|}} {{#each this.signingAlgorithms as |algorithm|}}
<E.Message>{{error.message}}</E.Message> <option
value={{algorithm}}
selected={{eq R.rowData.value algorithm}}
>
{{algorithm}}
</option>
{{/each}} {{/each}}
</F.Error> </F.Select>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}} {{/if}}
</:fieldset> </:row>
</Form::Field::KeyValue>
<:field as |F|> {{! Allowed audiences }}
<F.TextInput <Form::Field::KeyValue
@name='allowed_audiences' @name='allowed_audiences'
@options={{@model.allowed_audiences}} @model={{@model}}
@model={{@model}} @properties={{array 'value'}}
/> @data={{@model.allowed_audiences}}
</:field> @legend={{t 'form.allowed_audiences.label'}}
@helperText={{t 'form.allowed_audiences.help'}}
</Form::Field::ListWrapper> @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 }} {{! Claims Scopes }}
<Form::Field::KeyValue
<Form::Field::ListWrapper @name='claims_scopes'
@layout='horizontal' @model={{@model}}
@isOptional={{true}} @properties={{array 'value'}}
@data={{@model.claims_scopes}}
@legend={{t 'form.claims_scopes.label'}}
@helperText={{t 'form.claims_scopes.help'}}
@disabled={{form.disabled}} @disabled={{form.disabled}}
@isOptional={{true}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'form.claims_scopes.label'}} <F.TextInput
</F.Legend> aria-label={{t 'form.claims_scopes.label'}}
<F.HelperText> data-test-value-input
{{t 'form.claims_scopes.help'}} @value={{R.rowData.value}}
</F.HelperText> {{on 'input' (fn R.updateRow 'value')}}
/>
{{#if @model.errors.claims_scopes}} </R.Field>
<F.Error as |E|> {{#if R.canDelete}}
{{#each @model.errors.claims_scopes as |error|}} <R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}} {{/if}}
</:fieldset> </:row>
</Form::Field::KeyValue>
<:field as |F|>
<F.TextInput
@name='claims_scopes'
@options={{@model.claims_scopes}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
{{! Account Claim Maps }} {{! Account Claim Maps }}
<Form::Field::ListWrapper <Form::Field::KeyValue
@layout='horizontal' @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}} @isOptional={{true}}
@disabled={{form.disabled}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'form.account_claim_maps.label'}} <F.Label>{{t 'form.from_claim.label'}}</F.Label>
</F.Legend> <F.TextInput
<F.HelperText> data-test-key-input
{{t 'form.account_claim_maps.help'}} @value={{R.rowData.key}}
</F.HelperText> {{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
{{#if @model.errors.account_claim_maps}} <R.Field as |F|>
<F.Error as |E|> <F.Label>{{t 'form.to_claim.label'}}</F.Label>
{{#each @model.errors.account_claim_maps as |error|}} <F.Select
<E.Message>{{error.message}}</E.Message> 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}} {{/each}}
</F.Error> </F.Select>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}} {{/if}}
</:fieldset> </:row>
<:field as |F|>
<F.KeyValue </Form::Field::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>
{{! Certificates }} {{! Certificates }}
<Form::Field::ListWrapper <Form::Field::KeyValue
@layout='horizontal' @name='idp_ca_certs'
@isOptional={{true}} @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}} @disabled={{form.disabled}}
@isOptional={{true}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'form.idp_ca_certs.label'}} <F.Textarea
</F.Legend> aria-label={{t 'form.idp_ca_certs.label'}}
<F.HelperText> data-test-value-input
{{t 'form.idp_ca_certs.help'}} @value={{R.rowData.value}}
</F.HelperText> {{on 'input' (fn R.updateRow 'value')}}
/>
{{#if @model.errors.idp_ca_certs}} </R.Field>
<F.Error as |E|> {{#if R.canDelete}}
{{#each @model.errors.idp_ca_certs as |error|}} <R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}} {{/if}}
</:fieldset> </:row>
</Form::Field::KeyValue>
<:field as |F|>
<F.Textarea
@name='idp_ca_certs'
@options={{@model.idp_ca_certs}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
name='max_age' name='max_age'
@@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking';
export default class FormAuthMethodOidcComponent extends Component { export default class FormAuthMethodOidcComponent extends Component {
// =attributes // =attributes
/** /**
* @type {object} * @type {object}
*/ */
@@ -38,6 +39,35 @@ export default class FormAuthMethodOidcComponent extends Component {
@tracked selectedPrompts = this.parsePromptsArray(); @tracked selectedPrompts = this.parsePromptsArray();
@tracked skipPromptsList = this.isToggleChecked(); @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} * @returns {string}
*/ */
@@ -53,7 +83,6 @@ export default class FormAuthMethodOidcComponent extends Component {
} }
//actions //actions
/** /**
* @param {string} value * @param {string} value
*/ */
@@ -225,83 +225,76 @@
{{/if}} {{/if}}
</Hds::Form::TextInput::Field> </Hds::Form::TextInput::Field>
<Form::Field::ListWrapper <Form::Field::KeyValue
@layout='horizontal' @name='critical_options'
@isOptional={{true}} @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}} @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}} @isOptional={{true}}
@disabled={{form.disabled}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'resources.credential-library.form.extensions.label'}} <F.Label>{{t 'form.key.label'}}</F.Label>
</F.Legend> <F.TextInput
<F.HelperText> data-test-key-input
{{t 'resources.credential-library.form.extensions.help'}} @value={{R.rowData.key}}
</F.HelperText> {{on 'input' (fn R.updateRow 'key')}}
/>
</R.Field>
{{#if @model.errors.extensions}} <R.Field as |F|>
<F.Error as |E|> <F.Label>{{t 'form.value.label'}}</F.Label>
{{#each @model.errors.extensions as |error|}} <F.TextInput
<E.Message>{{error.message}}</E.Message> data-test-value-input
{{/each}} @value={{R.rowData.value}}
</F.Error> {{on 'input' (fn R.updateRow 'value')}}
/>
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}} {{/if}}
</:fieldset> </:row>
<: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>
</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)}} {{#if (can 'save model' @model)}}
<form.actions <form.actions
@@ -4,7 +4,6 @@
*/ */
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { options } from 'api/models/credential-library'; import { options } from 'api/models/credential-library';
export default class FormCredentialLibraryVaultSshCertComponent extends Component { export default class FormCredentialLibraryVaultSshCertComponent extends Component {
@@ -20,17 +20,15 @@
/> />
</Form::Field::JsonSecret::Skeleton> </Form::Field::JsonSecret::Skeleton>
{{else}} {{else}}
<Rose::CodeEditor as |c|> <Hds::CodeEditor
<c.fieldEditor class='secret-editor-json'
class='secret-editor-json' @ariaLabel={{t 'form.secret-editor.label'}}
@onInput={{@onInput}} @value={{@value}}
@value={{@value}} @isLintingEnabled={{true}}
@options={{hash @language='json'
mode='application/json' @onInput={{@onInput}}
gutters=(array 'CodeMirror-lint-markers') @cspNonce={{(csp-nonce)}}
lint='true' data-test-code-editor
}} />
/>
</Rose::CodeEditor>
{{/if}} {{/if}}
</div> </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 <Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}} @onSubmit={{@submit}}
@cancel={{@cancel}} @cancel={{@cancel}}
@disabled={{@model.isSaving}} @disabled={{@model.isSaving}}
@@ -192,7 +193,7 @@
{{! role_arn }} {{! role_arn }}
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
name='role_arn' name='role_arn'
@isOptional={{true}} @isRequired={{true}}
@value={{@model.role_arn}} @value={{@model.role_arn}}
@isInvalid={{@model.errors.role_arn}} @isInvalid={{@model.errors.role_arn}}
disabled={{form.disabled}} disabled={{form.disabled}}
@@ -271,50 +272,44 @@
</Hds::Form::TextInput::Field> </Hds::Form::TextInput::Field>
{{! role_tags }} {{! role_tags }}
<Form::Field::ListWrapper <Form::Field::KeyValue
@layout='horizontal' @name='role_tags'
@isOptional={{true}} @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}} @disabled={{form.disabled}}
@isOptional={{true}}
> >
<:fieldset as |F|> <:row as |R|>
<F.Legend> <R.Field as |F|>
{{t 'resources.storage-bucket.form.role_tags.label'}} <F.Label>{{t 'form.key.label'}}</F.Label>
</F.Legend> <F.TextInput
<F.HelperText> data-test-key-input
{{t 'resources.storage-bucket.form.role_tags.help'}} @value={{R.rowData.key}}
</F.HelperText> {{on 'input' (fn R.updateRow 'key')}}
/>
{{#if @model.errors.role_tags}} </R.Field>
<F.Error as |E|> <R.Field as |F|>
{{#each @model.errors.role_tags as |error|}} <F.Label>{{t 'form.value.label'}}</F.Label>
<E.Message> <F.TextInput
{{error.message}} data-test-value-input
</E.Message> @value={{R.rowData.value}}
{{/each}} {{on 'input' (fn R.updateRow 'value')}}
</F.Error> />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}} {{/if}}
</:fieldset> </:row>
<:field as |F|> </Form::Field::KeyValue>
<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>
{{else}} {{else}}
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
name='access_key_id' name='access_key_id'
@value={{@model.access_key_id}} @value={{@model.access_key_id}}
@isOptional={{true}} @isRequired={{true}}
disabled={{form.disabled}} disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'access_key_id')}} {{on 'input' (set-from-event @model 'access_key_id')}}
as |F| as |F|
@@ -333,7 +328,7 @@
<Hds::Form::TextInput::Field <Hds::Form::TextInput::Field
name='secret_access_key' name='secret_access_key'
@value={{@model.secret_access_key}} @value={{@model.secret_access_key}}
@isOptional={{true}} @isRequired={{true}}
disabled={{form.disabled}} disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'secret_access_key')}} {{on 'input' (set-from-event @model 'secret_access_key')}}
as |F| as |F|
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff. Show more