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