Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb895024e0 | ||
|
|
2e94ebdb64 | ||
|
|
ae1e6d2748 | ||
|
|
c72d4f35a0 | ||
|
|
9742d824c4 | ||
|
|
639aa47e19 | ||
|
|
5fa0d11aa9 | ||
|
|
fed32aa57b | ||
|
|
1e2134bd7e | ||
|
|
064375d457 | ||
|
|
deff38de56 | ||
|
|
15950d3a09 | ||
|
|
c4539b8e30 | ||
|
|
904864b14e | ||
|
|
390d179917 | ||
|
|
3b6a421af2 | ||
|
|
cc8d4fe952 | ||
|
|
dfa62dd04b | ||
|
|
520020f2c6 | ||
|
|
31f1ed8dd4 | ||
|
|
5abd4f1f4f | ||
|
|
862ce645cf | ||
|
|
9eccbb91db | ||
|
|
343e1d2193 | ||
|
|
9a11925216 | ||
|
|
f7f1041daa | ||
|
|
1cbf3b227e | ||
|
|
faf4ecfac6 | ||
|
|
ca4aad0a2b | ||
|
|
ed569306bb | ||
|
|
65abd7a4e3 | ||
|
|
422cff4547 | ||
|
|
7695135302 | ||
|
|
5be4868e9a | ||
|
|
fce97fd57a | ||
|
|
af8655ec45 | ||
|
|
daed714b98 | ||
|
|
584e62cff3 | ||
|
|
6d2f1dcbbf | ||
|
|
5e7b74a7ed | ||
|
|
6de20b03ef | ||
|
|
2869020e65 | ||
|
|
40c8ceeca6 | ||
|
|
0623d4ad47 | ||
|
|
0b9c235dd2 | ||
|
|
e16b4e68c8 | ||
|
|
078e36f9b8 | ||
|
|
8874d0be86 | ||
|
|
b59afd3455 | ||
|
|
85a2684532 | ||
|
|
f9a7864b07 | ||
|
|
371301b4ba | ||
|
|
9c10d79661 | ||
|
|
018d879a0a | ||
|
|
b64f1876dc | ||
|
|
055607b3dd | ||
|
|
8a7fa26cb5 | ||
|
|
9cac16fe6a | ||
|
|
61297dba02 | ||
|
|
2f8a53e3ea | ||
|
|
96b9a20844 | ||
|
|
443f230251 | ||
|
|
86ee79e6bf | ||
|
|
b24e5a2c22 | ||
|
|
8c6b1aa69a | ||
|
|
36c2aee271 | ||
|
|
b049635d28 | ||
|
|
42dd029611 | ||
|
|
e02c50aef8 | ||
|
|
d9cf112694 | ||
|
|
a4456f50cd | ||
|
|
a358061f22 | ||
|
|
f155e8ad44 | ||
|
|
c930bb235b | ||
|
|
543cf4697f | ||
|
|
66e9e971a1 | ||
|
|
a961b5e4be | ||
|
|
f8796b362e | ||
|
|
415f638028 | ||
|
|
4523615829 | ||
|
|
61214af01a | ||
|
|
2f0bf5dc17 | ||
|
|
6e71712092 | ||
|
|
7e56e1cccf | ||
|
|
1e3f533d83 | ||
|
|
57568f9a8c | ||
|
|
7c1814118d | ||
|
|
ca50a963fd | ||
|
|
6e188c6996 | ||
|
|
0095c5c666 | ||
|
|
bdd7f303c4 | ||
|
|
f0383522d8 | ||
|
|
a32cc13171 | ||
|
|
dbda8c5674 | ||
|
|
186873d9d4 | ||
|
|
37482c0b36 | ||
|
|
ccdc5a03ac | ||
|
|
2ae24c4627 | ||
|
|
b9e9fd21ee | ||
|
|
6eb83b2a25 | ||
|
|
70aceb1699 | ||
|
|
5e208c4115 | ||
|
|
390f051e43 | ||
|
|
ab0f1fd549 | ||
|
|
788df7b16d | ||
|
|
01a2d8e9a6 | ||
|
|
e842eb001d | ||
|
|
f195e5dc9d | ||
|
|
7f7f0f1859 | ||
|
|
b494a703eb | ||
|
|
0f52b86364 | ||
|
|
1b31188d5e | ||
|
|
30e7268297 | ||
|
|
6956b30fd6 | ||
|
|
98d19ef591 | ||
|
|
f0ffcea424 | ||
|
|
6454fb7061 | ||
|
|
c0d7af7899 | ||
|
|
2424395415 | ||
|
|
dde2f259e8 | ||
|
|
e38c28c288 | ||
|
|
0ff91f9182 | ||
|
|
857bf01596 | ||
|
|
965e82473d | ||
|
|
0d55812f64 | ||
|
|
e807e1a3af | ||
|
|
64e568bd72 | ||
|
|
401ff354e8 | ||
|
|
9c31154941 | ||
|
|
1947365c91 | ||
|
|
be528dae47 | ||
|
|
d3c789c66c | ||
|
|
15727f61fd | ||
|
|
486d8b116a | ||
|
|
91ffbfb315 | ||
|
|
9b33812f9e | ||
|
|
b40c53fb45 | ||
|
|
97258f7525 | ||
|
|
ace65eab3a | ||
|
|
20e6df522a | ||
|
|
c8abc3845a | ||
|
|
6cd2f0f7d8 | ||
|
|
1266930647 | ||
|
|
695d9d413d | ||
|
|
edaf19f59b | ||
|
|
de5defeab2 | ||
|
|
63ec418cf0 | ||
|
|
e4f4394197 | ||
|
|
0b9357e9b3 | ||
|
|
777380e19f | ||
|
|
25b3a940a3 | ||
|
|
a97f66e011 | ||
|
|
fac7d2b128 | ||
|
|
ad1bb34453 | ||
|
|
8605a54221 | ||
|
|
98cd9dc1ec | ||
|
|
bd857836ee | ||
|
|
4974273bd0 | ||
|
|
c4e58fb780 | ||
|
|
2515efead6 | ||
|
|
fda3ca9d4b | ||
|
|
9648e1fa3b | ||
|
|
e395b1196c | ||
|
|
b7e6492d26 | ||
|
|
6311d7587e | ||
|
|
07573d6b2c | ||
|
|
90890441e1 | ||
|
|
1430adce7e | ||
|
|
89afee7297 | ||
|
|
78f89da348 | ||
|
|
9bacb55e87 | ||
|
|
dea4a62955 | ||
|
|
ef163709f2 | ||
|
|
0cfa814cdd | ||
|
|
694c43296a | ||
|
|
c98bfb5622 | ||
|
|
7cebece600 | ||
|
|
a9cfc43404 | ||
|
|
c38ba294cc | ||
|
|
ce2649272f | ||
|
|
3d60d047e3 | ||
|
|
a3082e97e6 | ||
|
|
333eebd731 | ||
|
|
320af9f6f7 | ||
|
|
af9b832d38 | ||
|
|
bea791b2e6 | ||
|
|
e31ac72531 | ||
|
|
075d8c6d8f | ||
|
|
56f42c3f5a | ||
|
|
af7196215b | ||
|
|
80d2898e56 | ||
|
|
f607e77274 | ||
|
|
007f5d8b46 | ||
|
|
a9d110a1b6 | ||
|
|
79b06ee565 | ||
|
|
0af81d9bc1 | ||
|
|
9e668e8467 | ||
|
|
69da1fa04c | ||
|
|
392792eb92 | ||
|
|
9c018f131f | ||
|
|
0542218537 | ||
|
|
7ac904a31f | ||
|
|
a7d9a7e348 | ||
|
|
0d0dadf398 | ||
|
|
45c16252a7 | ||
|
|
9b3cbaa6b3 | ||
|
|
89cfb14d6e | ||
|
|
a68d7d103a | ||
|
|
4dc5e13f24 |
No files matched your search
@@ -11,15 +11,15 @@ jobs:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
@@ -28,8 +28,8 @@ jobs:
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
@@ -37,17 +37,17 @@ jobs:
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- name: Build
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:oss
|
||||
- name: Upload artifact admin-ui
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: admin-ui
|
||||
path: ./ui/admin/dist/
|
||||
- name: Upload artifact OSS admin UI
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: admin-ui-oss
|
||||
path: ./ui/admin/dist/
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
@@ -65,12 +65,12 @@ jobs:
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- name: Build
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:enterprise
|
||||
- name: Upload artifact enterprise admin UI
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: admin-ui-ent
|
||||
path: ./ui/admin/dist/
|
||||
@@ -79,8 +79,8 @@ jobs:
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
@@ -88,12 +88,12 @@ jobs:
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- name: Build
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:hcp
|
||||
- name: Upload artifact HCP admin UI
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: admin-ui-hcp
|
||||
path: ./ui/admin/dist/
|
||||
@@ -1,34 +0,0 @@
|
||||
name: "Trigger Build Artifacts for Testing"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
||||
required: true
|
||||
product_version:
|
||||
required: true
|
||||
type: string
|
||||
description: The version number belonging to the oss binary to be used
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
description: The git SHA of the oss artifacts to be used
|
||||
|
||||
jobs:
|
||||
trigger-gha-build-artifacts-for-testing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Release description
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Triggering Build Artifacts for Testing using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
||||
- name: Trigger Build Artifacts for Testing in boundary-ui-releases
|
||||
run: |
|
||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||
gh workflow run build-artifacts-for-testing.yml \
|
||||
--repo hashicorp/boundary-ui-releases \
|
||||
--ref main \
|
||||
-f TAG="${{ inputs.branch }}" \
|
||||
-f PRODUCT_VERSION="${{ inputs.product_version }}" \
|
||||
-f COMMIT="${{ inputs.commit }}" \
|
||||
@@ -30,11 +30,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8f1a6fed33af5212fab8a999d004627ae8901d1b # codeql-bundle-v2.17.5
|
||||
uses: github/codeql-action/init@3096afedf9873361b2b2f65e1445b13272c83eb8 # codeql-bundle-v2.20.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8f1a6fed33af5212fab8a999d004627ae8901d1b # codeql-bundle-v2.17.5
|
||||
uses: github/codeql-action/analyze@3096afedf9873361b2b2f65e1445b13272c83eb8 # codeql-bundle-v2.20.0
|
||||
@@ -1,10 +1,15 @@
|
||||
name: Check Copyright Headers
|
||||
name: Add Copyright Headers
|
||||
on: pull_request
|
||||
jobs:
|
||||
copywrite:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
||||
- name: Check Header Compliance
|
||||
run: copywrite headers --plan
|
||||
- name: Add missing Copyright Headers
|
||||
run: copywrite headers
|
||||
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
||||
with:
|
||||
commit_message: Add missing copyright headers
|
||||
@@ -6,15 +6,15 @@ jobs:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
@@ -24,15 +24,15 @@ jobs:
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run compliance:licenses
|
||||
@@ -43,15 +43,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn lint
|
||||
@@ -62,13 +62,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn test
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||
- run: |
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
name: "Trigger Test Boundary UI release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
trigger-gha-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Release description
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Triggering Test release using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
||||
- name: Trigger Release workflow in boundary-ui-releases
|
||||
run: |
|
||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||
gh workflow run release.yml \
|
||||
--repo hashicorp/boundary-ui-releases \
|
||||
--ref main \
|
||||
-f TAG="${{ github.event.inputs.branch }}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Trigger Boundary UI release"
|
||||
name: "Trigger Boundary Desktop release"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -9,14 +9,14 @@ jobs:
|
||||
trigger-gha-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Release description
|
||||
run: |
|
||||
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
|
||||
- name: Trigger Release workflow in boundary-ui-releases
|
||||
- name: Trigger Release workflow in boundary-desktop-releases
|
||||
run: |
|
||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||
gh workflow run release.yml \
|
||||
--repo hashicorp/boundary-ui-releases \
|
||||
--repo hashicorp/boundary-desktop-releases \
|
||||
--ref main \
|
||||
-f TAG="${GITHUB_REF#refs/tags/}"
|
||||
@@ -1,5 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn compliance:licenses
|
||||
npx lint-staged
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
COMMIT_MSG_FILE=$1
|
||||
COMMIT_SOURCE=$2
|
||||
SHA1=$3
|
||||
|
||||
@@ -98,7 +98,7 @@ List of available project commands. `yarn run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|---|
|
||||
| build | Builds Admin UI. |
|
||||
| build | Builds Admin UI for production. |
|
||||
| test | Runs tests in CI mode. |
|
||||
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
|
||||
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
|
||||
@@ -119,9 +119,9 @@ of README.
|
||||
|
||||
### Building Admin UI for Production
|
||||
|
||||
See ui/admin/README.md Building for Production for more information on how to
|
||||
build the Admin UI. Be sure to set build-related environment variables as
|
||||
necessary for your target environment, as described in the Admin UI readme.
|
||||
See [Admin UI README](ui/admin/README.md#building-for-production) for more information on how to
|
||||
build the Admin UI for production. Be sure to set build-related environment variables as
|
||||
necessary for your target environment, as described in the [Environment Variables](ui/admin/README.md#environment-variables) section of README.
|
||||
|
||||
To quickly produce a production build of Admin UI, run from this folder:
|
||||
|
||||
@@ -144,9 +144,9 @@ Assets are saved to `ui/admin/dist/`.
|
||||
|
||||
### Building Desktop UI for Production
|
||||
|
||||
See ui/desktop/README.md Building for Production for more information on how to
|
||||
See [Desktop UI README](ui/desktop/README.md#building-for-production) for more information on how to
|
||||
build the Desktop UI. Be sure to set build-related environment variables as
|
||||
necessary for your target environment, as described in the Desktop UI readme.
|
||||
necessary for your target environment, as described in the [Environment Variables](ui/desktop/README.md#environment-variables-prod) section of README.
|
||||
|
||||
To quickly produce a production build of Desktop UI, run from this folder:
|
||||
|
||||
@@ -174,8 +174,9 @@ EXE archived desktop UI is available at asset location under `zip` folder.
|
||||
|
||||
This describes how to connect local Boundary UI to your local instance of Boundary.
|
||||
|
||||
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://learn.hashicorp.com/tutorials/boundary/getting-started-dev?in=boundary/getting-started#start-a-dev-environment).
|
||||
#### For admin
|
||||
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://developer.hashicorp.com/boundary/tutorials/get-started-community/community-get-started-dev)
|
||||
|
||||
#### For Admin
|
||||
|
||||
This assumes you are within `boundary-ui/ui/admin`.
|
||||
|
||||
@@ -185,7 +186,7 @@ Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
||||
|
||||
Once you open the UI you will see the login screen, authenticate using generated admin login name and password.
|
||||
|
||||
#### For desktop
|
||||
#### For Desktop
|
||||
|
||||
This assumes you are within `boundary-ui/ui/desktop`.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import sinon from 'sinon';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { resourceNames } from 'api/handlers/cache-daemon-handler';
|
||||
import { singularize } from 'ember-inflector';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
/**
|
||||
* This test helper can be used to help setup your sinon stubs in your tests.
|
||||
@@ -69,16 +70,18 @@ export default function setupStubs(hooks) {
|
||||
.withArgs('searchCacheDaemon')
|
||||
.onCall(i)
|
||||
.returns({
|
||||
[resourceName]: models.map((model) => {
|
||||
// Use internal serializer to serialize the model correctly
|
||||
// according to our mirage serializers
|
||||
const modelData =
|
||||
this.server.serializerOrRegistry.serialize(model);
|
||||
[underscore(resourceNames[singularize(resourceName)])]: models.map(
|
||||
(model) => {
|
||||
// Use internal serializer to serialize the model correctly
|
||||
// according to our mirage serializers
|
||||
const modelData =
|
||||
this.server.serializerOrRegistry.serialize(model);
|
||||
|
||||
// Serialize the data properly to standard JSON as that is what
|
||||
// we're expecting from the cache daemon response
|
||||
return JSON.parse(JSON.stringify(modelData));
|
||||
}),
|
||||
// Serialize the data properly to standard JSON as that is what
|
||||
// we're expecting from the cache daemon response
|
||||
return JSON.parse(JSON.stringify(modelData));
|
||||
},
|
||||
),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -55,63 +55,224 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
})
|
||||
plugin;
|
||||
|
||||
// AWS & Azure
|
||||
// AWS, Azure & GCP
|
||||
|
||||
@attr('boolean', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
disable_credential_rotation;
|
||||
|
||||
// AWS and GCP specific
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'gcp'],
|
||||
},
|
||||
description:
|
||||
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
||||
})
|
||||
worker_filter;
|
||||
|
||||
// AWS specific
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
region;
|
||||
|
||||
// AWS static credentials
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'static-credential',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
access_key_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'static-credential',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
secret_access_key;
|
||||
|
||||
// AWS dynamic credentials
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role ARN to use.',
|
||||
})
|
||||
role_arn;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role external ID to use.',
|
||||
})
|
||||
role_external_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role session to use.',
|
||||
})
|
||||
role_session_name;
|
||||
|
||||
@attr('object-as-array', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
credentialType: 'dynamic-credential',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The role tags to use.',
|
||||
})
|
||||
role_tags;
|
||||
|
||||
// Azure specific
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
tenant_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
client_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
subscription_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
secret_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
})
|
||||
secret_value;
|
||||
|
||||
// GCP specific
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The project ID associated with the service account.',
|
||||
})
|
||||
project_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description: 'The deployment area within a region.',
|
||||
})
|
||||
zone;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description:
|
||||
'The email address associated with the service account. The email address used to uniquely identify the service account. It is required for authentication and authorization.',
|
||||
})
|
||||
client_email;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
description:
|
||||
'The unique identifier for the service account that will be impersonated.',
|
||||
})
|
||||
target_service_account_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
description:
|
||||
'The ID of the private key used to sign the JWT and authentication.',
|
||||
isNestedSecret: true,
|
||||
})
|
||||
private_key_id;
|
||||
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'gcp',
|
||||
},
|
||||
description:
|
||||
'The private key used to sign the JWT and obtain a OAuth 2.0 access token.',
|
||||
isNestedSecret: true,
|
||||
})
|
||||
private_key;
|
||||
}
|
||||
@@ -63,6 +63,10 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
plugin;
|
||||
|
||||
@attr('number', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
description:
|
||||
"The number of seconds between the time boundary syncs the hosts in this set using this host set's plugin. If not provided a system determined default is used",
|
||||
})
|
||||
@@ -79,12 +83,20 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
host_ids;
|
||||
|
||||
@attr('string-array', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
preferred_endpoints;
|
||||
|
||||
// AWS specific
|
||||
// AWS & GCP specific
|
||||
@attr('string-array', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: ['aws', 'gcp'],
|
||||
},
|
||||
isNestedAttribute: true,
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
@@ -92,6 +104,10 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
|
||||
// Azure specific, comes in from API as "filter". This is to avoid collisions with filter
|
||||
@attr('string', {
|
||||
for: {
|
||||
type: 'plugin',
|
||||
name: 'azure',
|
||||
},
|
||||
description: '',
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { generateMQLExpression } from '../utils/mql-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
/**
|
||||
* Not all types are yet supported by the cache daemon so we'll
|
||||
@@ -94,12 +95,14 @@ export default class CacheDaemonHandler {
|
||||
const sessionData = this.session.data?.authenticated;
|
||||
const auth_token_id = sessionData?.id;
|
||||
const token = sessionData?.token;
|
||||
const resourceName = resourceNames[type];
|
||||
|
||||
remainingQuery = {
|
||||
...remainingQuery,
|
||||
query: searchQuery,
|
||||
auth_token_id,
|
||||
token,
|
||||
resource: resourceNames[type],
|
||||
resource: resourceName,
|
||||
};
|
||||
|
||||
let cacheDaemonResults = {};
|
||||
@@ -130,7 +133,7 @@ export default class CacheDaemonHandler {
|
||||
}
|
||||
} else {
|
||||
__electronLog?.error(
|
||||
'Failed to search cache daemon; falling back to search controller',
|
||||
`Failed to search cache daemon for ${resourceName}. Error:`,
|
||||
e.message,
|
||||
);
|
||||
|
||||
@@ -138,10 +141,7 @@ export default class CacheDaemonHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Currently returns with a singular top level field with resource name
|
||||
// e.g. { targets: [...] } or { sessions: [..] }
|
||||
// So this just unwraps to the array, or undefined
|
||||
const [results] = Object.values(cacheDaemonResults);
|
||||
const results = cacheDaemonResults[underscore(resourceName)];
|
||||
const payload = { items: paginateResults(results, page, pageSize) };
|
||||
|
||||
const schema = store.modelFor(type);
|
||||
@@ -167,7 +167,16 @@ export default class CacheDaemonHandler {
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
// or EmberArray since the ember store query method asserts it has to be an array
|
||||
// so we can't just return an object.
|
||||
records.meta = { totalItems: results?.length ?? 0 };
|
||||
// Also include whether the results are incomplete or is still refreshing
|
||||
// as part of the response
|
||||
records.meta = {
|
||||
totalItems: results?.length ?? 0,
|
||||
isLoadIncomplete: cacheDaemonResults.incomplete ?? false,
|
||||
// Sometimes the refresh status returns an error status if it takes too long but it's still refreshing
|
||||
isCacheRefreshing:
|
||||
cacheDaemonResults.refresh_status !== 'not-refreshing',
|
||||
};
|
||||
|
||||
return records;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { registerDeprecationHandler } from '@ember/debug';
|
||||
|
||||
const deprecationList = ['ember-data:deprecate-early-static'];
|
||||
|
||||
export function initialize() {
|
||||
registerDeprecationHandler((message, options, next) => {
|
||||
// TODO: Remove deprecation handling for `deprecate-early-static` warning
|
||||
// once we upgrade to ember-cli-mirage v3.0.0 which fixes this issue.
|
||||
// More info about this issue here: https://github.com/miragejs/ember-cli-mirage/issues/2441
|
||||
if (options?.id && deprecationList.includes(options.id)) {
|
||||
return;
|
||||
}
|
||||
if (options?.url) {
|
||||
message += options.url;
|
||||
}
|
||||
next(message, options);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -8,7 +8,16 @@ import GeneratedCredentialModel from '../generated/models/credential';
|
||||
/**
|
||||
* Supported Credential types.
|
||||
*/
|
||||
export const types = ['username_password', 'ssh_private_key', 'json'];
|
||||
|
||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
|
||||
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
|
||||
export const TYPE_CREDENTIAL_JSON = 'json';
|
||||
|
||||
export const TYPES_CREDENTIAL = Object.freeze([
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_JSON,
|
||||
]);
|
||||
|
||||
export default class CredentialModel extends GeneratedCredentialModel {
|
||||
// =attributes
|
||||
@@ -28,7 +37,7 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isJSON() {
|
||||
return this.type === 'json';
|
||||
return this.type === TYPE_CREDENTIAL_JSON;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,6 +45,6 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUnknown() {
|
||||
return !types.includes(this.type);
|
||||
return !TYPES_CREDENTIAL.includes(this.type);
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,33 @@ export const TYPES_HOST_CATALOG = Object.freeze([
|
||||
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws';
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure';
|
||||
export const TYPE_HOST_CATALOG_PLUGIN_GCP = 'gcp';
|
||||
export const TYPES_HOST_CATALOG_PLUGIN = [
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
];
|
||||
|
||||
export const TYPE_CREDENTIAL_STATIC = 'static-credential';
|
||||
export const TYPE_CREDENTIAL_DYNAMIC = 'dynamic-credential';
|
||||
|
||||
export const TYPES_CREDENTIALS = Object.freeze([
|
||||
TYPE_CREDENTIAL_STATIC,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
]);
|
||||
|
||||
export const DYNAMIC_CREDENTIAL_FIELDS = [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
];
|
||||
export const STATIC_CREDENTIAL_FIELDS = ['access_key_id, secret_access_key'];
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
|
||||
#credentialType;
|
||||
/**
|
||||
* True if the host catalog is static.
|
||||
* @type {boolean}
|
||||
@@ -38,6 +57,29 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
return this.type === 'plugin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from credentialType
|
||||
* @type {string}
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (DYNAMIC_CREDENTIAL_FIELDS.some((field) => this[field])) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
}
|
||||
}
|
||||
return this.#credentialType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value to credentialType
|
||||
* @type {string}
|
||||
*/
|
||||
set credentialType(type) {
|
||||
this.#credentialType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the host catalog is an unknown type.
|
||||
* @type {boolean}
|
||||
@@ -53,7 +95,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAWS() {
|
||||
return this.compositeType === 'aws';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +103,15 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAzure() {
|
||||
return this.compositeType === 'azure';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if host catalog plugin type is GCP.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isGCP() {
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
*/
|
||||
|
||||
import GeneratedHostSetModel from '../generated/models/host-set';
|
||||
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
|
||||
import {
|
||||
TYPES_HOST_CATALOG_PLUGIN,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
} from './host-catalog';
|
||||
|
||||
export default class HostSetModel extends GeneratedHostSetModel {
|
||||
// =attributes
|
||||
@@ -40,14 +45,22 @@ export default class HostSetModel extends GeneratedHostSetModel {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isAWS() {
|
||||
return this.compositeType === 'aws';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if a host-set plugin is Azure or not.
|
||||
*/
|
||||
get isAzure() {
|
||||
return this.compositeType === 'azure';
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if host catalog plugin type is GCP.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isGCP() {
|
||||
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,9 @@ import { equal } from '@ember/object/computed';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { flattenObject } from '../utils/flatten-nested-object';
|
||||
import { TYPES_CREDENTIAL_LIBRARY } from 'api/models/credential-library';
|
||||
import { TYPE_CREDENTIAL_JSON } from 'api/models/credential';
|
||||
|
||||
export const STATUS_SESSION_ACTIVE = 'active';
|
||||
export const STATUS_SESSION_PENDING = 'pending';
|
||||
@@ -23,7 +26,7 @@ export const statusTypes = [
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class SessionCredential {
|
||||
export class SessionCredential {
|
||||
// =classes
|
||||
|
||||
/**
|
||||
@@ -34,12 +37,14 @@ class SessionCredential {
|
||||
name;
|
||||
description;
|
||||
type;
|
||||
credentialtype;
|
||||
|
||||
constructor(id, name, description, type) {
|
||||
constructor(id, name, description, type, credentialtype) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.credentialType = credentialtype;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,8 +80,10 @@ class SessionCredential {
|
||||
get secrets() {
|
||||
if (this.#payloadSecret?.decoded) {
|
||||
const secretJSON = this.#payloadSecret.decoded;
|
||||
return Object.keys(secretJSON).map(
|
||||
(key) => new SessionCredential.SecretItem(key, secretJSON[key]),
|
||||
return this.extractSecrets(
|
||||
secretJSON,
|
||||
this.source.type,
|
||||
this.source.credentialType,
|
||||
);
|
||||
} else {
|
||||
// decode from base64
|
||||
@@ -85,11 +92,41 @@ class SessionCredential {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts secrets from the payload secret JSON object.
|
||||
* We only need to flatten the object if the type is vault or if its a json type credential.
|
||||
* @param {object} secretJSON - The payload secret JSON object.
|
||||
* @param {string} type - The credential source type.
|
||||
* @param {string} credentialType - The credential type.
|
||||
* @returns {SessionCredential.SecretItem[]} - The array of secret items.
|
||||
*/
|
||||
extractSecrets(secretJSON, type, credentialType) {
|
||||
let source;
|
||||
if (credentialType === TYPE_CREDENTIAL_JSON) {
|
||||
source = flattenObject(secretJSON);
|
||||
} else if (TYPES_CREDENTIAL_LIBRARY.includes(type) && secretJSON?.data) {
|
||||
source = flattenObject(secretJSON.data);
|
||||
} else {
|
||||
source = secretJSON;
|
||||
}
|
||||
|
||||
return Object.entries(source).map(
|
||||
([key, value]) => new SessionCredential.SecretItem(key, value),
|
||||
);
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
constructor(cred) {
|
||||
const { id, name, description, type } = cred.credential_source;
|
||||
this.source = new SessionCredential.Source(id, name, description, type);
|
||||
const { id, name, description, type, credential_type } =
|
||||
cred.credential_source;
|
||||
this.source = new SessionCredential.Source(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
credential_type,
|
||||
);
|
||||
this.#payloadSecret = cred.secret;
|
||||
this.rawCredential = cred;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import RESTSerializer from '@ember-data/serializer/rest';
|
||||
import { underscore } from '@ember/string';
|
||||
import { get } from '@ember/object';
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
/**
|
||||
* Manages serialization/normalization of data to/from the API.
|
||||
@@ -42,7 +43,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
*/
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const { type, options } = attribute;
|
||||
const { type: recordType } = snapshot.record;
|
||||
const { type: recordType, compositeType } = snapshot.record;
|
||||
let value = super.serializeAttribute(...arguments);
|
||||
// Remove secret attributes that are null or empty
|
||||
if (options.isSecret) {
|
||||
@@ -74,18 +75,38 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
delete json[key];
|
||||
}
|
||||
// If an attribute has a `for` option, it must match the
|
||||
// record's `type`, else the attribute is excluded
|
||||
// record's `type`, otherwise the attribute is excluded
|
||||
// from serialization.
|
||||
//note: This doesn't handle secrets yet, we can add support for them if needed.
|
||||
if (
|
||||
options?.for &&
|
||||
options.for !== recordType &&
|
||||
!options.for.includes(recordType)
|
||||
) {
|
||||
if (options.isNestedAttribute) {
|
||||
delete json?.attributes?.[key];
|
||||
} else {
|
||||
delete json[key];
|
||||
|
||||
if (options?.for) {
|
||||
// Check the typeof the `for` option to determine if it is a plugin or not
|
||||
const isPlugin = options.for?.type === 'plugin';
|
||||
// Helper function to handle deleting attribute and secret keys
|
||||
const deleteKey = (json) => {
|
||||
if (options.isNestedAttribute) {
|
||||
delete json?.attributes?.[key];
|
||||
} else if (options.isNestedSecret && json?.secrets?.[key]) {
|
||||
delete json?.secrets?.[key];
|
||||
if (Object.keys(json.secrets).length === 0) {
|
||||
delete json.secrets;
|
||||
}
|
||||
} else {
|
||||
delete json[key];
|
||||
}
|
||||
};
|
||||
|
||||
// For plugins, the `for` option is an object with a `name` key
|
||||
// For non-plugins, the `for` option can be string or an array
|
||||
if (
|
||||
(isPlugin && !options.for?.name.includes(compositeType)) ||
|
||||
(!isPlugin &&
|
||||
!options.for?.includes(recordType) &&
|
||||
typeOf(options.for) === 'array') ||
|
||||
(!isPlugin &&
|
||||
options.for !== recordType &&
|
||||
typeOf(options.for) === 'string')
|
||||
) {
|
||||
deleteKey(json);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -4,61 +4,45 @@
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
const fieldsByType = {
|
||||
aws: [
|
||||
'disable_credential_rotation',
|
||||
'region',
|
||||
'access_key_id',
|
||||
'secret_access_key',
|
||||
],
|
||||
azure: [
|
||||
'disable_credential_rotation',
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'subscription_id',
|
||||
'secret_id',
|
||||
'secret_value',
|
||||
],
|
||||
};
|
||||
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/host-catalog';
|
||||
|
||||
export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
// =methods
|
||||
serialize(snapshot) {
|
||||
const { credentialType } = snapshot.record;
|
||||
const serialized = super.serialize(...arguments);
|
||||
switch (snapshot.record.compositeType) {
|
||||
case 'static':
|
||||
return this.serializeStatic(...arguments);
|
||||
default:
|
||||
return serialized;
|
||||
// By default, disable credential rotation for dynamic credentials
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
serialized.attributes.disable_credential_rotation = true;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { isPlugin, compositeType } = snapshot.record;
|
||||
const { compositeType, credentialType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
// Nested conditionals for readability
|
||||
// This deletes any fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedAttribute && json.attributes) {
|
||||
// The key must be included in the fieldsByType list above
|
||||
if (!fieldsByType[compositeType].includes(key))
|
||||
delete json.attributes[key];
|
||||
}
|
||||
// This deletes any fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedSecret && json.secrets) {
|
||||
if (!fieldsByType[compositeType].includes(key)) delete json.secrets[key];
|
||||
}
|
||||
const isSamePluginName = options?.for?.name?.includes(compositeType);
|
||||
|
||||
// new one for isNestedSecret
|
||||
// Remove attributes that are not applicable to the current credential type for the same plugin
|
||||
// For example, 'aws' dynamic plugin has two credential types: 'static' and 'dynamic'
|
||||
// API expects the fields to be 'null' for the fields that are not from the current credential type (that's why we are not using the application serializer for this, where we delete attrs)
|
||||
// Empty secrets should be removed from the JSON
|
||||
if (
|
||||
isSamePluginName &&
|
||||
options.for.credentialType &&
|
||||
options.for.credentialType !== credentialType
|
||||
) {
|
||||
if (options.isNestedAttribute) {
|
||||
json.attributes[key] = null;
|
||||
} else if (options.isNestedSecret && json.secrets[key]) {
|
||||
delete json.secrets[key];
|
||||
if (Object.keys(json.secrets).length === 0) {
|
||||
delete json.secrets;
|
||||
}
|
||||
} else {
|
||||
delete json[key];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
serializeStatic() {
|
||||
const serialized = super.serialize(...arguments);
|
||||
// Delete unnecessary fields for static host-catalog
|
||||
delete serialized.attributes;
|
||||
delete serialized.secrets;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,6 @@
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
const fieldByType = {
|
||||
aws: ['preferred_endpoints', 'filters', 'sync_interval_seconds'],
|
||||
azure: ['preferred_endpoints', 'filter_string', 'sync_interval_seconds'],
|
||||
};
|
||||
export default class HostSetSerializer extends ApplicationSerializer {
|
||||
// =properties
|
||||
|
||||
@@ -63,16 +59,8 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
serializeAttribute(snapshot, json, key) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { isPlugin, compositeType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
|
||||
if (isPlugin && options.isNestedAttribute) {
|
||||
if (!fieldByType[compositeType].includes(key)) {
|
||||
delete json.attributes[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'filter_string') {
|
||||
const { filter_string } = json.attributes;
|
||||
|
||||
@@ -29,6 +29,10 @@ export const modelIndexes = {
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
|
||||
'auth-method':
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
|
||||
'session-recording':
|
||||
'&id, attributes.created_time, attributes.type, attributes.state, attributes.start_time, attributes.end_time, attributes.duration, attributes.scope.scope_id, attributes.create_time_values.user.id, attributes.create_time_values.user.name, attributes.create_time_values.target.id, attributes.create_time_values.target.name, attributes.create_time_values.target.scope.id, attributes.create_time_values.target.scope.name',
|
||||
alias:
|
||||
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
|
||||
};
|
||||
|
||||
export const formatDbName = (userId, clusterUrl) =>
|
||||
@@ -109,7 +113,7 @@ export default class IndexedDbService extends Service {
|
||||
}
|
||||
|
||||
this.#db = new Dexie(dbName);
|
||||
this.#db.version(1).stores(modelIndexes);
|
||||
this.#db.version(2).stores(modelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
import { typeOf } from '@ember/utils';
|
||||
|
||||
/**
|
||||
* Flattens a nested object into a single-level object.
|
||||
* @param obj
|
||||
* @param result
|
||||
* @param parentKey
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
export const flattenObject = (obj, result = {}, parentKey = '') => {
|
||||
for (const key in obj) {
|
||||
// Check if the value is an object and it is not null
|
||||
if (typeOf(obj[key]) === 'object' && obj[key] !== null) {
|
||||
// Recursively flatten the object
|
||||
flattenObject(obj[key], result, `${parentKey}${key}.`);
|
||||
} else if (obj[key] !== null && obj[key] !== '') {
|
||||
// Only add to result if the value is not null or an empty string
|
||||
result[`${parentKey}${key}`] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default, initialize } from 'api/initializers/deprecations';
|
||||
@@ -25,7 +25,10 @@ const isTesting = environmentConfig.environment === 'test';
|
||||
export default function (mirageConfig) {
|
||||
let finalConfig = {
|
||||
...mirageConfig,
|
||||
models: { ...discoverEmberDataModels(), ...mirageConfig.models },
|
||||
models: {
|
||||
...discoverEmberDataModels(mirageConfig.store),
|
||||
...mirageConfig.models,
|
||||
},
|
||||
serializers: applyEmberDataSerializers(mirageConfig.serializers),
|
||||
routes,
|
||||
};
|
||||
@@ -775,18 +778,7 @@ function routes() {
|
||||
});
|
||||
|
||||
// session recordings
|
||||
this.get(
|
||||
'/session-recordings',
|
||||
(
|
||||
{ sessionRecordings },
|
||||
{ queryParams: { scope_id: scopeId, recursive } },
|
||||
) => {
|
||||
if (recursive && scopeId === 'global') {
|
||||
return sessionRecordings.all();
|
||||
}
|
||||
return sessionRecordings.where({ scopeId });
|
||||
},
|
||||
);
|
||||
this.get('/session-recordings');
|
||||
this.get(
|
||||
'/session-recordings/:idMethod',
|
||||
async ({ sessionRecordings }, { params: { idMethod } }) => {
|
||||
|
||||
@@ -8,11 +8,23 @@ import { trait } from 'miragejs';
|
||||
import permissions from '../helpers/permissions';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import generateId from '../helpers/id';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_STATIC,
|
||||
TYPE_HOST_CATALOG_DYNAMIC,
|
||||
TYPES_HOST_CATALOG,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
const types = ['static', 'plugin'];
|
||||
// Represents known plugin types, except "foobar" which models the possibility
|
||||
// of receiving an _unknown_ type, which the UI must handle gracefully.
|
||||
const pluginTypes = ['aws', 'azure', 'foobar'];
|
||||
const pluginTypes = [
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
'foobar',
|
||||
];
|
||||
|
||||
let pluginTypeCounter = 1;
|
||||
|
||||
@@ -20,7 +32,7 @@ export default factory.extend({
|
||||
id: () => generateId('hc_'),
|
||||
|
||||
// Cycle through available types
|
||||
type: (i) => types[i % types.length],
|
||||
type: (i) => TYPES_HOST_CATALOG[i % TYPES_HOST_CATALOG.length],
|
||||
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('host-catalog') || [
|
||||
@@ -30,7 +42,7 @@ export default factory.extend({
|
||||
'delete',
|
||||
],
|
||||
authorized_collection_actions: function () {
|
||||
const isStatic = this.type === 'static';
|
||||
const isStatic = this.type === TYPE_HOST_CATALOG_STATIC;
|
||||
return {
|
||||
// only static catalogs allow host create at this time
|
||||
hosts: isStatic ? ['create', 'list'] : ['list'],
|
||||
@@ -38,8 +50,18 @@ export default factory.extend({
|
||||
};
|
||||
},
|
||||
|
||||
worker_filter: function () {
|
||||
if (
|
||||
this.type === TYPE_HOST_CATALOG_DYNAMIC &&
|
||||
(this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_AWS ||
|
||||
this.plugin?.name === TYPE_HOST_CATALOG_PLUGIN_GCP)
|
||||
) {
|
||||
return `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
|
||||
}
|
||||
},
|
||||
|
||||
plugin: function (i) {
|
||||
if (this.type === 'plugin') {
|
||||
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
|
||||
return {
|
||||
id: `plugin-id-${i}`,
|
||||
name: pluginTypes[pluginTypeCounter++ % pluginTypes.length],
|
||||
@@ -50,35 +72,50 @@ export default factory.extend({
|
||||
|
||||
attributes() {
|
||||
switch (this.plugin?.name) {
|
||||
case 'aws':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AWS:
|
||||
return {
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
region: `us-${faker.location.cardinalDirection()}-${faker.number.int(
|
||||
9,
|
||||
)}`,
|
||||
};
|
||||
case 'azure':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
|
||||
return {
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
tenant_id: faker.string.uuid(),
|
||||
client_id: faker.string.uuid(),
|
||||
subscription_id: faker.string.uuid(),
|
||||
};
|
||||
case TYPE_HOST_CATALOG_PLUGIN_GCP:
|
||||
return {
|
||||
disable_credential_rotation: faker.datatype.boolean(),
|
||||
project_id: faker.string.uuid(),
|
||||
client_email: faker.internet.email(),
|
||||
target_service_account_id: faker.string.uuid(),
|
||||
zone: `us-${faker.location.cardinalDirection()}${faker.number.int(
|
||||
9,
|
||||
)}-${faker.string.fromCharacters('abc')}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
secrets() {
|
||||
switch (this.plugin?.name) {
|
||||
case 'aws':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AWS:
|
||||
return {
|
||||
access_key_id: faker.string.nanoid(),
|
||||
secret_access_key: faker.string.nanoid(),
|
||||
};
|
||||
case 'azure':
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
|
||||
return {
|
||||
secret_id: faker.string.nanoid(),
|
||||
secret_value: faker.string.nanoid(),
|
||||
};
|
||||
case TYPE_HOST_CATALOG_PLUGIN_GCP:
|
||||
return {
|
||||
private_key: faker.string.nanoid(),
|
||||
private_key_id: faker.string.nanoid(),
|
||||
};
|
||||
}
|
||||
},
|
||||
withChildren: trait({
|
||||
|
||||
@@ -7,6 +7,22 @@ import factory from '../generated/factories/host-set';
|
||||
import permissions from '../helpers/permissions';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import generateId from '../helpers/id';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
function generateFilters(prefix) {
|
||||
const filtersAmount = faker.number.int({ min: 1, max: 5 });
|
||||
let filters = [];
|
||||
for (let i = 0; i < filtersAmount; ++i) {
|
||||
filters.push(`${prefix}${faker.word.words(1)}=${faker.word.words(1)}`);
|
||||
}
|
||||
return {
|
||||
filters,
|
||||
};
|
||||
}
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('hs_'),
|
||||
@@ -42,22 +58,15 @@ export default factory.extend({
|
||||
},
|
||||
|
||||
attributes() {
|
||||
// AWS specific
|
||||
if (this.plugin?.name === 'aws') {
|
||||
const filtersAmount = faker.number.int({ min: 1, max: 5 });
|
||||
let filters = [];
|
||||
for (let i = 0; i < filtersAmount; ++i) {
|
||||
filters.push(`${faker.word.words(1)}=${faker.word.words(1)}`);
|
||||
}
|
||||
return {
|
||||
filters,
|
||||
};
|
||||
}
|
||||
// Azure specific
|
||||
if (this.plugin?.name === 'azure') {
|
||||
return {
|
||||
filter: `${faker.database.column()}=${faker.database.collation()}`,
|
||||
};
|
||||
switch (this.plugin?.name) {
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AWS:
|
||||
return generateFilters('tag:');
|
||||
case TYPE_HOST_CATALOG_PLUGIN_AZURE:
|
||||
return {
|
||||
filter: `${faker.database.column()}=${faker.database.collation()}`,
|
||||
};
|
||||
case TYPE_HOST_CATALOG_PLUGIN_GCP:
|
||||
return generateFilters('labels.');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,24 +7,28 @@ import factory from '../generated/factories/host';
|
||||
import permissions from '../helpers/permissions';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import generateId from '../helpers/id';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_STATIC,
|
||||
TYPE_HOST_CATALOG_DYNAMIC,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('h_'),
|
||||
name() {
|
||||
if (this.type === 'static') return faker.word.words();
|
||||
if (this.type === TYPE_HOST_CATALOG_STATIC) return faker.word.words();
|
||||
},
|
||||
type() {
|
||||
return this.hostCatalog?.type || factory.attrs.type;
|
||||
},
|
||||
authorized_actions: function () {
|
||||
const isStatic = this.type === 'static';
|
||||
const isStatic = this.type === TYPE_HOST_CATALOG_STATIC;
|
||||
const defaults = ['no-op', 'read'];
|
||||
// Only static allows update/delete at this time.
|
||||
if (isStatic) defaults.push('update', 'delete');
|
||||
return permissions.authorizedActionsFor('host') || defaults;
|
||||
},
|
||||
plugin() {
|
||||
if (this.type === 'plugin') {
|
||||
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
|
||||
return this.hostCatalog.plugin;
|
||||
}
|
||||
},
|
||||
@@ -38,12 +42,12 @@ export default factory.extend({
|
||||
},
|
||||
// Return external fields only for plugins
|
||||
external_id() {
|
||||
if (this.type === 'plugin') {
|
||||
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
|
||||
return faker.string.uuid();
|
||||
}
|
||||
},
|
||||
external_name(i) {
|
||||
if (this.type === 'plugin') {
|
||||
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
|
||||
return `${this.plugin.name} provided name ${i}`;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ export default factory.extend({
|
||||
authorized_actions() {
|
||||
const authorizedActions = ['no-op', 'read', 'update'];
|
||||
|
||||
// Storage policies can only be attatched to global and org scopes.
|
||||
// Storage policies can only be attached to global and org scopes.
|
||||
if (this.type === 'global' || this.type === 'org') {
|
||||
authorizedActions.push('attach-storage-policy', 'detach-storage-policy');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
// import config from '../../config/environment';
|
||||
// import { v1 } from 'ember-uuid';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
// /**
|
||||
|
||||
@@ -83,6 +83,37 @@ export default function initializeMockIPC(server, config) {
|
||||
raw: btoa('just-a-random-string'),
|
||||
},
|
||||
},
|
||||
{
|
||||
credential_source: {
|
||||
id: 'clvlt_4cvscMTl0N',
|
||||
name: 'Credential Library 0',
|
||||
description: 'Source Description',
|
||||
credential_store_id: 'csvlt_Q1HFGt7Jpm',
|
||||
type: 'vault-generic',
|
||||
},
|
||||
secret: {
|
||||
raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ',
|
||||
decoded: {
|
||||
data: {
|
||||
backslash: 'password\\withslash\\',
|
||||
backslash1: 'password\\fwithslash\\f',
|
||||
password: '123',
|
||||
username: 'test',
|
||||
email: {
|
||||
address: 'test.com',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
created_time: '2024-04-12T18:38:36.226715555Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
credential_source: {
|
||||
id: 'credjson_7cKBbBEkC3',
|
||||
@@ -98,6 +129,7 @@ export default function initializeMockIPC(server, config) {
|
||||
nested_secret: {
|
||||
session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321',
|
||||
complex_nest: {
|
||||
blackslash: 'password\\withslash\\',
|
||||
hash: 'qazxswedcvfrtgbnjhyujm.1234567890',
|
||||
},
|
||||
},
|
||||
@@ -190,6 +222,27 @@ export default function initializeMockIPC(server, config) {
|
||||
isClientAgentRunning() {
|
||||
return false;
|
||||
}
|
||||
cacheDaemonStatus() {
|
||||
return { version: 'Boundary v1.0.0' };
|
||||
}
|
||||
getCliVersion() {
|
||||
return { versionNumber: '1.0.0' };
|
||||
}
|
||||
getDesktopVersion() {
|
||||
return { desktopVersion: '1.0.0' };
|
||||
}
|
||||
getLogLevel() {
|
||||
return 'info';
|
||||
}
|
||||
getLogPath() {
|
||||
return '~/.config/Boundary/logs/desktop-client.log';
|
||||
}
|
||||
setLogLevel() {}
|
||||
clientAgentStatus() {
|
||||
return { version: '0.0.1-dev', status: 'running' };
|
||||
}
|
||||
pauseClientAgent() {}
|
||||
resumeClientAgent() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
-17
@@ -24,37 +24,38 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dexie": "^3.2.4",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-auto-import": "^2.8.1",
|
||||
"ember-can": "^4.2.0",
|
||||
"ember-cli-babel": "^7.26.10",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-mirage": "^2.4.0",
|
||||
"ember-cli-mirage": "^3.0.3",
|
||||
"miragejs": "^0.1.48",
|
||||
"ember-data": "~4.12.0",
|
||||
"ember-fetch": "^8.1.1",
|
||||
"tracked-built-ins": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^4.0.4",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"concurrently": "^9.1.0",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-cli": "^4.12.1",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"ember-cli": "^5.12.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-exam": "^8.0.0",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -64,16 +65,12 @@
|
||||
"eslint-plugin-ember": "^11.11.1",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-qunit": "^8.0.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": ">4.0.0"
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.2.1",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
|
||||
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupIntl(hooks, 'en-us'); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from 'dummy/config/environment';
|
||||
import { initialize } from 'dummy/initializers/deprecations';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Initializer | deprecations', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
};
|
||||
|
||||
this.TestApplication.initializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
});
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.application.boot();
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,19 @@ module('Unit | Model | host catalog', function (hooks) {
|
||||
assert.false(modelRandom.isAzure);
|
||||
});
|
||||
|
||||
test('it has isGCP property and return the expected values', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelGCP = store.createRecord('host-catalog', {
|
||||
type: 'plugin',
|
||||
plugin: { name: 'gcp' },
|
||||
});
|
||||
const modelRandom = store.createRecord('host-catalog', {
|
||||
plugin: { name: 'random' },
|
||||
});
|
||||
assert.true(modelGCP.isGCP);
|
||||
assert.false(modelRandom.isGCP);
|
||||
});
|
||||
|
||||
test('get compositeType returns expected values', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('host-catalog', {
|
||||
@@ -94,8 +107,13 @@ module('Unit | Model | host catalog', function (hooks) {
|
||||
type: 'plugin',
|
||||
plugin: { name: 'no-such-type' },
|
||||
});
|
||||
const modelD = store.createRecord('host-catalog', {
|
||||
type: 'plugin',
|
||||
plugin: { name: 'gcp' },
|
||||
});
|
||||
assert.strictEqual(modelA.compositeType, 'static');
|
||||
assert.strictEqual(modelB.compositeType, 'aws');
|
||||
assert.strictEqual(modelD.compositeType, 'gcp');
|
||||
assert.strictEqual(modelC.compositeType, 'unknown');
|
||||
});
|
||||
|
||||
@@ -107,8 +125,12 @@ module('Unit | Model | host catalog', function (hooks) {
|
||||
const modelStatic = store.createRecord('host-catalog', {
|
||||
compositeType: 'static',
|
||||
});
|
||||
const modelGCP = store.createRecord('host-catalog', {
|
||||
compositeType: 'gcp',
|
||||
});
|
||||
assert.strictEqual(modelPlugin.type, 'plugin');
|
||||
assert.strictEqual(modelPlugin.plugin.name, 'aws');
|
||||
assert.strictEqual(modelStatic.type, 'static');
|
||||
assert.strictEqual(modelGCP.plugin.name, 'gcp');
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { SessionCredential } from 'api/models/session';
|
||||
import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library';
|
||||
|
||||
module('Unit | Model | session', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -16,6 +18,46 @@ module('Unit | Model | session', function (hooks) {
|
||||
assert.notOk(model.isAvailable);
|
||||
});
|
||||
|
||||
test('`extractSecrets` method should return an array of secret items', function (assert) {
|
||||
const sessionCredential = new SessionCredential({
|
||||
credential_source: {
|
||||
id: '123',
|
||||
name: 'Test Source',
|
||||
description: 'Test Description',
|
||||
type: 'Test Type',
|
||||
},
|
||||
});
|
||||
|
||||
const secrets = {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
};
|
||||
|
||||
const formattedSecretes = sessionCredential.extractSecrets(
|
||||
secrets,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
);
|
||||
assert.deepEqual(formattedSecretes, [
|
||||
new SessionCredential.SecretItem('username', 'user'),
|
||||
new SessionCredential.SecretItem('password', 'pass'),
|
||||
]);
|
||||
|
||||
// check for nested data, empty values should be filtered out
|
||||
const nestedSecrets = {
|
||||
data: { username: 'user', password: '', email: 'test.com' },
|
||||
};
|
||||
|
||||
const formattedNestedSecrets = sessionCredential.extractSecrets(
|
||||
nestedSecrets,
|
||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||
);
|
||||
|
||||
assert.deepEqual(formattedNestedSecrets, [
|
||||
new SessionCredential.SecretItem('username', 'user'),
|
||||
new SessionCredential.SecretItem('email', 'test.com'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('it allows cancellation of an active session', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('session', {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
|
||||
import { TYPE_HOST_CATALOG_PLUGIN_GCP } from 'api/models/host-catalog';
|
||||
|
||||
module('Unit | Serializer | application', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -133,7 +134,7 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes attributes with `for` option containing array or string correctly', function (assert) {
|
||||
test('it serializes attributes with `for` option containing array or string correctly for non plugins', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('auth-method', {
|
||||
type: TYPE_AUTH_METHOD_OIDC, //has both string and array `for` option
|
||||
@@ -184,6 +185,54 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes attributes with `for` option containing array or string correctly for plugin types', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('host-catalog', {
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
name: 'GCP plugin',
|
||||
description: 'this is a GCP plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_tags: [
|
||||
{ key: 'Project', value: 'Automation' },
|
||||
{ key: 'foo', value: 'bar' },
|
||||
],
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
client_email: 'email',
|
||||
target_service_account_id: 'service-account',
|
||||
private_key_id: 'key-id',
|
||||
private_key: 'key',
|
||||
});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
const expectedResult = {
|
||||
name: 'GCP plugin',
|
||||
description: 'this is a GCP plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
target_service_account_id: 'service-account',
|
||||
client_email: 'email',
|
||||
},
|
||||
secrets: {
|
||||
private_key_id: 'key-id',
|
||||
private_key: 'key',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(serializedRecord, expectedResult);
|
||||
});
|
||||
|
||||
test('it normalizes array records from an `items` root key', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('user');
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_CREDENTIAL_STATIC,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
module('Unit | Serializer | host catalog', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -27,17 +33,153 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
name: 'static',
|
||||
description: 'this is a static host-catalog',
|
||||
type: 'static',
|
||||
attributes: {},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws plugin as expected, ignoring azure fields', async function (assert) {
|
||||
test('it serializes a new gcp plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: 'aws',
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||
name: 'GCP',
|
||||
description: 'this is a GCP plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_tags: [
|
||||
{ key: 'Project', value: 'Automation' },
|
||||
{ key: 'foo', value: 'bar' },
|
||||
],
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
client_email: 'email',
|
||||
target_service_account_id: 'service-account',
|
||||
private_key_id: 'key-id',
|
||||
private_key: 'key',
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'GCP',
|
||||
description: 'this is a GCP plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
target_service_account_id: 'service-account',
|
||||
client_email: 'email',
|
||||
},
|
||||
secrets: {
|
||||
private_key_id: 'key-id',
|
||||
private_key: 'key',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws static plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
credentialType: TYPE_CREDENTIAL_STATIC,
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'west',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_tags: [
|
||||
{ key: 'Project', value: 'Automation' },
|
||||
{ key: 'foo', value: 'bar' },
|
||||
],
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'west',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws dynamic credential type plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
name: 'AWS',
|
||||
credentialType: TYPE_CREDENTIAL_DYNAMIC,
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are static AWS fields and should be excluded
|
||||
region: 'west',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
// these are Azure fields and should be excluded
|
||||
tenant_id: 'a1b2c3',
|
||||
client_id: 'a1b2c3',
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_tags: [
|
||||
{ key: 'Project', value: 'Automation' },
|
||||
{ key: 'foo', value: 'bar' },
|
||||
],
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'west',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_arn: 'test',
|
||||
role_tags: { Project: 'Automation', foo: 'bar' },
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws static credential type plugin as expected, ignoring azure and GCP fields', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
name: 'AWS',
|
||||
credentialType: TYPE_CREDENTIAL_STATIC,
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
region: 'spain',
|
||||
access_key_id: 'foobars',
|
||||
@@ -48,14 +190,21 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'spain',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
@@ -65,7 +214,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new azure record as expected, ignoring aws fields', async function (assert) {
|
||||
test('it serializes a new azure record as expected, ignoring aws and GCP fields', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: 'azure',
|
||||
@@ -75,12 +224,15 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
region: 'spain',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
worker_filter: 'workerfilter',
|
||||
// these are Azure fields and should be excluded
|
||||
tenant_id: 'a1b2c3',
|
||||
client_id: 'a1b2c3',
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'Azure',
|
||||
@@ -106,6 +258,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'host-catalog',
|
||||
credentialType: 'static',
|
||||
attributes: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
@@ -113,6 +266,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
plugin: {
|
||||
name: 'aws',
|
||||
},
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'andorra',
|
||||
},
|
||||
},
|
||||
@@ -122,9 +276,14 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
description: 'test description',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: false,
|
||||
region: 'andorra',
|
||||
role_arn: null,
|
||||
role_external_id: null,
|
||||
role_session_name: null,
|
||||
role_tags: null,
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
@@ -164,7 +323,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it normalizes an static host catalog as expected', function (assert) {
|
||||
test('it normalizes a static host catalog as expected', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('host-catalog');
|
||||
const hostCatalog = store.createRecord('host-catalog').constructor;
|
||||
@@ -193,6 +352,8 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
secret_access_key: null,
|
||||
secret_id: null,
|
||||
secret_value: null,
|
||||
private_key_id: null,
|
||||
private_key: null,
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
@@ -215,6 +376,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
name: 'aws',
|
||||
description: 'aws host catalog',
|
||||
},
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: false,
|
||||
region: 'Illinois',
|
||||
@@ -238,6 +400,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
type: 'plugin',
|
||||
description: 'Test description',
|
||||
authorized_actions: ['no-op'],
|
||||
worker_filter: 'workerfilter',
|
||||
plugin: {
|
||||
id: 'plugin-id-5',
|
||||
name: 'aws',
|
||||
@@ -249,6 +412,8 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
secret_access_key: 'zq{2:IVc8@W^',
|
||||
secret_id: null,
|
||||
secret_value: null,
|
||||
private_key_id: null,
|
||||
private_key: null,
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
@@ -309,6 +474,70 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
subscription_id: 'subscription',
|
||||
access_key_id: null,
|
||||
secret_access_key: null,
|
||||
private_key_id: null,
|
||||
private_key: null,
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
included: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('it normalizes a GCP plugin type host catalog as expected', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('host-catalog');
|
||||
const hostCatalog = store.createRecord('host-catalog').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
name: 'Host catalog test',
|
||||
description: 'Test description',
|
||||
authorized_actions: ['no-op', 'read', 'update', 'delete'],
|
||||
type: 'plugin',
|
||||
plugin: {
|
||||
id: 'plugin-id-7',
|
||||
name: 'gcp',
|
||||
description: 'gcp host catalog',
|
||||
},
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
project_id: 'project_id',
|
||||
zone: 'zone',
|
||||
target_service_account_id: 'target_service_account_id',
|
||||
},
|
||||
secrets: {
|
||||
private_key_id: '0xF3B0a6f8',
|
||||
private_key: 'zq{2:IVc8@W^',
|
||||
},
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
hostCatalog,
|
||||
payload,
|
||||
);
|
||||
assert.deepEqual(normalized, {
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'host-catalog',
|
||||
attributes: {
|
||||
name: 'Host catalog test',
|
||||
type: 'plugin',
|
||||
description: 'Test description',
|
||||
authorized_actions: ['no-op', 'read', 'update', 'delete'],
|
||||
plugin: {
|
||||
id: 'plugin-id-7',
|
||||
name: 'gcp',
|
||||
description: 'gcp host catalog',
|
||||
},
|
||||
disable_credential_rotation: true,
|
||||
private_key_id: '0xF3B0a6f8',
|
||||
private_key: 'zq{2:IVc8@W^',
|
||||
project_id: 'project_id',
|
||||
zone: 'zone',
|
||||
target_service_account_id: 'target_service_account_id',
|
||||
access_key_id: null,
|
||||
secret_access_key: null,
|
||||
secret_id: null,
|
||||
secret_value: null,
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ class SessionService extends Service {}
|
||||
module('Unit | Service | indexed-db', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
this.owner.register('service:session', SessionService);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { setupTest } from 'dummy/tests/helpers';
|
||||
module('Unit | Service | store', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:store');
|
||||
assert.ok(service);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { flattenObject } from 'api/utils/flatten-nested-object';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | flatten-nested-object', function () {
|
||||
test('it flattens a nested object to single level object', function (assert) {
|
||||
const input = {
|
||||
a: 1,
|
||||
b: { c: 2, d: 3, e: { f: { g: { h: 10 } } } },
|
||||
};
|
||||
const expected = {
|
||||
a: 1,
|
||||
'b.c': 2,
|
||||
'b.d': 3,
|
||||
'b.e.f.g.h': 10,
|
||||
};
|
||||
assert.deepEqual(flattenObject(input), expected);
|
||||
});
|
||||
|
||||
test('removes keys with null/empty values', function (assert) {
|
||||
const inputOne = {
|
||||
a: null,
|
||||
b: { c: 2 },
|
||||
d: {
|
||||
e: { f: { g: { h: '' } } },
|
||||
},
|
||||
i: 0,
|
||||
j: false,
|
||||
};
|
||||
const inputTwo = {
|
||||
d: { e: null },
|
||||
};
|
||||
const expectedResultOne = {
|
||||
'b.c': 2,
|
||||
i: 0,
|
||||
j: false,
|
||||
};
|
||||
const expectedResultTwo = {};
|
||||
|
||||
assert.deepEqual(flattenObject(inputOne), expectedResultOne);
|
||||
assert.deepEqual(flattenObject(inputTwo), expectedResultTwo);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { camelize } from '@ember/string';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const seedIndexDb = async (resource, store, indexedDb, server) => {
|
||||
const resourceData =
|
||||
@@ -529,4 +530,80 @@ module('Unit | Utility | indexed-db-query', function (hooks) {
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('it filters on date type fields and returns results', async function (assert) {
|
||||
const pastDate = new Date('2024-09-19T10:00:00.000Z');
|
||||
const createdTime = new Date('2024-09-23T10:00:00.000Z');
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime,
|
||||
});
|
||||
await seedIndexDb('session-recording', store, indexedDb, this.server);
|
||||
const query = { filters: { created_time: [{ gte: pastDate }] } };
|
||||
|
||||
const result = await queryIndexedDb(
|
||||
indexedDb.db,
|
||||
'session-recording',
|
||||
query,
|
||||
);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
});
|
||||
|
||||
test('it filters on date type fields and returns results - from and to', async function (assert) {
|
||||
const from = new Date('2024-09-19T10:00:00.000Z');
|
||||
const to = new Date('2024-09-25T10:00:00.000Z');
|
||||
const createdTime = new Date('2024-09-23T10:00:00.000Z');
|
||||
const createdTime2 = new Date('2024-09-28T10:00:00.000Z');
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime,
|
||||
});
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime2,
|
||||
});
|
||||
await seedIndexDb('session-recording', store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
created_time: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ gte: from }, { lte: to }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(
|
||||
indexedDb.db,
|
||||
'session-recording',
|
||||
query,
|
||||
);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(
|
||||
result[0].attributes.created_time.toISOString(),
|
||||
createdTime.toISOString(),
|
||||
);
|
||||
});
|
||||
|
||||
test('it filters on date type fields and returns no results', async function (assert) {
|
||||
const createdTime = faker.date.between({
|
||||
from: '2024-09-19T00:00:00.000Z',
|
||||
to: '2024-09-20T00:00:00.000Z',
|
||||
});
|
||||
const pastDate = faker.date.between({
|
||||
from: '2024-09-23T00:00:00.000Z',
|
||||
to: '2024-09-24T00:00:00.000Z',
|
||||
});
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime,
|
||||
});
|
||||
await seedIndexDb('session-recording', store, indexedDb, this.server);
|
||||
const query = { filters: { created_time: [{ gte: pastDate }] } };
|
||||
|
||||
const result = await queryIndexedDb(
|
||||
indexedDb.db,
|
||||
'session-recording',
|
||||
query,
|
||||
);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import SimpleAuthBaseAuthenticator from 'ember-simple-auth/authenticators/base';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
import { waitForPromise } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* Encapsulates common authenticator functionality.
|
||||
@@ -58,10 +58,21 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
*/
|
||||
async validateToken(token, tokenID) {
|
||||
const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID);
|
||||
const response = await fetch(tokenValidationURL, {
|
||||
method: 'get',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
|
||||
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
|
||||
const response = await waitForPromise(
|
||||
fetch(tokenValidationURL, {
|
||||
method: 'get',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
);
|
||||
// Note: Always consume response body in order to avoid memory leaks
|
||||
// visit https://undici.nodejs.org/#/?id=garbage-collection for more info.
|
||||
// We do not use the undici package but the link informs us that garbage
|
||||
// collection is undefined when response body is not consumed.
|
||||
await response.json();
|
||||
|
||||
// 401 and 404 responses mean the token is invalid, whereas other types of
|
||||
// error responses do not tell us about the validity of the token.
|
||||
if (response.status === 401 || response.status === 404) return reject();
|
||||
@@ -103,7 +114,7 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts to the `deauthEntpoint` on a best-effort basis and then returns.
|
||||
* Posts to the `deauthEndpoint` on a best-effort basis and then returns.
|
||||
* Deauthentication with the server is not guaranteed and request failures
|
||||
* are ignored.
|
||||
*
|
||||
@@ -113,12 +124,14 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
async invalidate(options) {
|
||||
const { token } = options;
|
||||
const deauthEndpointURL = this.buildDeauthEndpointURL(options);
|
||||
await fetch(deauthEndpointURL, {
|
||||
method: 'delete',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).catch(() => {
|
||||
/* no op */
|
||||
});
|
||||
await waitForPromise(
|
||||
fetch(deauthEndpointURL, {
|
||||
method: 'delete',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).catch(() => {
|
||||
/* no op */
|
||||
}),
|
||||
);
|
||||
return super.invalidate(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
import BaseAuthenticator from './base';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
import { waitForPromise } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* The OIDC base authenticator encapsulates the multistep OIDC flow.
|
||||
* The OIDC base authenticator encapsulates the multi-step OIDC flow.
|
||||
*
|
||||
* 1. Start authentication flow: this step is actually a combination of two
|
||||
* sub steps:
|
||||
@@ -47,7 +47,9 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
async startAuthentication(options) {
|
||||
const url = this.buildAuthEndpointURL(options);
|
||||
const body = JSON.stringify({ command: 'start' });
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
|
||||
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
|
||||
const response = await waitForPromise(fetch(url, { method: 'post', body }));
|
||||
const json = await response.json();
|
||||
if (response.status < 400) {
|
||||
// Store meta about the pending OIDC flow
|
||||
@@ -94,14 +96,20 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
},
|
||||
});
|
||||
// Fetch the endpoint and get the response JSON
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
const response = await waitForPromise(fetch(url, { method: 'post', body }));
|
||||
|
||||
// Note: Always consume response body in order to avoid memory leaks
|
||||
// visit https://undici.nodejs.org/#/?id=garbage-collection for more info.
|
||||
// We do not use the undici package but the link informs us that garbage
|
||||
// collection is undefined when response body is not consumed.
|
||||
const json = await response.json();
|
||||
|
||||
if (response.status === 202) {
|
||||
// The token isn't ready yet, keep trying.
|
||||
return false;
|
||||
} else if (response.status < 400) {
|
||||
// Response was successful, meaning a token was obtained.
|
||||
// Authenticate with the session service using the response JSON.
|
||||
const json = await response.json();
|
||||
await this.session.authenticate('authenticator:oidc', json);
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import BaseAuthenticator from './base';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
import { waitForPromise } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -66,7 +66,11 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
attributes: { login_name, password },
|
||||
});
|
||||
const authEndpointURL = this.buildAuthEndpointURL(options);
|
||||
const response = await fetch(authEndpointURL, { method: 'post', body });
|
||||
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
|
||||
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
|
||||
const response = await waitForPromise(
|
||||
fetch(authEndpointURL, { method: 'post', body }),
|
||||
);
|
||||
const json = await response.json();
|
||||
return response.status < 400
|
||||
? resolve(this.normalizeData(json, login_name))
|
||||
|
||||
+14
-13
@@ -21,35 +21,36 @@
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"ember-cli-babel": "^7.26.11",
|
||||
"ember-auto-import": "2.8.1",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-fetch": "^8.1.1",
|
||||
"ember-simple-auth": "^4.2.2"
|
||||
"ember-simple-auth": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^4.0.4",
|
||||
"@ember/test-waiters": "^3.1.0",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"concurrently": "^9.1.0",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-cli": "^4.12.2",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"ember-cli": "^5.12.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-cookies": "^0.5.2",
|
||||
"ember-cookies": "^1.2.0",
|
||||
"ember-disable-prototype-extensions": "^1.1.3",
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -62,9 +63,9 @@
|
||||
"loader.js": "^4.7.0",
|
||||
"pretender": "^3.4.3",
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"webpack": "^5.78.0"
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.2.1",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
|
||||
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupIntl(hooks, 'en-us'); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return [200];
|
||||
return [200, {}, '{}'];
|
||||
});
|
||||
await authenticator.restore(mockData);
|
||||
});
|
||||
@@ -64,7 +64,7 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return [401];
|
||||
return [401, {}, '{}'];
|
||||
});
|
||||
try {
|
||||
await authenticator.restore(mockData);
|
||||
@@ -80,7 +80,7 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return [404];
|
||||
return [404, {}, '{}'];
|
||||
});
|
||||
try {
|
||||
await authenticator.restore(mockData);
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Message
|
||||
@title={{t (concat 'errors.' this.messageCode '.title')}}
|
||||
@subtitle={{concat (t 'errors.generic.title') ' ' @status}}
|
||||
@icon={{this.icon}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{t (concat 'errors.' this.messageCode '.description')}}
|
||||
</message.description>
|
||||
{{#if this.helpRoute}}
|
||||
<message.link @route={{this.helpRoute}}>{{t 'actions.help'}}</message.link>
|
||||
<Hds::ApplicationState data-test-error-application-state as |A|>
|
||||
<A.Header
|
||||
@title={{t (concat 'errors.' this.messageCode '.title')}}
|
||||
@icon={{this.icon}}
|
||||
@errorCode={{@status}}
|
||||
/>
|
||||
<A.Body @text={{t (concat 'errors.' this.messageCode '.description')}} />
|
||||
{{#if @docLink}}
|
||||
<A.Footer as |F|>
|
||||
<F.LinkStandalone
|
||||
@icon='help'
|
||||
@text={{t 'actions.help'}}
|
||||
@href={{@docLink}}
|
||||
@iconPosition='trailing'
|
||||
/>
|
||||
</A.Footer>
|
||||
{{/if}}
|
||||
</Rose::Message>
|
||||
</Hds::ApplicationState>
|
||||
@@ -3,8 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import MessageComponent from 'rose/components/rose/message';
|
||||
import { computed } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
/*
|
||||
* Helpful error booleans are attached based on the error status code:
|
||||
@@ -22,40 +21,29 @@ import { computed } from '@ember/object';
|
||||
|
||||
const statuses = ['401', '403', '404', '500'];
|
||||
|
||||
export default class ErrorMessageComponent extends MessageComponent {
|
||||
export default class ErrorMessageComponent extends Component {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns an icon for error status.
|
||||
* @return {string}
|
||||
*/
|
||||
@computed('args.status')
|
||||
get icon() {
|
||||
switch (this.args.status) {
|
||||
case '404':
|
||||
return 'flight-icons/svg/help-16';
|
||||
return 'help';
|
||||
case '401':
|
||||
case '403':
|
||||
default:
|
||||
return 'flight-icons/svg/alert-circle-16';
|
||||
return 'alert-circle';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Enable when help documentation can be linked in.
|
||||
/**
|
||||
* Returns an route string for help text in message.
|
||||
* @return {string}
|
||||
*/
|
||||
// get helpRoute() {
|
||||
// return 'index'
|
||||
// }
|
||||
|
||||
/**
|
||||
* Returns 'unknown' status code when provided error status
|
||||
* isn't part of predefined statuses.
|
||||
* @return {string}
|
||||
*/
|
||||
@computed('args.status')
|
||||
get messageCode() {
|
||||
let messageCode = this.args.status;
|
||||
if (!statuses.includes(messageCode)) messageCode = 'unknown';
|
||||
|
||||
@@ -3,17 +3,10 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Button
|
||||
...attributes
|
||||
@style='inline-link-action'
|
||||
@iconLeft={{if
|
||||
this.isLoading
|
||||
'flight-icons/svg/loading-16'
|
||||
'flight-icons/svg/reload-16'
|
||||
}}
|
||||
class={{if this.isLoading 'loading-button is-loading' 'loading-button'}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.refresh'}}
|
||||
@color='secondary'
|
||||
@icon={{if this.isLoading 'loading' 'reload'}}
|
||||
@disabled={{this.isLoading}}
|
||||
{{on 'click' this.toggleRefresh}}
|
||||
>
|
||||
{{yield}}
|
||||
</Rose::Button>
|
||||
/>
|
||||
@@ -3,12 +3,17 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if this.lastRefreshed}}
|
||||
<Rose::Toolbar::Info>
|
||||
{{relative-datetime-live this.lastRefreshed}}
|
||||
</Rose::Toolbar::Info>
|
||||
{{/if}}
|
||||
|
||||
<LoadingButton @onClick={{this.onClick}}>
|
||||
{{t 'actions.refresh'}}
|
||||
</LoadingButton>
|
||||
<div class='toolbar-refresher'>
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{#if this.lastRefreshed}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{relative-datetime-live this.lastRefreshed}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<LoadingButton @onClick={{this.onClick}}>
|
||||
{{t 'actions.refresh'}}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@ export default class ToolbarRefresherComponent extends Component {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Executes the click handler argument and trackes the time it completed.
|
||||
* Executes the click handler argument and tracks the time it completed.
|
||||
*/
|
||||
@action
|
||||
async onClick() {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
|
||||
import { DurationFormat } from '@formatjs/intl-durationformat';
|
||||
import { secondsToDuration } from 'core/utils/seconds-to-duration';
|
||||
|
||||
export default class extends Helper {
|
||||
// =services
|
||||
@@ -19,12 +20,10 @@ export default class extends Helper {
|
||||
*/
|
||||
compute([durationInMs]) {
|
||||
const durationInSeconds = durationInMs / 1000;
|
||||
const durationFormat = new DurationUnitFormat(this.intl.primaryLocale, {
|
||||
const durationFormat = new DurationFormat(this.intl.primaryLocale, {
|
||||
style: 'narrow',
|
||||
format: '{days} {hours} {minutes} {seconds}',
|
||||
round: true,
|
||||
});
|
||||
|
||||
return durationFormat.format(durationInSeconds);
|
||||
return durationFormat.format(secondsToDuration(durationInSeconds));
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { join } from '@ember/runloop';
|
||||
import { runInDebug, assert } from '@ember/debug';
|
||||
import { A } from '@ember/array';
|
||||
|
||||
/**
|
||||
* This helper is a modern update on ember-route-action-helper, which is
|
||||
* now unmaintained.
|
||||
*
|
||||
* See https://github.com/DockYard/ember-route-action-helper
|
||||
*/
|
||||
export default class RouteActionHelper extends Helper {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
|
||||
// =attributes
|
||||
|
||||
get routes() {
|
||||
const { currentInfos, mapBy } = this.currentInfos;
|
||||
return A(currentInfos).mapBy(mapBy).reverse();
|
||||
}
|
||||
|
||||
get currentInfos() {
|
||||
// eslint-disable-next-line ember/no-private-routing-service
|
||||
const routerLib = this.router._router._routerMicrolib;
|
||||
return {
|
||||
currentInfos:
|
||||
routerLib.currentRouteInfos || routerLib.currentHandlerInfos,
|
||||
mapBy: (routerLib.currentRouteInfos && 'route') || 'handler',
|
||||
};
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
compute([actionName, ...outerArgs]) {
|
||||
const { action, handler } = this.getRouteWithAction(actionName);
|
||||
|
||||
runInDebug(() => {
|
||||
assert(
|
||||
`
|
||||
[boundary-core-helper-route-action]
|
||||
Unable to find action ${actionName}
|
||||
`,
|
||||
handler,
|
||||
);
|
||||
});
|
||||
|
||||
return (...innerArgs) =>
|
||||
join(handler, action, ...outerArgs.concat(innerArgs));
|
||||
}
|
||||
|
||||
getRouteWithAction(actionName) {
|
||||
let action;
|
||||
const handler = A(this.routes).find((route) => {
|
||||
const actions = route.actions || route._actions;
|
||||
action = actions[actionName];
|
||||
return typeof action === 'function';
|
||||
});
|
||||
|
||||
return { action, handler };
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
|
||||
import { DurationFormat } from '@formatjs/intl-durationformat';
|
||||
import { secondsToDuration } from 'core/utils/seconds-to-duration';
|
||||
|
||||
export default class extends Helper {
|
||||
// =services
|
||||
@@ -29,14 +30,13 @@ export default class extends Helper {
|
||||
difference > 0 ? Math.floor(difference / 1000) : 0;
|
||||
|
||||
// Format the duration
|
||||
const duration = new DurationUnitFormat(this.intl.primaryLocale, {
|
||||
style: 'timer',
|
||||
format: `{hours}:{minutes}:{seconds} ${this.intl.t(
|
||||
'resources.session.remaining',
|
||||
)}`,
|
||||
const duration = new DurationFormat(this.intl.primaryLocale, {
|
||||
style: 'digital',
|
||||
});
|
||||
const formattedTime = duration.format(
|
||||
secondsToDuration(differenceInSeconds),
|
||||
);
|
||||
|
||||
// Return the formatted duration
|
||||
return duration.format(differenceInSeconds);
|
||||
return `${formattedTime} ${this.intl.t('resources.session.remaining')}`;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.filter-tag {
|
||||
@@ -27,33 +26,20 @@
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.hds-segmented-group {
|
||||
> div,
|
||||
table {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hds-segmented-group {
|
||||
.hds-dropdown-toggle-button__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.hds-table {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.search-filtering-toolbar {
|
||||
display: flex;
|
||||
|
||||
// TODO: Remove these copied styles by migrating to HDS for the toolbar refresher component.
|
||||
// The original styles assumed we were using the old toolbar component under a "rose-toolbar" class.
|
||||
.rose-button-inline-link-action {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.8461538462;
|
||||
font-weight: 600;
|
||||
color: var(--stark);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -67,3 +53,22 @@
|
||||
color: var(--token-color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-refresher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
> .hds-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility classes
|
||||
.container-inputs__margin-bottom {
|
||||
input:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Takes in the number of seconds and converts to an object with corresponding
|
||||
* values for each time duration. For example:
|
||||
* { weeks: 0, days: 0, hours: 22, minutes: 15, seconds: 11 }
|
||||
* @param {number} value
|
||||
* @returns {object}
|
||||
*/
|
||||
export const secondsToDuration = (value) => {
|
||||
const second = 1;
|
||||
const minute = second * 60;
|
||||
const hour = minute * 60;
|
||||
const day = hour * 24;
|
||||
const week = day * 7;
|
||||
|
||||
const weeks = Math.max(Math.floor(value / week), 0);
|
||||
const days = Math.max(Math.floor((value % week) / day), 0);
|
||||
const hours = Math.max(Math.floor((value % day) / hour), 0);
|
||||
const minutes = Math.max(Math.floor((value % hour) / minute), 0);
|
||||
const seconds = Math.max(Math.floor((value % minute) / second), 0);
|
||||
|
||||
return { weeks, days, hours, minutes, seconds };
|
||||
};
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/route-action';
|
||||
export { default } from 'core/utils/seconds-to-duration';
|
||||
+14
-17
@@ -31,19 +31,20 @@
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-durationformat": "^0.2.4",
|
||||
"api": "*",
|
||||
"auth": "*",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-auto-import": "^2.8.1",
|
||||
"ember-browser-services": "^4.0.3",
|
||||
"ember-cli-babel": "^7.26.11",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-sass": "^11.0.1",
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-intl": "^6.0.0-beta.6",
|
||||
"ember-intl": "^7.0.7",
|
||||
"ember-loading": "^2.0.0",
|
||||
"ember-truth-helpers": "^3.0.0",
|
||||
"filesize": "^10.0.7",
|
||||
"intl-unofficial-duration-unit-format": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -51,23 +52,23 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^4.0.4",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"concurrently": "^9.1.0",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-cli": "^4.12.2",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"ember-cli": "^5.12.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-inline-svg": "^1.0.1",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -81,15 +82,11 @@
|
||||
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.2.1",
|
||||
"rose": "*",
|
||||
"sinon": "^17.0.1",
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": "^4.0.0",
|
||||
"lodash": "^4.17.21"
|
||||
"sinon": "^19.0.2",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
|
||||
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupIntl(hooks, 'en-us'); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | dropdown/index', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
const firstItem = {
|
||||
id: '1',
|
||||
|
||||
@@ -5,69 +5,73 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Component | error/message', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
const ERROR_MSG_TITLE =
|
||||
'[data-test-error-application-state] .hds-application-state__title';
|
||||
const ERROR_MSG_CODE =
|
||||
'[data-test-error-application-state] .hds-application-state__error-code';
|
||||
const ERROR_MSG_BODY =
|
||||
'[data-test-error-application-state] .hds-application-state__body-text';
|
||||
const ERROR_MSG_LINK =
|
||||
'[data-test-error-application-state] .hds-link-standalone';
|
||||
const ERROR_MSG_LINK_TEXT =
|
||||
'[data-test-error-application-state] .hds-link-standalone__text';
|
||||
|
||||
test('it renders predefined message for known error status', async function (assert) {
|
||||
await render(hbs`<Error::Message @status='401' />`);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'You are not signed in',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-subtitle').textContent.trim(),
|
||||
'Error 401',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-description').textContent.trim(),
|
||||
'You are not signed in. Please sign in and try again later.',
|
||||
);
|
||||
|
||||
assert.dom(ERROR_MSG_TITLE).hasText('You are not signed in');
|
||||
assert.dom(ERROR_MSG_CODE).hasText('ERROR 401');
|
||||
assert
|
||||
.dom(ERROR_MSG_BODY)
|
||||
.hasText('You are not signed in. Please sign in and try again later.');
|
||||
|
||||
await render(hbs`<Error::Message @status='403' />`);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'You are not authorized',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-subtitle').textContent.trim(),
|
||||
'Error 403',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-description').textContent.trim(),
|
||||
'You must be granted permissions to view this data. Ask your administrator if you think you should have access.',
|
||||
);
|
||||
|
||||
assert.dom(ERROR_MSG_TITLE).hasText('You are not authorized');
|
||||
assert.dom(ERROR_MSG_CODE).hasText('ERROR 403');
|
||||
assert
|
||||
.dom(ERROR_MSG_BODY)
|
||||
.hasText(
|
||||
'You must be granted permissions to view this data. Ask your administrator if you think you should have access.',
|
||||
);
|
||||
|
||||
await render(hbs`<Error::Message @status='404' />`);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'Resource not found',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-subtitle').textContent.trim(),
|
||||
'Error 404',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-description').textContent.trim(),
|
||||
'We could not find the requested resource. You can ask your administrator or try again later.',
|
||||
);
|
||||
|
||||
assert.dom(ERROR_MSG_TITLE).hasText('Resource not found');
|
||||
assert.dom(ERROR_MSG_CODE).hasText('ERROR 404');
|
||||
assert
|
||||
.dom(ERROR_MSG_BODY)
|
||||
.hasText(
|
||||
'We could not find the requested resource. You can ask your administrator or try again later.',
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders default error for unknown error status', async function (assert) {
|
||||
await render(hbs`<Error::Message @status='599' />`);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'Something went wrong',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-subtitle').textContent.trim(),
|
||||
'Error 599',
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-message-description').textContent.trim(),
|
||||
"We're not sure what happened. Please contact your administrator or try again later.",
|
||||
|
||||
assert.dom(ERROR_MSG_TITLE).hasText('Something went wrong');
|
||||
assert.dom(ERROR_MSG_CODE).hasText('ERROR 599');
|
||||
assert
|
||||
.dom(ERROR_MSG_BODY)
|
||||
.hasText(
|
||||
"We're not sure what happened. Please contact your administrator or try again later.",
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders help link with provided doc url', async function (assert) {
|
||||
await render(
|
||||
hbs`<Error::Message @status='599' @docLink='https://support' />`,
|
||||
);
|
||||
|
||||
assert.dom(ERROR_MSG_LINK_TEXT).hasText('Need help');
|
||||
assert.dom(ERROR_MSG_LINK).hasAttribute('href', 'https://support');
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | filter-tags/index', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
this.set('filter', {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
module('Integration | Component | form | authenticate', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it shows login name and password when LDAP is selected from the list', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
@@ -7,19 +7,22 @@ import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find, click } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Component | loading-button', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<LoadingButton />`);
|
||||
assert.ok(find('.loading-button'));
|
||||
assert.ok(find('.hds-button'));
|
||||
});
|
||||
|
||||
test('it executes a function on refresh button click', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.onClick = () => assert.ok(true, 'refresh was clicked');
|
||||
await render(hbs`<LoadingButton @onClick={{this.onClick}} />`);
|
||||
assert.ok(find('.loading-button'));
|
||||
assert.ok(find('.hds-button'));
|
||||
await click('button');
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ module('Integration | Component | session-status', function (hooks) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.model = store.createRecord('session', { status: 'active' });
|
||||
await render(hbs`<SessionStatus @model={{this.model}} />`);
|
||||
assert.ok(find('.flight-icon-check'));
|
||||
assert.ok(find('.hds-icon-check'));
|
||||
assert.ok(find('.hds-badge--color-success'));
|
||||
assert.ok(find('.hds-badge--type-filled'));
|
||||
});
|
||||
@@ -24,7 +24,7 @@ module('Integration | Component | session-status', function (hooks) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.model = store.createRecord('session', { status: 'canceling' });
|
||||
await render(hbs`<SessionStatus @model={{this.model}} />`);
|
||||
assert.ok(find('.flight-icon-alert-triangle'));
|
||||
assert.ok(find('.hds-icon-alert-triangle'));
|
||||
assert.ok(find('.hds-badge--color-warning'));
|
||||
assert.ok(find('.hds-badge--type-filled'));
|
||||
});
|
||||
@@ -33,7 +33,7 @@ module('Integration | Component | session-status', function (hooks) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.model = store.createRecord('session', { status: 'terminated' });
|
||||
await render(hbs`<SessionStatus @model={{this.model}} />`);
|
||||
assert.ok(find('.flight-icon-x'));
|
||||
assert.ok(find('.hds-icon-x'));
|
||||
assert.ok(find('.hds-badge--color-critical'));
|
||||
assert.ok(find('.hds-badge--type-filled'));
|
||||
});
|
||||
@@ -42,7 +42,7 @@ module('Integration | Component | session-status', function (hooks) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.model = store.createRecord('session', { status: 'pending' });
|
||||
await render(hbs`<SessionStatus @model={{this.model}} />`);
|
||||
assert.ok(find('.flight-icon-delay'));
|
||||
assert.ok(find('.hds-icon-delay'));
|
||||
assert.ok(find('.hds-badge--color-neutral'));
|
||||
assert.ok(find('.hds-badge--type-filled'));
|
||||
});
|
||||
@@ -51,7 +51,7 @@ module('Integration | Component | session-status', function (hooks) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.model = store.createRecord('session', { status: 'any string' });
|
||||
await render(hbs`<SessionStatus @model={{this.model}} />`);
|
||||
assert.notOk(find('.flight-icon'));
|
||||
assert.notOk(find('.hds-icon'));
|
||||
assert.ok(find('.hds-badge--color-neutral'));
|
||||
assert.ok(find('.hds-badge--type-outlined'));
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | format-day-year', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders correctly when it is 1 year', async function (assert) {
|
||||
this.set('inputValue', 365);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | format-time-duration', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('It renders in human-readable duration strings given different durations in milliseconds', async function (assert) {
|
||||
// Set mock input (in milliseconds) durations
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | relative-datetime-live', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
let now;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | reverse-indexed-display-name', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders the resource name with its position in parent array reversed', async function (assert) {
|
||||
this.set('array', ['Boundary', 'is', 'cool!']);
|
||||
|
||||
@@ -11,11 +11,9 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | time-remaining', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
|
||||
test('it calculates the correct time remaining', async function (assert) {
|
||||
assert.expect(1);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it calculates the correct time remainin with less than 24 hours', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() + 1000 * 60 * 60 * 24));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
@@ -23,9 +21,29 @@ module('Integration | Helper | time-remaining', function (hooks) {
|
||||
assert.strictEqual(this.element.textContent.trim(), '23:59:59 remaining');
|
||||
});
|
||||
|
||||
test('it handles negative time remaining', async function (assert) {
|
||||
assert.expect(1);
|
||||
test('it calculates the correct time remaining with more than 24 hours', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() + 1001 * 60 * 60 * 24));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
|
||||
assert.strictEqual(
|
||||
this.element.textContent.trim(),
|
||||
'1 day, 0:01:26 remaining',
|
||||
);
|
||||
});
|
||||
|
||||
test('it calculates the correct time remaining with more than a week', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() + 1001 * 60 * 60 * 24 * 8));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
|
||||
assert.strictEqual(
|
||||
this.element.textContent.trim(),
|
||||
'1 wk, 1 day, 0:11:31 remaining',
|
||||
);
|
||||
});
|
||||
|
||||
test('it handles negative time remaining', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() - 1000 * 60 * 60 * 24));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
|
||||
@@ -22,7 +22,7 @@ const listWithOneItem = [{ key: 1, value: 'boundary' }];
|
||||
const limit = 3;
|
||||
module('Integration | Helper | truncate-list', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders correctly when there is list with more than the limit specificed', async function (assert) {
|
||||
this.set('inputValue', list);
|
||||
|
||||
@@ -30,7 +30,6 @@ module('Unit | Initializer | deprecations', function (hooks) {
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.application.boot();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Service | clock-tick', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:clock-tick');
|
||||
assert.ok(service);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { secondsToDuration } from 'core/utils/seconds-to-duration';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | seconds-to-duration', function () {
|
||||
test('it handles 0 correctly', function (assert) {
|
||||
const result = secondsToDuration(0);
|
||||
|
||||
assert.strictEqual(result.seconds, 0);
|
||||
assert.strictEqual(result.minutes, 0);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts seconds correctly', function (assert) {
|
||||
const result = secondsToDuration(45);
|
||||
|
||||
assert.strictEqual(result.seconds, 45);
|
||||
assert.strictEqual(result.minutes, 0);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts minutes correctly', function (assert) {
|
||||
const result = secondsToDuration(100);
|
||||
|
||||
assert.strictEqual(result.seconds, 40);
|
||||
assert.strictEqual(result.minutes, 1);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts hours correctly', function (assert) {
|
||||
const result = secondsToDuration(5000);
|
||||
|
||||
assert.strictEqual(result.seconds, 20);
|
||||
assert.strictEqual(result.minutes, 23);
|
||||
assert.strictEqual(result.hours, 1);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts days correctly', function (assert) {
|
||||
const result = secondsToDuration(500000);
|
||||
|
||||
assert.strictEqual(result.seconds, 20);
|
||||
assert.strictEqual(result.minutes, 53);
|
||||
assert.strictEqual(result.hours, 18);
|
||||
assert.strictEqual(result.days, 5);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts weeks correctly', function (assert) {
|
||||
const result = secondsToDuration(800000);
|
||||
|
||||
assert.strictEqual(result.seconds, 20);
|
||||
assert.strictEqual(result.minutes, 13);
|
||||
assert.strictEqual(result.hours, 6);
|
||||
assert.strictEqual(result.days, 2);
|
||||
assert.strictEqual(result.weeks, 1);
|
||||
});
|
||||
|
||||
test('it handles negative values', function (assert) {
|
||||
const result = secondsToDuration(-800000);
|
||||
|
||||
assert.strictEqual(result.seconds, 0);
|
||||
assert.strictEqual(result.minutes, 0);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,6 @@ delete: Delete
|
||||
remove: Remove
|
||||
discard: Discard
|
||||
disconnect: Disconnect
|
||||
dismiss: Dismiss
|
||||
save: Save
|
||||
submit: Submit
|
||||
manage: Manage
|
||||
@@ -16,12 +15,14 @@ confirm: Confirm
|
||||
done: Done
|
||||
ok: OK
|
||||
deny: Deny
|
||||
help: Need help?
|
||||
help: Need help
|
||||
next: Next
|
||||
back: Back
|
||||
view: View
|
||||
retry: Retry
|
||||
play: Play
|
||||
pause: Pause
|
||||
resume: Resume
|
||||
get-topic-help: Get help for this topic
|
||||
copy-to-clipboard: Copy to Clipboard
|
||||
copy-error-detail-to-clipboard: Copy error detail to clipboard
|
||||
@@ -60,3 +61,7 @@ yes: Yes
|
||||
no: No
|
||||
narrow-results: Narrow results
|
||||
overflow-options: Overflow Options
|
||||
show-errors: Show Errors
|
||||
hide-errors: Hide Errors
|
||||
edit-worker-filter: Edit Worker Filter
|
||||
add-worker-filter: Add Worker Filter
|
||||
@@ -44,6 +44,8 @@ titles:
|
||||
not-set: Not set
|
||||
year: '{numberOfYears, plural, =1 {# year} other {# years}}'
|
||||
days: '{days, plural, =1 {# day} other {# days}}'
|
||||
workers: Workers
|
||||
no-worker-filter: No worker filter added
|
||||
descriptions:
|
||||
empty-set: There are no items to display yet. You may be able to add items or try back later.
|
||||
cluster-url-initialization: To get started, please enter your cluster URL
|
||||
@@ -105,3 +107,54 @@ unsupported-controller:
|
||||
releases-page: Go to releases page
|
||||
change-cluster-url: Change cluster URL
|
||||
error: Unable to download supported version of desktop app. You’ll need to download version 1.7.1 from the releases page
|
||||
settings:
|
||||
application: Application
|
||||
cli: CLI
|
||||
cache-daemon: Cache Daemon
|
||||
client-agent:
|
||||
title: Boundary Client Agent
|
||||
description: Required to use Transparent Sessions
|
||||
client: Client
|
||||
version: Version
|
||||
color-theme: Color Theme
|
||||
provider: Provider
|
||||
hcp: HashiCorp Cloud Platform
|
||||
self-managed: Self-managed
|
||||
server:
|
||||
title: Server
|
||||
description: Boundary connects you to targets using this server managed by your organization.
|
||||
logs:
|
||||
title: Logs
|
||||
logging-level: Logging level
|
||||
log-location: Log location
|
||||
levels:
|
||||
error: Error
|
||||
warn: Warn
|
||||
info: Info
|
||||
debug: Debug
|
||||
alerts:
|
||||
cache-daemon: There may be a problem with the cache daemon
|
||||
client-agent: There may be a problem with the client agent
|
||||
worker-filter-generator:
|
||||
title: Filter generator
|
||||
description: Choose what you want to format into a filter.
|
||||
link: Learn more about formatting a worker filter.
|
||||
toggle:
|
||||
title: Show filter generator
|
||||
description: The generator will automatically generate a pre-formatted filter for you. You can copy the result to the editor above.
|
||||
input-values:
|
||||
title: Input values
|
||||
description: Next, input what you want to format.
|
||||
formatted-result:
|
||||
title: Formatted result
|
||||
description: Copy and paste this into the editor above.
|
||||
label: Generated result
|
||||
tag:
|
||||
label: Tag
|
||||
helper: Format a worker tag
|
||||
name:
|
||||
label: Name
|
||||
helper: Format a worker name
|
||||
operator:
|
||||
matches: matches (regex)
|
||||
contains: contains
|
||||
@@ -18,6 +18,8 @@ description:
|
||||
type:
|
||||
label: Type
|
||||
help: The type of the resource, to help differentiate schemas
|
||||
plugin_type:
|
||||
label: plugin_type
|
||||
login_name:
|
||||
label: Login Name
|
||||
help: The account login name
|
||||
@@ -134,6 +136,8 @@ key:
|
||||
label: Key
|
||||
value:
|
||||
label: Value
|
||||
operator:
|
||||
label: Operator
|
||||
callback_url:
|
||||
label: Callback URL
|
||||
help: The URL to which the third-party provider sends the authentication response. Paste this into the provider configuration.
|
||||
@@ -148,10 +152,10 @@ subscription_id:
|
||||
help: The subscription ID for the subscription with read access.
|
||||
access_key_id:
|
||||
label: Access Key ID
|
||||
help: The access key ID generated by AWS for the IAM user to use with this host catalog.
|
||||
help: The access key ID generated by AWS for the IAM user to use with this AWS Host Catalog.
|
||||
secret_access_key:
|
||||
label: Secret Access Key
|
||||
help: The secret access key generated by AWS for the IAM user to use with this host catalog.
|
||||
help: The secret access key generated by AWS for the IAM user to use with this AWS Host Catalog.
|
||||
secret:
|
||||
label: Client Secret Value
|
||||
help: The client secret value generated by Azure for Boundary to use for authentication.
|
||||
@@ -179,3 +183,5 @@ created:
|
||||
other_fields:
|
||||
label: Other Fields
|
||||
help: This information is provided by the server and cannot be edited in Boundary
|
||||
worker_filter:
|
||||
label: Worker Filter
|
||||
@@ -211,20 +211,54 @@ host-catalog:
|
||||
delete: Delete Host Catalog
|
||||
help:
|
||||
static: Provide a list of hosts and host sets with static addresses
|
||||
plugin: Automate discovery of target hosts and services on Azure or AWS
|
||||
plugin: Automate discovery of target hosts and services on Azure, AWS, or GCP
|
||||
role_arn: The ARN (Amazon Resource Name) used by this instance.
|
||||
disable_credential_rotation: Credential rotation is automatically disabled when you use dynamic credentials.
|
||||
types:
|
||||
static: Static
|
||||
unknown: Plugin
|
||||
aws: AWS
|
||||
azure: Azure
|
||||
plugin: Dynamic
|
||||
gcp: GCP
|
||||
credential:
|
||||
label: Credential type
|
||||
help: Select the credential type that you want to use.
|
||||
static-credential:
|
||||
edit: Static Credentials
|
||||
label: Use an access key (Static Credentials)
|
||||
help: Use an access key that you have generated in AWS to use with this AWS Host Catalog.
|
||||
dynamic-credential:
|
||||
edit: Dynamic Credentials
|
||||
label: Use Assume Role (Dynamic Credentials)
|
||||
help: Specify a role used to generate credentials to use with this AWS Host Catalog.
|
||||
form:
|
||||
aws_region:
|
||||
label: AWS Region
|
||||
help: AWS region of the hosts to be added to the catalog.
|
||||
help: AWS region of the hosts to be added to this AWS Host Catalog.
|
||||
azure_client_id:
|
||||
label: Client/Application ID
|
||||
help: The client (application) ID of an Azure service principal that Boundary will use to authenticate and discover hosts from Azure.
|
||||
worker_filter:
|
||||
help: Insert a worker filter in order to route requests to the correct worker.
|
||||
project_id:
|
||||
label: Project ID
|
||||
help: Project ID of the instances you want to add to this GCP Host Catalog.
|
||||
zone:
|
||||
label: Zone
|
||||
help: Zone of the instances you want to add to this GCP Host Catalog.
|
||||
client_email:
|
||||
label: Client Email
|
||||
help: The email address used to uniquely identify the service account.
|
||||
target_service_account_id:
|
||||
label: Target Service Account ID
|
||||
help: The unique identifier for the service account that will be impersonated. This is only used when authenticating with service account impersonation.
|
||||
private_key_id:
|
||||
label: Private Key ID
|
||||
help: The unique identifier of private key.
|
||||
private_key:
|
||||
label: Private Key
|
||||
help: The private key used to obtain a OAuth 2.0 access token. Key must be PEM encoded.
|
||||
host-set:
|
||||
title: Host Set
|
||||
title_plural: Host Sets
|
||||
@@ -241,9 +275,11 @@ host-set:
|
||||
filter:
|
||||
label: Filter
|
||||
aws:
|
||||
help: Create a filter to select resources using values such as tag name or tag value. The AWS filter format is tag:<tag-name>=<tag-value>. For example:tag:application=production
|
||||
help: 'Create a filter to select resources using values such as tag name or tag value. The AWS filter format is tag:<tag-name>=<tag-value>. For example: tag:application=production'
|
||||
azure:
|
||||
help: Create a filter to select resources using values such as tag name or tag value. The Azure filter format is:tagName eq ‘<tag-name>’ and tagValue eq ‘<tag-value>'. For example:tagName eq 'application' and tagValue eq 'production'
|
||||
gcp:
|
||||
help: 'Create a filter to select resources using values such as label name or label value. The GCP filter format is labels.<label-name>=<label-value>. For example: labels.application=production'
|
||||
preferred_endpoints:
|
||||
label: Preferred Endpoints
|
||||
help: Preferred address at which the host should be accessed when multiple options are present, in cidr:<valid IPv4/6 CIDR> or dns:<globbed name> format.
|
||||
@@ -357,6 +393,12 @@ session-recording:
|
||||
duration: '{time} duration'
|
||||
questions:
|
||||
delete: Are you sure you want to delete this recording?
|
||||
filters:
|
||||
time:
|
||||
title: Time
|
||||
last-twenty-four-hours: Last 24 hours
|
||||
last-three-days: Last 3 days
|
||||
last-seven-days: Last 7 days
|
||||
connection:
|
||||
title: Connection
|
||||
title_index: Connection {index}
|
||||
@@ -384,9 +426,9 @@ session-recording:
|
||||
messages:
|
||||
none:
|
||||
title: Channel Playback
|
||||
missing:
|
||||
title: We can't play back this channel because the file is missing
|
||||
description: This likely means the connected storage bucket is misconfigured or the file was deleted.
|
||||
error:
|
||||
title: Playback error
|
||||
description: This may mean the connected storage bucket is misconfigured, the file was deleted, or integrity validation failed.
|
||||
link: Retrieving lost session recordings
|
||||
not-supported:
|
||||
title: Playback is not supported for this channel
|
||||
@@ -439,6 +481,7 @@ storage-bucket:
|
||||
plugin-types:
|
||||
aws: Amazon S3
|
||||
minio: MinIO
|
||||
unknown: Unknown
|
||||
form:
|
||||
scope:
|
||||
label: Scope
|
||||
@@ -447,7 +490,6 @@ storage-bucket:
|
||||
help_org: This storage bucket will be available for use with targets within this org.
|
||||
provider:
|
||||
label: Provider
|
||||
help: Boundary currently only supports Amazon S3 storage.
|
||||
endpoint_url:
|
||||
label: Endpoint URL
|
||||
help: Cannot be changed later.
|
||||
@@ -459,7 +501,7 @@ storage-bucket:
|
||||
help: The base path where session recordings will be stored.
|
||||
role_arn:
|
||||
label: Role ARN
|
||||
help: The ARN (Assume Role Name) used by this instance that the worker runs on.
|
||||
help: The ARN (Amazon Resource Name) used by this instance that the worker runs on.
|
||||
role_external_id:
|
||||
label: Role external ID
|
||||
help: Used to provide access to this account if you have this configured.
|
||||
@@ -479,8 +521,7 @@ storage-bucket:
|
||||
label: Secret access key
|
||||
help: The secret access key generated by your storage provider for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
|
||||
worker_filter:
|
||||
label: Worker filter
|
||||
help: Filter for Boundary workers that have network access to your storage. Boolean expression.
|
||||
help: Filters to the worker(s) that can handle requests for this storage bucket.
|
||||
disable_credential_rotation:
|
||||
label: Disable credential rotation
|
||||
help: Disabling credential rotation could allow entities outside of Boundary to know the client secret in use.
|
||||
@@ -615,6 +656,10 @@ role:
|
||||
help: Customize which scopes are associated with this role.
|
||||
view-projects: '{selected}/{total} total projects'
|
||||
apply: Apply changes to org
|
||||
remove-org-and-projects: Remove org and projects
|
||||
remove-org-only: Remove just the org
|
||||
remove-orgs-and-projects: Remove orgs and projects
|
||||
remove-orgs-only: Remove just the orgs
|
||||
form:
|
||||
this:
|
||||
label: Add this scope
|
||||
@@ -627,6 +672,12 @@ role:
|
||||
descendants:
|
||||
label: Add all descendants
|
||||
help: Add all of the orgs and the projects in the orgs that are underneath this scope.
|
||||
remove-org:
|
||||
title: Remove this org?
|
||||
description: You're about to remove {orgDisplayName}. Do you want to remove the projects under {orgDisplayName} as well? Or do you just want to remove {orgDisplayName} from this project?
|
||||
remove-all-orgs:
|
||||
title: Remove all selected orgs?
|
||||
description: You're about to remove all the selected orgs. Do you want to remove the projects under these orgs as well? Or do you just want to remove just the orgs?
|
||||
target:
|
||||
title: Target
|
||||
title_plural: Targets
|
||||
@@ -689,7 +740,7 @@ target:
|
||||
help: The local proxy port on which to listen by default when a session is started on a client.
|
||||
sidebar:
|
||||
label: Session Recording
|
||||
help: Record SSH sessions on this target and save them to an Amazon S3 bucket.
|
||||
help: Record SSH sessions on this target and save them to a storage bucket.
|
||||
link: Session Recording settings
|
||||
questions:
|
||||
delete-host-sources:
|
||||
@@ -750,11 +801,9 @@ target:
|
||||
title: Add Injected Application Credential Sources
|
||||
description: Select injected Application credential sources to assign to this target.
|
||||
workers:
|
||||
title: Workers
|
||||
description: You can customize how your workers route traffic to this target by setting up a filter that matches specific worker tags. If your target is in a private network, we recommend setting up an egress filter to specify a worker inside the network so that Boundary can access the host. Ingress and egress filters are allowed to select the same worker.
|
||||
messages:
|
||||
none:
|
||||
title: No worker filter added
|
||||
description: You haven't added an {type} worker filter yet.
|
||||
diagram:
|
||||
client: Client
|
||||
@@ -780,13 +829,10 @@ target:
|
||||
title: Edit Ingress Worker Filter
|
||||
description: Specify workers Boundary should use to start the route to this target.
|
||||
worker-filter:
|
||||
title: Worker filter
|
||||
description: The worker filter format for this target.
|
||||
accordion-label:
|
||||
egress-workers: Egress workers
|
||||
ingress-workers: Ingress workers
|
||||
actions:
|
||||
edit-worker-filter: Edit worker filter
|
||||
credential-store:
|
||||
title: Credential Store
|
||||
title_plural: Credential Stores
|
||||
@@ -796,6 +842,13 @@ credential-store:
|
||||
title: Welcome to Credential Stores
|
||||
titles:
|
||||
new: New Credential Store
|
||||
worker-filter:
|
||||
description: A filter used to control which PKI workers can handle Vault requests, allowing the use of private Vault instances with Boundary.
|
||||
messages:
|
||||
none:
|
||||
description: You haven't added a worker filter yet.
|
||||
edit:
|
||||
description: Specify workers that have access to the credential store.
|
||||
actions:
|
||||
delete: Delete Credential Store
|
||||
types:
|
||||
@@ -833,9 +886,6 @@ credential-store:
|
||||
help: A PEM-encoded private key matching the client certificate from Client Certificate.
|
||||
client_certificate_key_hmac:
|
||||
label: Client Certificate Key HMAC
|
||||
worker_filter:
|
||||
label: Worker Filter
|
||||
help: A filter used to control which PKI workers can handle Vault requests, allowing the use of private Vault instances with Boundary.
|
||||
credential-library:
|
||||
title: Credential Library
|
||||
title_plural: Credential Libraries
|
||||
@@ -873,7 +923,7 @@ credential-library:
|
||||
help: The body of the HTTP request the library sends to Vault when requesting credentials. Only allowed when HTTP method is set to POST.
|
||||
username:
|
||||
label: Username
|
||||
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principles when making the request to Vault.
|
||||
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principals when making the request to Vault.
|
||||
key_type:
|
||||
label: Key Type
|
||||
help: Specifies the desired key type to use when generating a private key.
|
||||
@@ -969,7 +1019,6 @@ managed-group:
|
||||
help: The list of groups that this Managed Group belongs to.
|
||||
worker:
|
||||
title: Worker
|
||||
title_plural: Workers
|
||||
titles:
|
||||
new: New PKI Worker
|
||||
description: Workers are the server components that perform the session handling.
|
||||
|
||||
@@ -14,7 +14,18 @@ active-public: Public
|
||||
recording: Recording
|
||||
completed: Completed
|
||||
failed: Failed
|
||||
installed: Installed
|
||||
not-running: Not Running
|
||||
running: Running
|
||||
paused: Paused
|
||||
authenticated: Authenticated
|
||||
unauthenticated: Unauthenticated
|
||||
loading:
|
||||
title: Loading all items...
|
||||
description: All of your {resource} are still loading. Please wait while we finish loading everything on screen.
|
||||
generic-description: All of your resources are still loading. Please wait while we finish loading everything on screen.
|
||||
incomplete-loading:
|
||||
limit: Loaded the first {resultCount} results. Please narrow down your search or filters if the result you're looking for is not shown.
|
||||
refreshing:
|
||||
description: Updating cache...
|
||||
tooltip: Some items may not appear until the cache is finished updating
|
||||
@@ -10,11 +10,7 @@ module.exports = {
|
||||
extends: ['recommended', 'ember-template-lint-plugin-prettier:recommended'],
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'addon/components/rose/anonymous/index.hbs',
|
||||
'addon/components/rose/table/row/cell/index.hbs',
|
||||
'addon/components/rose/anonymous/index.hbs',
|
||||
],
|
||||
files: ['addon/components/rose/anonymous/index.hbs'],
|
||||
rules: {
|
||||
'no-yield-only': false,
|
||||
},
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
}}
|
||||
{{yield
|
||||
(hash
|
||||
radioGroup=(component 'rose/form/radio/group' disabled=this.disabled)
|
||||
checkbox=(component 'rose/form/checkbox' disabled=this.disabled)
|
||||
checkboxGroup=(component
|
||||
'rose/form/checkbox/group' disabled=this.disabled
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{yield
|
||||
(hash
|
||||
radio=(component
|
||||
'rose/form/radio/radio'
|
||||
name=@name
|
||||
selectedValue=@selectedValue
|
||||
disabled=@disabled
|
||||
changed=@changed
|
||||
inline=@inline
|
||||
)
|
||||
)
|
||||
}}
|
||||
@@ -1,43 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<div class='{{this.className}} {{if @error " error"}} {{if @inline "inline"}}'>
|
||||
|
||||
<RadioButton
|
||||
...attributes
|
||||
@radioClass='rose-form-radio-field'
|
||||
@radioId={{this.id}}
|
||||
type='radio'
|
||||
disabled={{@disabled}}
|
||||
@name={{@name}}
|
||||
@value={{@value}}
|
||||
@groupValue={{@selectedValue}}
|
||||
@disabled={{@disabled}}
|
||||
@changed={{@changed}}
|
||||
/>
|
||||
|
||||
<label class='rose-form-radio-label' for={{this.id}}>
|
||||
|
||||
{{#if @icon}}
|
||||
<Rose::Icon @name={{@icon}} @size='medium' />
|
||||
{{/if}}
|
||||
|
||||
{{@label}}
|
||||
<span class='radio-card-overlay'></span>
|
||||
{{#if @helperText}}
|
||||
<Rose::Form::HelperText
|
||||
@id='helper-text-{{this.id}}'
|
||||
@error={{@error}}
|
||||
@link={{@link}}
|
||||
@linkText={{@linkText}}
|
||||
>
|
||||
{{@helperText}}
|
||||
</Rose::Form::HelperText>
|
||||
{{/if}}
|
||||
</label>
|
||||
|
||||
{{yield}}
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { generateComponentID } from '../../../../../utilities/component-auto-id';
|
||||
|
||||
export default class RoseFormRadioRadioComponent extends Component {
|
||||
// =attributes
|
||||
className = 'rose-form-radio';
|
||||
id = generateComponentID();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<LinkTo
|
||||
class='rose-link-button
|
||||
{{if @style (concat "rose-button-" @style)}}
|
||||
{{if @iconOnly "has-icon-only"}}
|
||||
{{if @iconLeft "has-icon-left"}}
|
||||
{{if @iconRight "has-icon-right"}}'
|
||||
@route={{@route}}
|
||||
...attributes
|
||||
>
|
||||
<span class='rose-button-wrapper'>
|
||||
{{#if @iconLeft}}
|
||||
<Rose::Icon @name={{@iconLeft}} @size='medium' />
|
||||
{{/if}}
|
||||
|
||||
<span class='rose-button-body'>
|
||||
{{yield}}
|
||||
</span>
|
||||
|
||||
{{#if @iconOnly}}
|
||||
<Rose::Icon @name={{@iconOnly}} @size='medium' />
|
||||
{{/if}}
|
||||
|
||||
{{#if @iconRight}}
|
||||
<Rose::Icon @name={{@iconRight}} @size='medium' />
|
||||
{{/if}}
|
||||
</span>
|
||||
</LinkTo>
|
||||
@@ -1,33 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<article ...attributes class='rose-message'>
|
||||
|
||||
{{#if @title}}
|
||||
<header class='rose-message-header'>
|
||||
{{#if @icon}}
|
||||
<Rose::Icon @name={{@icon}} />
|
||||
{{/if}}
|
||||
|
||||
<h2 class='rose-message-title'>{{@title}}</h2>
|
||||
|
||||
{{#if @subtitle}}
|
||||
<h3 class='rose-message-subtitle'>{{@subtitle}}</h3>
|
||||
{{/if}}
|
||||
</header>
|
||||
{{/if}}
|
||||
|
||||
<section class='rose-message-body'>
|
||||
{{yield
|
||||
(hash
|
||||
description=(component
|
||||
'rose/anonymous' tagName='p' class='rose-message-description'
|
||||
)
|
||||
link=(component 'link-to')
|
||||
)
|
||||
}}
|
||||
</section>
|
||||
|
||||
</article>
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class RoseMessageComponent extends Component {}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<li class='rose-metadata-list-item {{(concat "hds-foreground-" @color)}}'>
|
||||
{{#if @icon}}
|
||||
<FlightIcon @name={{@icon}} />
|
||||
<Hds::Icon @name={{@icon}} />
|
||||
{{/if}}
|
||||
{{yield}}
|
||||
</li>
|
||||
@@ -1,41 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<section
|
||||
...attributes
|
||||
class='rose-notification is-{{@style}}{{if @dismiss " is-dismissible"}}'
|
||||
role='alert'
|
||||
aria-live={{if @dismiss 'assertive'}}
|
||||
aria-atomic='true'
|
||||
>
|
||||
|
||||
<header class='rose-notification-header'>
|
||||
<Rose::Icon
|
||||
class='rose-notification-icon'
|
||||
@name={{this.icon}}
|
||||
@size='medium'
|
||||
/>
|
||||
{{@heading}}
|
||||
</header>
|
||||
|
||||
<div class='rose-notification-body'>
|
||||
{{yield}}
|
||||
</div>
|
||||
|
||||
{{#if @dismiss}}
|
||||
<button
|
||||
type='button'
|
||||
class='rose-notification-dismiss'
|
||||
title={{@dismissText}}
|
||||
{{on 'click' @dismiss}}
|
||||
>
|
||||
<Rose::Icon @name='flight-icons/svg/x-16' @size='medium' />
|
||||
<span class='rose-notification-dismiss-text'>
|
||||
{{@dismissText}}
|
||||
</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
</section>
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default class RoseNotificationComponent extends Component {
|
||||
/**
|
||||
* Returns an icon name based on the notification style.
|
||||
* @type {string}
|
||||
*/
|
||||
@computed('style')
|
||||
get icon() {
|
||||
let icon = 'flight-icons/svg/info-16';
|
||||
|
||||
switch (this.style) {
|
||||
case 'error':
|
||||
icon = 'flight-icons/svg/x-square-16';
|
||||
break;
|
||||
case 'success':
|
||||
icon = 'flight-icons/svg/check-circle-16';
|
||||
break;
|
||||
case 'warning':
|
||||
icon = 'flight-icons/svg/alert-triangle-16';
|
||||
break;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<hr class='rose-separator' />
|
||||
@@ -1,27 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<table
|
||||
...attributes
|
||||
class='rose-table {{if @style (concat "rose-table-" @style)}}'
|
||||
>
|
||||
{{#if @caption}}
|
||||
<caption>{{@caption}}</caption>
|
||||
{{/if}}
|
||||
|
||||
{{yield
|
||||
(hash
|
||||
header=(component
|
||||
'rose/table/section' tagName='thead' classNames='rose-table-header'
|
||||
)
|
||||
body=(component
|
||||
'rose/table/section' tagName='tbody' classNames='rose-table-body'
|
||||
)
|
||||
footer=(component
|
||||
'rose/table/section' tagName='tfoot' classNames='rose-table-footer'
|
||||
)
|
||||
)
|
||||
}}
|
||||
</table>
|
||||
@@ -1,6 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{yield}}
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import layout from './index';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: 'td',
|
||||
classNameBindings: ['cellTypeStyle', 'cellShrink', 'cellAlignmentStyle'],
|
||||
cellTypeStyle: computed(function () {
|
||||
return this.tagName.match('th')
|
||||
? `rose-table-header-cell`
|
||||
: 'rose-table-cell';
|
||||
}),
|
||||
cellShrink: computed(function () {
|
||||
if (this.shrink) {
|
||||
return 'rose-table-cell-shrink';
|
||||
}
|
||||
}),
|
||||
cellAlignmentStyle: computed(function () {
|
||||
if (this.alignRight) {
|
||||
return 'align-right';
|
||||
}
|
||||
|
||||
if (this.alignCenter) {
|
||||
return 'align-center';
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<tr
|
||||
...attributes
|
||||
class='rose-table-row
|
||||
{{if @hidden (concat "rose-table-row-visually-hidden")}}
|
||||
{{if @style (concat "rose-table-row-" @style)}}'
|
||||
>
|
||||
{{yield
|
||||
(hash
|
||||
cell=(component 'rose/table/row/cell')
|
||||
headerCell=(component 'rose/table/row/cell' tagName='th')
|
||||
)
|
||||
}}
|
||||
</tr>
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import layout from './index';
|
||||
|
||||
/**
|
||||
* A table row component that uses tr table element and
|
||||
* configures a table cell element.
|
||||
*/
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
});
|
||||
Loaded 100 of 860 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user