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