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 |
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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
- 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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
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@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
||||
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@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
||||
uses: github/codeql-action/analyze@3096afedf9873361b2b2f65e1445b13272c83eb8 # codeql-bundle-v2.20.0
|
||||
@@ -6,7 +6,7 @@ jobs:
|
||||
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: Add missing Copyright Headers
|
||||
run: copywrite 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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
- 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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
- 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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
- 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@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
- 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
|
||||
|
||||
@@ -55,23 +55,37 @@ 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 specific
|
||||
// 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: '',
|
||||
})
|
||||
@@ -79,12 +93,22 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// 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: '',
|
||||
})
|
||||
@@ -92,24 +116,44 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// 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.',
|
||||
})
|
||||
@@ -118,32 +162,117 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
// 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,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,9 +14,11 @@ 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';
|
||||
@@ -27,6 +29,14 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
||||
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
|
||||
|
||||
@@ -53,7 +63,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (this.role_arn) {
|
||||
if (DYNAMIC_CREDENTIAL_FIELDS.some((field) => this[field])) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
@@ -85,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,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,101 +4,45 @@
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
} from '../models/host-catalog';
|
||||
|
||||
// These fields are Azure specific
|
||||
const azureFields = [
|
||||
'disable_credential_rotation',
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'subscription_id',
|
||||
'secret_id',
|
||||
'secret_value',
|
||||
];
|
||||
|
||||
// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs]
|
||||
const AWSfieldsWithCredentialType = {
|
||||
'static-credential': [
|
||||
'access_key_id',
|
||||
'secret_access_key',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'worker_filter',
|
||||
],
|
||||
'dynamic-credential': [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'worker_filter',
|
||||
],
|
||||
};
|
||||
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/host-catalog';
|
||||
|
||||
export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
serialize(snapshot) {
|
||||
const { compositeType, credentialType } = snapshot.record;
|
||||
const { credentialType } = snapshot.record;
|
||||
const serialized = super.serialize(...arguments);
|
||||
|
||||
// This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS
|
||||
// Ideally we should refactor the model to include the compositeType using `for` attribute
|
||||
if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) {
|
||||
delete serialized.worker_filter;
|
||||
}
|
||||
|
||||
// By default, disable credential rotation for dynamic credentials
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
serialized.attributes.disable_credential_rotation = true;
|
||||
}
|
||||
|
||||
switch (compositeType) {
|
||||
case 'static':
|
||||
return this.serializeStatic(...arguments);
|
||||
default:
|
||||
return serialized;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { compositeType, credentialType, isPlugin } = snapshot.record;
|
||||
const { compositeType, credentialType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
const isSamePluginName = options?.for?.name?.includes(compositeType);
|
||||
|
||||
const fields =
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE
|
||||
? azureFields
|
||||
: AWSfieldsWithCredentialType[credentialType];
|
||||
|
||||
// Delete any nested attribute fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedAttribute && json.attributes) {
|
||||
if (!fields.includes(key)) {
|
||||
delete json.attributes[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any secret fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedSecret && json.secrets) {
|
||||
if (!fields.includes(key)) {
|
||||
// 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 (json['secrets'] && Object.keys(json.secrets).length === 0) {
|
||||
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;
|
||||
delete serialized.worker_filter;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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'],
|
||||
@@ -39,13 +51,17 @@ export default factory.extend({
|
||||
},
|
||||
|
||||
worker_filter: function () {
|
||||
if (this.type === 'plugin' && this.plugin?.name === 'aws') {
|
||||
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],
|
||||
@@ -56,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"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": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
@@ -39,15 +39,15 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@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": "^5.12.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
@@ -68,13 +68,10 @@
|
||||
"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",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": ">4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -9,6 +9,7 @@ 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) {
|
||||
@@ -32,6 +33,53 @@ 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 gcp plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
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);
|
||||
});
|
||||
@@ -45,14 +93,9 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
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',
|
||||
@@ -71,6 +114,10 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
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',
|
||||
@@ -80,7 +127,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws dynamic plugin as expected', async function (assert) {
|
||||
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,
|
||||
@@ -89,7 +136,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
// these are static AWS fields and should be excluded
|
||||
region: 'west',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
@@ -124,7 +171,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws static plugin as expected, ignoring azure fields', async function (assert) {
|
||||
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,
|
||||
@@ -143,6 +190,8 @@ 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',
|
||||
@@ -152,6 +201,10 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
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',
|
||||
@@ -161,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',
|
||||
@@ -178,6 +231,8 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
project_id: 'project',
|
||||
zone: 'zone',
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'Azure',
|
||||
@@ -225,6 +280,10 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
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);
|
||||
@@ -264,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;
|
||||
@@ -293,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: {},
|
||||
},
|
||||
@@ -351,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: {},
|
||||
},
|
||||
@@ -411,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);
|
||||
});
|
||||
});
|
||||
@@ -58,6 +58,7 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
*/
|
||||
async validateToken(token, tokenID) {
|
||||
const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID);
|
||||
|
||||
// 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(
|
||||
@@ -66,6 +67,12 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
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();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { reject } from 'rsvp';
|
||||
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:
|
||||
@@ -97,13 +97,19 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
});
|
||||
// Fetch the endpoint and get the response JSON
|
||||
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 {
|
||||
|
||||
@@ -21,30 +21,31 @@
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"ember-auto-import": "2.8.1",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"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": "^3.3.1",
|
||||
"@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": "^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",
|
||||
@@ -62,8 +63,8 @@
|
||||
"loader.js": "^4.7.0",
|
||||
"pretender": "^3.4.3",
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.2.1",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -65,3 +65,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility classes
|
||||
.container-inputs__margin-bottom {
|
||||
input:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/route-action';
|
||||
@@ -34,16 +34,17 @@
|
||||
"@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": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-sass": "^11.0.1",
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-intl": "^7.0.5",
|
||||
"ember-intl": "^7.0.7",
|
||||
"ember-loading": "^2.0.0",
|
||||
"ember-truth-helpers": "^3.0.0",
|
||||
"filesize": "^10.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -51,15 +52,15 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@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": "^5.12.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
@@ -81,16 +82,12 @@
|
||||
"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": "^18.0.0",
|
||||
"sinon": "^19.0.2",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": "^4.0.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
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';
|
||||
|
||||
@@ -13,61 +13,65 @@ module('Integration | Component | error/message', function (hooks) {
|
||||
setupRenderingTest(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');
|
||||
});
|
||||
});
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,7 +8,6 @@ delete: Delete
|
||||
remove: Remove
|
||||
discard: Discard
|
||||
disconnect: Disconnect
|
||||
dismiss: Dismiss
|
||||
save: Save
|
||||
submit: Submit
|
||||
manage: Manage
|
||||
@@ -16,7 +15,7 @@ confirm: Confirm
|
||||
done: Done
|
||||
ok: OK
|
||||
deny: Deny
|
||||
help: Need help?
|
||||
help: Need help
|
||||
next: Next
|
||||
back: Back
|
||||
view: View
|
||||
@@ -64,3 +63,5 @@ 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
|
||||
@@ -133,3 +135,26 @@ settings:
|
||||
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,7 +211,7 @@ 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:
|
||||
@@ -220,6 +220,7 @@ host-catalog:
|
||||
aws: AWS
|
||||
azure: Azure
|
||||
plugin: Dynamic
|
||||
gcp: GCP
|
||||
credential:
|
||||
label: Credential type
|
||||
help: Select the credential type that you want to use.
|
||||
@@ -234,13 +235,30 @@ 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:
|
||||
label: 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
|
||||
@@ -257,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.
|
||||
@@ -501,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.
|
||||
@@ -782,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
|
||||
@@ -812,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
|
||||
@@ -828,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:
|
||||
@@ -865,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
|
||||
@@ -1001,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.
|
||||
|
||||
@@ -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: '',
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{yield (hash row=(component 'rose/table/row'))}}
|
||||
@@ -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 section component that configures a row component.
|
||||
* Section can be: thead, tbody, or tfoot table elements.
|
||||
*/
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: '',
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export default function mapTypeToColor(flashMsgType) {
|
||||
// Maps the flash message type parameter to the correct hds color.
|
||||
switch (flashMsgType) {
|
||||
case 'error':
|
||||
return 'critical';
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'neutral';
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { v1 } from 'ember-uuid';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* A function that generates a component ID, based on UUID.
|
||||
* @return {string}
|
||||
*/
|
||||
export function generateComponentID() {
|
||||
return `component-${v1()}`;
|
||||
return `component-${uuidv4()}`;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/form/radio/card';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/form/radio/group';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/form/radio/radio';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/link-button';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/message';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/notification';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/table/row';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/table/row/cell';
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/table/section';
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/table';
|
||||
export { default } from 'rose/helpers/map-type-to-color';
|
||||
@@ -42,10 +42,5 @@ $xxxxs: sizing.rems(xxxxs); // 1
|
||||
$body-padding: $l;
|
||||
|
||||
padding: $body-padding;
|
||||
|
||||
> .rose-separator {
|
||||
margin-left: -$body-padding;
|
||||
margin-right: -$body-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,21 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// This list is sorted in ascending order to improve readability
|
||||
|
||||
@import 'button';
|
||||
@import 'card';
|
||||
@import 'cards';
|
||||
@import 'code-editor';
|
||||
@import 'dialog';
|
||||
@import 'dropdown';
|
||||
@import 'form';
|
||||
@import 'frame';
|
||||
@import 'header';
|
||||
@import 'layout';
|
||||
@import 'icon';
|
||||
@import 'layout';
|
||||
@import 'list';
|
||||
@import 'notification';
|
||||
@import 'card';
|
||||
@import 'cards';
|
||||
@import 'page-header';
|
||||
@import 'separator';
|
||||
@import 'nav';
|
||||
@import 'table';
|
||||
@import 'message';
|
||||
@import 'link-button';
|
||||
@import 'toolbar';
|
||||
@import 'code-editor';
|
||||
@import 'metadata-list';
|
||||
@import 'nav';
|
||||
@import 'page-header';
|
||||
@import 'toolbar';
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// Link button uses rose-button styling
|
||||
.rose-link-button {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@use '../utilities/type';
|
||||
@use '../variables/sizing';
|
||||
@use '../variables/media';
|
||||
|
||||
.rose-message {
|
||||
--message-color: var(--ui-gray-starker-1);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--message-color);
|
||||
width: sizing.rems(l) * 15; // 16 rems/320 px
|
||||
margin-bottom: sizing.rems(l);
|
||||
|
||||
@media (width <= (media.width(small))) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Icons always need tweaking
|
||||
.rose-icon {
|
||||
--size: #{sizing.rems(m)};
|
||||
|
||||
> svg {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-message-header {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'icon title'
|
||||
'icon subtitle';
|
||||
grid-auto-columns: auto 1fr;
|
||||
margin-bottom: sizing.rems(m);
|
||||
|
||||
.rose-icon {
|
||||
// targeting 40px
|
||||
--size: #{sizing.rems(xl) + sizing.rems(xxs)};
|
||||
|
||||
grid-area: icon;
|
||||
margin-right: sizing.rems(s);
|
||||
}
|
||||
|
||||
.rose-message-title {
|
||||
@include type.type(l, semibold);
|
||||
|
||||
grid-area: title;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rose-message-subtitle {
|
||||
@include type.type(s, normal);
|
||||
|
||||
grid-area: subtitle;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-message-body {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'description description'
|
||||
'left-link right-link';
|
||||
|
||||
.rose-message-description {
|
||||
grid-area: description;
|
||||
margin-bottom: sizing.rems(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@use '../variables/sizing';
|
||||
@use '../utilities/type';
|
||||
|
||||
.rose-notification {
|
||||
$s: sizing.rems(s); // 12
|
||||
$xs: sizing.rems(xs); // 8
|
||||
$xxs: sizing.rems(xxs); // 4
|
||||
$xxxs: sizing.rems(xxxs); // 2
|
||||
$xxxxs: sizing.rems(xxxxs); // 1
|
||||
|
||||
position: relative;
|
||||
padding: sizing.rems(m);
|
||||
margin-bottom: sizing.rems(m);
|
||||
border: sizing.rems(xxxxs) solid;
|
||||
border-radius: $xxxs;
|
||||
box-shadow:
|
||||
0 $xs $s rgba(var(--stark-components), var(--opacity-3)),
|
||||
0 $xxs $xxs rgba(var(--stark-components), var(--opacity-4));
|
||||
|
||||
.rose-notification-icon {
|
||||
position: relative;
|
||||
top: $xxxxs;
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
.rose-notification-header {
|
||||
color: var(--header-color);
|
||||
@include type.type(m, semibold);
|
||||
}
|
||||
|
||||
.rose-notification-body {
|
||||
@include type.type(s);
|
||||
|
||||
// hack alert, because the icons are still hard to work with
|
||||
margin-left: sizing.rems(m) + $xxs;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.rose-notification-dismiss {
|
||||
position: absolute;
|
||||
color: currentcolor;
|
||||
background-color: transparent;
|
||||
border-radius: $xxxs;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
top: sizing.rems(s);
|
||||
right: sizing.rems(s);
|
||||
|
||||
.rose-icon {
|
||||
display: block;
|
||||
padding: $xxs;
|
||||
}
|
||||
|
||||
.rose-notification-dismiss-text {
|
||||
position: absolute;
|
||||
top: -1000px;
|
||||
left: -1000px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--stark-components), var(--opacity-3));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(var(--stark-components), var(--opacity-2));
|
||||
}
|
||||
}
|
||||
|
||||
--icon-color: var(--info);
|
||||
--header-color: var(--info-starker-1);
|
||||
--border-color: var(--info-subtler-1);
|
||||
--background-color: var(--info-subtler-3);
|
||||
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--background-color);
|
||||
|
||||
&.is-error {
|
||||
--icon-color: var(--failure);
|
||||
--header-color: var(--failure);
|
||||
--border-color: var(--failure-subtler-1);
|
||||
--background-color: var(--failure-subtler-2);
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
--icon-color: var(--success);
|
||||
--header-color: var(--success-starker-1);
|
||||
--border-color: var(--success-subtler-1);
|
||||
--background-color: var(--success-subtler-2);
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
--icon-color: var(--warning);
|
||||
--header-color: var(--warning-starker-1);
|
||||
--border-color: var(--warning-subtler-1);
|
||||
--background-color: var(--warning-subtler-2);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@use '../variables/sizing';
|
||||
|
||||
.rose-separator {
|
||||
background-color: var(--ui-gray-subtler-3);
|
||||
border: 0;
|
||||
height: sizing.rems(xxxxs);
|
||||
margin: sizing.rems(l) 0;
|
||||
}
|
||||
@@ -9,5 +9,4 @@
|
||||
@import 'input';
|
||||
@import 'checkbox';
|
||||
@import 'select';
|
||||
@import 'radio';
|
||||
@import 'fieldset';
|
||||
@@ -1,184 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use '../../variables/typography';
|
||||
|
||||
$radio-svg: (
|
||||
'default':
|
||||
"<svg viewBox='0 0 24 24' fill='%23aaaaaa' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'hover':
|
||||
"<svg viewBox='0 0 24 24' fill='%236f7682' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'focus':
|
||||
"<svg viewBox='0 0 24 24' fill='%231563ff' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'checked':
|
||||
"<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' fill='%231563ff'/></svg>",
|
||||
'disabled':
|
||||
"<svg viewBox='0 0 24 24' fill='%23bac1cc' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'disabled-checked':
|
||||
"<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' fill='%23bac1cc'/></svg>",
|
||||
'error':
|
||||
"<svg viewBox='0 0 24 24' fill='%23c73445' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' /></svg>",
|
||||
'error-checked':
|
||||
"<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z' fill='%23c73445'/></svg>",
|
||||
);
|
||||
$data-svg-prefix: 'data:image/svg+xml;utf-8,';
|
||||
|
||||
.rose-form-radio {
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
|
||||
.rose-form-radio-label {
|
||||
margin-right: sizing.rems(s);
|
||||
}
|
||||
}
|
||||
@include type.type(m);
|
||||
|
||||
position: relative;
|
||||
margin-bottom: sizing.rems(xs);
|
||||
color: var(--ui-gray);
|
||||
|
||||
.rose-form-radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.rose-form-helper-text {
|
||||
font-weight: normal;
|
||||
margin-left: sizing.rems(xxs);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include type.type(s, semibold);
|
||||
|
||||
color: currentcolor;
|
||||
padding-left: sizing.rems(l);
|
||||
padding-top: sizing.rems(xxxs);
|
||||
|
||||
.rose-icon {
|
||||
margin-right: sizing.rems(xxs);
|
||||
margin-left: sizing.rems(xxs);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: sizing.rems(xs);
|
||||
left: 0;
|
||||
display: block;
|
||||
width: sizing.rems(m);
|
||||
height: sizing.rems(m);
|
||||
pointer-events: none;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -#{sizing.rems(xxxxs)};
|
||||
left: -#{sizing.rems(xxxs)};
|
||||
display: block;
|
||||
width: sizing.rems(l);
|
||||
height: sizing.rems(l);
|
||||
transform: scale(2);
|
||||
pointer-events: none;
|
||||
content: ' ';
|
||||
background: no-repeat 50%/50% 50%;
|
||||
background-image: url($data-svg-prefix + map-get($radio-svg, 'default'));
|
||||
}
|
||||
}
|
||||
|
||||
.rose-form-radio-field {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: sizing.rems(m);
|
||||
height: sizing.rems(m);
|
||||
opacity: 0;
|
||||
|
||||
&:not(:disabled) {
|
||||
&:hover {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'hover'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'focus'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'checked'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'disabled'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'disabled-checked'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--failure);
|
||||
|
||||
.rose-form-radio-field {
|
||||
&:not(:disabled, :focus) {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'error'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .rose-form-radio-label {
|
||||
&::after {
|
||||
background-image: url($data-svg-prefix + map-get(
|
||||
$radio-svg,
|
||||
'error-checked'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@import 'table';
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-table {
|
||||
@include type.type(s, normal);
|
||||
|
||||
width: 100%;
|
||||
|
||||
.rose-table-row {
|
||||
border-bottom: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3);
|
||||
text-align: left;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// This styles are add to stop the input to apply extra margin (default on forms)
|
||||
.rose-form-input,
|
||||
.rose-form-input-field,
|
||||
.hds-form-select,
|
||||
[class^='hds-form-field'],
|
||||
[class^='hds-form-field']:not(:first-child) {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Fix margin issue with textarea elements inside tables
|
||||
textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rose-table-cell,
|
||||
.rose-table-header-cell {
|
||||
vertical-align: text-top;
|
||||
|
||||
&.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-header-cell {
|
||||
@include type.type(s, semibold);
|
||||
}
|
||||
|
||||
.rose-table-header .rose-table-header-cell {
|
||||
color: var(--ui-gray);
|
||||
}
|
||||
|
||||
.rose-table-header,
|
||||
.rose-table-footer {
|
||||
.rose-table-cell,
|
||||
.rose-table-header-cell {
|
||||
padding: sizing.rems(s);
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-body {
|
||||
.rose-table-cell,
|
||||
.rose-table-header-cell {
|
||||
padding: sizing.rems(m) sizing.rems(s);
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-cell-shrink {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[class*='rose-button'] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.rose-table-condensed {
|
||||
.rose-table-cell,
|
||||
.rose-table-header-cell {
|
||||
@include type.type(s);
|
||||
|
||||
$line-height: math.div(36, 14);
|
||||
|
||||
line-height: $line-height;
|
||||
padding: sizing.rems(xxs) + sizing.rems(xxxxs) sizing.rems(s);
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-header {
|
||||
border-bottom: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3);
|
||||
|
||||
.rose-table-header-cell {
|
||||
$line-height: math.div(16, 14);
|
||||
|
||||
line-height: $line-height;
|
||||
padding: sizing.rems(xs) sizing.rems(s);
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-row-visually-hidden {
|
||||
position: absolute;
|
||||
top: -9999rem;
|
||||
left: -9999rem;
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,8 @@
|
||||
--ui-gray-starker-3: #{color(ui-gray, 800)};
|
||||
--ui-gray-starker-4: #{color(ui-gray, 900)};
|
||||
--ui-header-subtler: #{color(ui-cool-gray, 700)};
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
// Dark Mode
|
||||
|
||||
@@ -27,13 +27,13 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Due to a limitation in how ember treats nested addons (see https://github.com/ember-cli/ember-cli/issues/4475)
|
||||
* this is neeeded to reach down into @hashicorp/ember-flight-icons' contentFor hook to run the logic
|
||||
* this is neeeded to reach down into @hashicorp/design-system-components' contentFor hook to run the logic
|
||||
* that injects the sprite into the DOM
|
||||
*/
|
||||
contentFor(type, config) {
|
||||
return this.findOwnAddonByName('@hashicorp/design-system-components')
|
||||
.findOwnAddonByName('@hashicorp/ember-flight-icons')
|
||||
.contentFor(type, config);
|
||||
return this.findOwnAddonByName(
|
||||
'@hashicorp/design-system-components',
|
||||
).contentFor(type, config);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -45,8 +45,6 @@ module.exports = {
|
||||
'../../node_modules/@hashicorp/design-system-tokens/dist/products/css';
|
||||
const hdsPath =
|
||||
'../../node_modules/@hashicorp/design-system-components/dist/styles';
|
||||
const iconsPath =
|
||||
'../../node_modules/@hashicorp/ember-flight-icons/dist/styles';
|
||||
|
||||
// Setup default sassOptions on the running application
|
||||
app.options.sassOptions = app.options.sassOptions || {};
|
||||
@@ -56,7 +54,6 @@ module.exports = {
|
||||
// Include the addon styles
|
||||
app.options.sassOptions.includePaths.push(tokensPath);
|
||||
app.options.sassOptions.includePaths.push(hdsPath);
|
||||
app.options.sassOptions.includePaths.push(iconsPath);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+12
-16
@@ -30,20 +30,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@hashicorp/design-system-components": "^4.4.1",
|
||||
"@hashicorp/design-system-components": "^4.13.0",
|
||||
"codemirror": "5.65.7",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-auto-import": "^2.8.1",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-clipboard": "^1.0.0",
|
||||
"ember-cli-clipboard": "^1.2.1",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-sass": "^11.0.1",
|
||||
"ember-composable-helpers": "^5.0.0",
|
||||
"ember-focus-trap": "^1.0.1",
|
||||
"ember-named-blocks-polyfill": "^0.2.5",
|
||||
"ember-radio-button": "github:yapplabs/ember-radio-button.git#b2ab65f",
|
||||
"ember-uuid": "^2.1.0",
|
||||
"jsonlint": "^1.6.3",
|
||||
"sass": "^1.69.5"
|
||||
"sass": "^1.69.5",
|
||||
"uuid": "^11.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
@@ -51,16 +50,16 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@ember/test-helpers": "^4.0.4",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-ember-modules-api-polyfill": "^2.12.0",
|
||||
"babel-plugin-htmlbars-inline-precompile": "3",
|
||||
"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": "^5.12.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
@@ -88,19 +87,16 @@
|
||||
"loader.js": "^4.7.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.2.1",
|
||||
"react": "^16",
|
||||
"react-dom": "^16",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-prettier-scss": "^0.0.1",
|
||||
"stylelint-config-standard-scss": "^11.0.0",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
},
|
||||
|
||||
@@ -72,19 +72,12 @@ module('Integration | Component | rose/form', function (hooks) {
|
||||
</Hds::Form::Select::Field>
|
||||
|
||||
<form.checkbox @label="Checkbox" @name="checkbox-field" />
|
||||
<form.radioGroup @name="radio-group-field" @selectedValue="green" as |radioGroup|>
|
||||
<radioGroup.radio
|
||||
@label="Red"
|
||||
@value="red"
|
||||
/>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
`);
|
||||
assert.ok(find('[name="input-field"]'));
|
||||
assert.ok(find('[name="textarea-field"]'));
|
||||
assert.ok(find('[name="select-field"]'));
|
||||
assert.ok(find('[name="checkbox-field"]'));
|
||||
assert.ok(find('[name="radio-group-field"]'));
|
||||
});
|
||||
|
||||
test('it supports an editability toggle when @showEditToggle is true', async function (assert) {
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/form/radio/group', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`
|
||||
<Rose::Form::Radio::Group @name="bird" as |radioGroup|>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="Pegion"
|
||||
@value="pegion"
|
||||
/>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="flamingo"
|
||||
@value="flamingo"
|
||||
/>
|
||||
</Rose::Form::Radio::Group>
|
||||
`);
|
||||
|
||||
assert.strictEqual(findAll('input').length, 2);
|
||||
assert.strictEqual(findAll('input')[0].name, 'bird');
|
||||
assert.strictEqual(findAll('input')[1].name, 'bird');
|
||||
});
|
||||
|
||||
test('it renders with @selectedValue value selected', async function (assert) {
|
||||
await render(hbs`
|
||||
<Rose::Form::Radio::Group @name="bird" @selectedValue="flamingo" as |radioGroup|>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="Pegion"
|
||||
@value="pegion"
|
||||
/>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="flamingo"
|
||||
@value="flamingo"
|
||||
/>
|
||||
</Rose::Form::Radio::Group>
|
||||
`);
|
||||
|
||||
assert.false(findAll('[type="radio"]')[0].checked);
|
||||
assert.true(findAll('[type="radio"]')[1].checked);
|
||||
|
||||
await click(findAll('[type="radio"]')[0]);
|
||||
|
||||
assert.true(findAll('[type="radio"]')[0].checked);
|
||||
assert.false(findAll('[type="radio"]')[1].checked);
|
||||
});
|
||||
|
||||
test('it reflects active radiofield value in @selectedValue and triggers a @changed function', async function (assert) {
|
||||
assert.expect(5);
|
||||
this.changed = (value) => {
|
||||
this.selectedValue = value;
|
||||
assert.ok(value);
|
||||
};
|
||||
await render(hbs`
|
||||
<Rose::Form::Radio::Group
|
||||
@name="bird"
|
||||
@selectedValue={{this.selectedValue}}
|
||||
@changed={{this.changed}}
|
||||
as |radioGroup|>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="Pegion"
|
||||
@value="pegion"
|
||||
/>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="flamingo"
|
||||
@value="flamingo"
|
||||
/>
|
||||
</Rose::Form::Radio::Group>
|
||||
`);
|
||||
|
||||
assert.notOk(this.selectedValue);
|
||||
|
||||
await click(findAll('[type="radio"]')[0]);
|
||||
assert.strictEqual(this.selectedValue, 'pegion');
|
||||
|
||||
await click(findAll('[type="radio"]')[1]);
|
||||
assert.strictEqual(this.selectedValue, 'flamingo');
|
||||
});
|
||||
|
||||
test('it uses @selectedValue value to set active radiofield', async function (assert) {
|
||||
this.set('selectedValue', 'cat');
|
||||
await render(hbs`
|
||||
<Rose::Form::Radio::Group @name="bird" @selectedValue={{this.selectedValue}} as |radioGroup|>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="Pegion"
|
||||
@value="pegion"
|
||||
/>
|
||||
<radioGroup.radio
|
||||
@name="bird"
|
||||
@label="flamingo"
|
||||
@value="flamingo"
|
||||
/>
|
||||
</Rose::Form::Radio::Group>
|
||||
`);
|
||||
|
||||
assert.false(findAll('[type="radio"]')[0].checked);
|
||||
assert.false(findAll('[type="radio"]')[1].checked);
|
||||
|
||||
this.set('selectedValue', 'flamingo');
|
||||
assert.false(findAll('[type="radio"]')[0].checked);
|
||||
assert.true(findAll('[type="radio"]')[1].checked);
|
||||
|
||||
this.set('selectedValue', 'pegion');
|
||||
assert.true(findAll('[type="radio"]')[0].checked);
|
||||
assert.false(findAll('[type="radio"]')[1].checked);
|
||||
|
||||
this.set('selectedValue', 'dog');
|
||||
assert.false(findAll('[type="radio"]')[0].checked);
|
||||
assert.false(findAll('[type="radio"]')[1].checked);
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/form/radio/radio', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio />`);
|
||||
assert.ok(await find('input'));
|
||||
assert.strictEqual(await find('input').type, 'radio');
|
||||
});
|
||||
|
||||
test('it renders with label', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio @label="Label"/>`);
|
||||
assert.strictEqual(await find('label').textContent.trim(), 'Label');
|
||||
});
|
||||
|
||||
test('it supports an icon', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::Form::Radio::Radio @icon="flight-icons/svg/user-circle-16" />`,
|
||||
);
|
||||
assert.ok(find('svg'));
|
||||
});
|
||||
|
||||
test('it is checked when @value and @selectedValue values match', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::Form::Radio::Radio @value="tree" @selectedValue="tree"/>`,
|
||||
);
|
||||
assert.true(await find('input').checked);
|
||||
});
|
||||
|
||||
test('it is not checked when @value and @selectedValue values are different', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio @value="tree"/>`);
|
||||
assert.false(await find('input').checked);
|
||||
|
||||
await render(hbs`<Rose::Form::Radio::Radio @selectedValue="tree"/>`);
|
||||
assert.false(await find('input').checked);
|
||||
|
||||
await render(
|
||||
hbs`<Rose::Form::Radio::Radio @value="root" @selectedValue="tree"/>`,
|
||||
);
|
||||
assert.false(await find('input').checked);
|
||||
});
|
||||
|
||||
test('it is not disabled by default', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio />`);
|
||||
assert.false(await find('input').disabled);
|
||||
});
|
||||
|
||||
test('it is not in error state by default', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio />`);
|
||||
assert.notOk(await find('.error'));
|
||||
});
|
||||
|
||||
test('it is disabled when @disabled={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio @disabled={{true}}/>`);
|
||||
assert.true(await find('input').disabled);
|
||||
});
|
||||
|
||||
test('it is in error state when @error={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio @error={{true}} />`);
|
||||
assert.ok(await find('.error'));
|
||||
});
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/link-button', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<Rose::LinkButton @route="index" />`);
|
||||
assert.ok(find('a'));
|
||||
assert.notOk(find('.rose-button'));
|
||||
assert.ok(find('.rose-link-button'));
|
||||
});
|
||||
|
||||
test('it renders with attributes', async function (assert) {
|
||||
await render(hbs`<Rose::LinkButton @route="index" id="link-button"/>`);
|
||||
assert.ok(find('#link-button'));
|
||||
});
|
||||
|
||||
test('it renders with content', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::LinkButton @route="index">Link button content</Rose::LinkButton>`,
|
||||
);
|
||||
assert.ok(
|
||||
find('.rose-link-button').textContent.trim(),
|
||||
'Link button content',
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders with rose button styles', async function (assert) {
|
||||
await render(hbs`<Rose::LinkButton @route="index" @style="sample"/>`);
|
||||
assert.ok(find(`.rose-button-sample`));
|
||||
});
|
||||
|
||||
test('it supports left icon with @iconLeft', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::LinkButton @route="index" @iconLeft="flight-icons/svg/chevron-left-16" />`,
|
||||
);
|
||||
assert.ok(find('.rose-icon'));
|
||||
assert.ok(find('.has-icon-left'));
|
||||
});
|
||||
|
||||
test('it supports left icon with @iconRight', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::LinkButton @route="index" @iconRight="flight-icons/svg/entry-point-16" />`,
|
||||
);
|
||||
assert.ok(find('.rose-icon'));
|
||||
assert.ok(find('.has-icon-right'));
|
||||
});
|
||||
|
||||
test('it supports only icon with @iconOnly', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::LinkButton @route="index" @iconOnly="flight-icons/svg/help-16" />`,
|
||||
);
|
||||
assert.ok(find('.rose-icon'));
|
||||
assert.ok(find('.has-icon-only'));
|
||||
});
|
||||
});
|
||||
Loaded 100 of 683 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user