Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e31adb9db | ||
|
|
f0feb9e563 | ||
|
|
44624e3820 | ||
|
|
225c94b853 | ||
|
|
bd1b8542eb | ||
|
|
c92e93ab1c | ||
|
|
1358727832 | ||
|
|
9bc84a2c59 | ||
|
|
7f00a83b19 | ||
|
|
19426cfd51 | ||
|
|
30c05eb834 | ||
|
|
f1d334b848 | ||
|
|
961f497b6b | ||
|
|
89f33f3714 | ||
|
|
4bad52f43a | ||
|
|
fd6cd3ddf7 | ||
|
|
adf7f8603b | ||
|
|
2a2e4350ee | ||
|
|
f563a4ef45 | ||
|
|
37b6ef6d3f | ||
|
|
96ff0ddf27 | ||
|
|
746862a897 | ||
|
|
5d6e65f206 | ||
|
|
6f9a464302 | ||
|
|
abf7866292 | ||
|
|
10c98c66e7 | ||
|
|
c1f7fe2ddd | ||
|
|
7f4586ed9e | ||
|
|
0b6049c8b0 | ||
|
|
2d347171a9 | ||
|
|
a085d4d4f7 | ||
|
|
7e2f3e3df3 | ||
|
|
3899daea7c | ||
|
|
3d1ee732b4 | ||
|
|
899d7beee0 | ||
|
|
4c7f3fad75 | ||
|
|
b8eb2c3c9b | ||
|
|
f5d612c8b5 | ||
|
|
4020f2d4e2 | ||
|
|
5794cfc7b2 | ||
|
|
1284a16978 | ||
|
|
f255a470cd | ||
|
|
f738e71657 | ||
|
|
beeee851ac | ||
|
|
eb365f0d39 | ||
|
|
57e614e8db | ||
|
|
738fa1f15f | ||
|
|
4c959ab297 | ||
|
|
e685295d04 | ||
|
|
204bd10e04 | ||
|
|
5a302b0bcc | ||
|
|
daa23f66c4 | ||
|
|
b06c396086 | ||
|
|
5555603a5c | ||
|
|
7c3b6d6d9e | ||
|
|
ce87d7728c | ||
|
|
e4da851172 | ||
|
|
e3433ec1f7 | ||
|
|
96f30d1b31 | ||
|
|
f4a48467e3 | ||
|
|
bada405511 | ||
|
|
96d7c055b0 | ||
|
|
a747614a74 | ||
|
|
84856407ee | ||
|
|
9bfd624713 | ||
|
|
b2a6e4d47f | ||
|
|
308240be89 | ||
|
|
c12edc7f73 | ||
|
|
44bd5ee1c6 | ||
|
|
8f3cccce32 | ||
|
|
1ef7d9b5a7 | ||
|
|
c445f95aa4 | ||
|
|
96db738f4b | ||
|
|
6181df4c69 | ||
|
|
376787fda1 | ||
|
|
80ffa856bd | ||
|
|
eb9fe573b8 | ||
|
|
e222d5b909 | ||
|
|
1c774aa783 | ||
|
|
04b51da60f | ||
|
|
28ddec6898 | ||
|
|
d987f844de | ||
|
|
b0d26fe1e6 | ||
|
|
e96131a933 | ||
|
|
3d58745bf1 | ||
|
|
9560148253 | ||
|
|
ec00efb656 | ||
|
|
ba34307918 | ||
|
|
28d8afae2e | ||
|
|
2e507be454 | ||
|
|
dbf3617224 | ||
|
|
200a7944e6 | ||
|
|
8dafc2b9a0 | ||
|
|
b7559c1f4e | ||
|
|
dd8e1864db | ||
|
|
d85a84980f | ||
|
|
4295966b7f | ||
|
|
a87c079ade | ||
|
|
b3604e3e40 | ||
|
|
6be8130329 | ||
|
|
ca3972c04f | ||
|
|
4d98729cde | ||
|
|
185af6f00e | ||
|
|
c5353d6e5e | ||
|
|
20a05dc96a | ||
|
|
30028b5ba0 | ||
|
|
3bafb708ea | ||
|
|
24d86a230a | ||
|
|
620f822504 | ||
|
|
93a57f26ee | ||
|
|
40ec4493ea | ||
|
|
de83d59bde | ||
|
|
a937c5e8de | ||
|
|
b820bb19d7 | ||
|
|
36db2d3f1d | ||
|
|
5786b86521 | ||
|
|
c6a5f93b44 | ||
|
|
8814e2eacb | ||
|
|
9b1e4a8291 | ||
|
|
01c2468f86 | ||
|
|
af705718ff | ||
|
|
e22ef12529 | ||
|
|
62510d9243 | ||
|
|
0fd25a6bb4 | ||
|
|
a268a7b75e | ||
|
|
3dded0a7bc | ||
|
|
ab3140be0a | ||
|
|
0fcd58c080 | ||
|
|
52f0d9f9e8 | ||
|
|
6585fa8e41 | ||
|
|
bef9b3b276 | ||
|
|
af411b854a | ||
|
|
a697dba5b9 | ||
|
|
1203081ec7 | ||
|
|
2db63ae2f8 | ||
|
|
9dd1617b57 | ||
|
|
834058ff45 | ||
|
|
0f41a0a576 | ||
|
|
1025cbfb87 | ||
|
|
ca3c1c7623 | ||
|
|
69d127a89e | ||
|
|
b4d9cd5bb7 | ||
|
|
4b1327db24 | ||
|
|
2f2ceed24e | ||
|
|
de0f20809d | ||
|
|
b1f5857cac | ||
|
|
ddbd1ca163 | ||
|
|
777e40fff8 | ||
|
|
68460331f5 | ||
|
|
32e9fe8b6a | ||
|
|
b3a2460b3f | ||
|
|
3260bf1b2a | ||
|
|
e69c6456d1 | ||
|
|
f8b3792e62 | ||
|
|
01c34c0a79 | ||
|
|
825751239f | ||
|
|
2465739060 | ||
|
|
e6834d2033 | ||
|
|
924100df4c | ||
|
|
1ac7d1e08b | ||
|
|
7becccd628 | ||
|
|
dff5cac4fe | ||
|
|
ecd0c1de20 | ||
|
|
878cb17b62 | ||
|
|
706981c231 | ||
|
|
d212cf7c40 | ||
|
|
19b6e5154f | ||
|
|
89263918ea | ||
|
|
e8fc6d163b | ||
|
|
34a670f8d7 | ||
|
|
6a46e2dd57 | ||
|
|
5e0fa7c744 | ||
|
|
474ce17a13 | ||
|
|
f00eed8633 | ||
|
|
42bcefa068 | ||
|
|
29bde998ed | ||
|
|
e29a83ec25 | ||
|
|
ddfcbde7d4 | ||
|
|
42e31d3ece | ||
|
|
cd8480bc67 | ||
|
|
06dc05be08 | ||
|
|
d3fda81af3 | ||
|
|
f58d345b63 | ||
|
|
f35382a4e0 | ||
|
|
8087430dd2 | ||
|
|
2a5f1f3668 | ||
|
|
a889cb30e0 | ||
|
|
e411c0ab7b | ||
|
|
a136371c1f | ||
|
|
83fa213854 | ||
|
|
750cc1ecae | ||
|
|
e25a854d24 | ||
|
|
3b32195271 | ||
|
|
f9411794c4 | ||
|
|
c5fdbfd62d | ||
|
|
f12ae90aba | ||
|
|
20c4bbcee2 | ||
|
|
f3232f34c2 | ||
|
|
8fedb23399 | ||
|
|
4ca519ce62 | ||
|
|
6d4768272a | ||
|
|
e8cc22879f | ||
|
|
b287683a61 | ||
|
|
ea43ee99d6 | ||
|
|
48db68e7ac | ||
|
|
7807b7a280 | ||
|
|
ad0e223742 | ||
|
|
c9a74f2800 | ||
|
|
570ed61edb | ||
|
|
835b302c19 | ||
|
|
a0fad9874a |
No files matched your search
@@ -15,5 +15,7 @@ project {
|
||||
"**/coverage/**",
|
||||
"ui/desktop/electron-app/out/**",
|
||||
"ui/desktop/electron-app/ember-dist/**",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/pnpm-workspace.yaml",
|
||||
]
|
||||
}
|
||||
@@ -11,21 +11,57 @@ assignees: ''
|
||||
|
||||
For questions, the best place to get answers is on our [discussion forum](https://discuss.hashicorp.com/c/boundary), as they will get more visibility from experienced users than the issue tracker.
|
||||
|
||||
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose by contacting us at security@hashicorp.com. Our PGP key is available at [our security page](https://www.hashicorp.com/security/).
|
||||
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose this by contacting us at security@hashicorp.com. Our PGP key is available on [our security page](https://www.hashicorp.com/security/).
|
||||
|
||||
Logging is available on Boundary Desktop Client version >= 2.1.0
|
||||
|
||||
Logs are found at:
|
||||
|
||||
Linux: ~/.config/Boundary/logs/desktop-client.log
|
||||
MacOS: ~/Library/Logs/Boundary/desktop-client.log
|
||||
Windows: $env:USERPROFILE\AppData\Roaming\Boundary\logs\desktop-client.log
|
||||
|
||||
If requests are taking longer than expected, please change the logging level to DEBUG. This will log the cache daemon request times as well.
|
||||
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
## :bug: Bug Report
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Run `boundary authenticate ...`
|
||||
2. Run `boundary connect ...`
|
||||
3. See error
|
||||
### **Description**
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
_A clear and concise description of the bug._
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
### **Steps to Reproduce**
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See the error
|
||||
|
||||
## **Expected behavior**
|
||||
|
||||
_A clear description of what you expected to happen._
|
||||
|
||||
## **Actual behavior**
|
||||
|
||||
_A clear description of what actually happened._
|
||||
|
||||
## **Screenshots (if applicable)**
|
||||
|
||||
_Add screenshots or screen recordings to help explain the issue._
|
||||
|
||||
## **Environment**
|
||||
|
||||
- **OS and Version:** (e.g., Windows 10 (22H2), macOS (14.7.2))
|
||||
- **Architecture:** (e.g., arm64, x86_64)
|
||||
- **Browser/Version:** (if applicable)
|
||||
- **Desktop Client Version:** (if applicable)
|
||||
- **Controller/worker versions:** (if applicable)
|
||||
|
||||
## **Logs & Console Output (if applicable)**
|
||||
|
||||
_Paste any relevant error logs or browser console output here._
|
||||
|
||||
## **Additional Context**
|
||||
|
||||
_Any other details that might be relevant._
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
@@ -18,3 +18,11 @@
|
||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
|
||||
## PCI review checklist
|
||||
|
||||
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
||||
- [ ] I have documented a clear reason for, and description of, the change I am making.
|
||||
- [ ] If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
|
||||
- [ ] If applicable, I've documented the impact of any changes to security controls.
|
||||
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
|
||||
@@ -27,7 +27,6 @@ echo "Trying to merge the latest oss commit - ${latest_oss_commit} from oss/${BR
|
||||
if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit ${latest_oss_commit}" "${latest_oss_commit}"); then
|
||||
# Here we can try to auto resolve conflicts
|
||||
# This logic will vary from project to project. The below is just an example.
|
||||
# We'll try to resolve package.json and yarn.lock conflicts by taking the OSS changes
|
||||
conflicts=$(git diff --name-only --diff-filter=U)
|
||||
printf "Found conflicts:\n${conflicts}\n"
|
||||
# Resolve package.json conflicts by taking oss
|
||||
@@ -36,11 +35,12 @@ if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit $
|
||||
git checkout --theirs package.json
|
||||
git add package.json
|
||||
fi
|
||||
# Resolve yarn.lock conflicts by taking oss
|
||||
if echo "$conflicts" | grep -q yarn.lock; then
|
||||
echo "Checking out oss yarn.lock..."
|
||||
git checkout --theirs yarn.lock
|
||||
git add yarn.lock
|
||||
# Resolve pnpm-lock.yaml conflicts by re-running pnpm install:
|
||||
# https://pnpm.io/git#merge-conflicts
|
||||
if echo "$conflicts" | grep -q pnpm-lock.yaml; then
|
||||
echo "Re-running pnpm install to fix conflicts by regenerating pnpm-lock.yaml..."
|
||||
pnpm install
|
||||
git add pnpm-lock.yaml
|
||||
fi
|
||||
|
||||
# If we can commit all the changes without conflict, we have resolved everything automatically
|
||||
|
||||
@@ -9,91 +9,86 @@ on:
|
||||
jobs:
|
||||
dependencies:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
build-admin-ui-oss:
|
||||
name: Build Admin UI OSS
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
run_install: false
|
||||
- name: Build
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:oss
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run build:ui:admin:oss
|
||||
- name: Upload artifact admin-ui
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui
|
||||
path: ./ui/admin/dist/
|
||||
- name: Upload artifact OSS admin UI
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui-oss
|
||||
path: ./ui/admin/dist/
|
||||
build-admin-ui-enterprise:
|
||||
name: Build Admin UI Enterprise
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
run_install: false
|
||||
- name: Build
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:enterprise
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run build:ui:admin:enterprise
|
||||
- name: Upload artifact enterprise admin UI
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui-ent
|
||||
path: ./ui/admin/dist/
|
||||
build-admin-ui-hcp:
|
||||
name: Build Admin UI HCP
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
run_install: false
|
||||
- name: Build
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:hcp
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run build:ui:admin:hcp
|
||||
- name: Upload artifact HCP admin UI
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: admin-ui-hcp
|
||||
path: ./ui/admin/dist/
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -27,18 +27,18 @@ jobs:
|
||||
language: ['javascript']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@3096afedf9873361b2b2f65e1445b13272c83eb8 # codeql-bundle-v2.20.0
|
||||
uses: github/codeql-action/init@70df9def86d22bf0ea4e7f8b956e7b92e7c1ea22 # codeql-bundle-v2.20.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@3096afedf9873361b2b2f65e1445b13272c83eb8 # codeql-bundle-v2.20.0
|
||||
uses: github/codeql-action/analyze@70df9def86d22bf0ea4e7f8b956e7b92e7c1ea22 # codeql-bundle-v2.20.7
|
||||
@@ -2,7 +2,7 @@ name: Add Copyright Headers
|
||||
on: pull_request
|
||||
jobs:
|
||||
copywrite:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -10,6 +10,6 @@ jobs:
|
||||
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
||||
- name: Add missing Copyright Headers
|
||||
run: copywrite headers
|
||||
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
||||
- uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1
|
||||
with:
|
||||
commit_message: Add missing copyright headers
|
||||
@@ -11,7 +11,7 @@ name: Jira Sync
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
name: Jira sync
|
||||
steps:
|
||||
- name: Check if community user
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -4,71 +4,71 @@ jobs:
|
||||
|
||||
dependencies:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
compliance:
|
||||
name: License Compliance
|
||||
needs: [dependencies]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run compliance:licenses
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm run compliance:licenses
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: [dependencies, compliance]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn lint
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm lint
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: [lint]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: node-modules-cache
|
||||
- uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1.7.3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
install-dependencies: true
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn test
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- run: pnpm install --offline
|
||||
- run: pnpm test
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
trigger-oss-merge:
|
||||
# Run this only on merge events in OSS repo
|
||||
if: ${{ github.event.pull_request.merged && github.repository == 'hashicorp/boundary-ui' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- name: Trigger Merge
|
||||
env:
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
types: [ oss-merge ]
|
||||
jobs:
|
||||
oss-merge:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Test E2E Desktop Client
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{contains(github.event.pull_request.labels.*.name, 'desktop')}}
|
||||
name: Run Desktop E2E Tests
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
ENOS_VAR_boundary_edition: "ent"
|
||||
ENOS_VAR_e2e_debug_no_run: true
|
||||
ENOS_VAR_boundary_license_path: "./license.hclic"
|
||||
SETUP_CLI: true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
ui/desktop/electron-app/pnpm-lock.yaml
|
||||
|
||||
- name: Set up Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
|
||||
|
||||
- name: Set up Enos
|
||||
uses: hashicorp/action-setup-enos@b9fa53484a1e8fdcc7b02a118bcf01d65b9414c9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Vault CLI
|
||||
run: |
|
||||
wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/vault.zip
|
||||
unzip /tmp/vault.zip -d /usr/local/bin
|
||||
|
||||
- name: Checkout boundary-enterprise
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: hashicorp/boundary-enterprise
|
||||
# ref: ${{ github.event.inputs.boundary-enterprise-branch }}
|
||||
path: support/src/boundary-enterprise
|
||||
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||
|
||||
- name: GH fix for localhost resolution
|
||||
run: |
|
||||
cat /etc/hosts && echo "-----------"
|
||||
sudo sed -i 's/::1 *localhost ip6-localhost ip6-loopback/::1 ip6 -localhost ip6-loopback/g' /etc/hosts
|
||||
cat /etc/hosts
|
||||
ssh -V
|
||||
|
||||
echo "127.0.0.1 localhost boundary" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 localhost worker" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 localhost vault" | sudo tee -a /etc/hosts
|
||||
|
||||
# TODO: Build image ourselves in case we don't want to use latest?
|
||||
- name: Get Boundary docker image
|
||||
run: |
|
||||
docker pull hashicorp/boundary-enterprise:latest
|
||||
docker save hashicorp/boundary-enterprise:latest | gzip > /tmp/boundary-enterprise.zip
|
||||
|
||||
# TODO: Set up license in a better way? Use directly rather than writing to file and pull from secrets manager
|
||||
- name: Set up test infra
|
||||
id: infra
|
||||
run: |
|
||||
echo "${{ secrets.BOUNDARY_ENT_LICENSE }}" > ./support/src/boundary-enterprise/enos/license.hclic
|
||||
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
||||
mkdir -p ./enos/terraform-plugin-cache
|
||||
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||
export ENOS_VAR_boundary_docker_image_name=hashicorp/boundary-enterprise:latest && \
|
||||
export ENOS_VAR_boundary_docker_image_file=/tmp/boundary-enterprise.zip && \
|
||||
enos scenario launch --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
cd e2e-tests
|
||||
pnpm playwright install --with-deps
|
||||
|
||||
# Runner is headless so we need to enable a display to run the client
|
||||
- name: Run Tests
|
||||
run: |
|
||||
source <(bash ./support/src/boundary-enterprise/enos/scripts/test_e2e_env.sh)
|
||||
cd e2e-tests
|
||||
export DISPLAY=:99
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
pnpm run desktop
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: playwright-report
|
||||
path: e2e-tests/playwright-report
|
||||
|
||||
- name: Clean up test infra
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||
enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
trigger-gha-release:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Release description
|
||||
|
||||
@@ -19,7 +19,6 @@ coverage-*
|
||||
libpeerconnection.log
|
||||
npm-debug.log*
|
||||
testem.log
|
||||
yarn-error.log
|
||||
.watchman-cookie*
|
||||
|
||||
### VisualStudioCode ###
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
yarn compliance:licenses
|
||||
npx lint-staged
|
||||
pnpm compliance:licenses
|
||||
pnpm lint-staged
|
||||
@@ -21,4 +21,3 @@ else
|
||||
# This is to allow interaction with the terminal for commitizen
|
||||
exec < /dev/tty && npx git-cz --hook || true
|
||||
fi
|
||||
|
||||
Binary file not shown.
@@ -25,7 +25,7 @@ subproject, e.g. running or testing.
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Optional Tooling](#optional-tooling)
|
||||
- [Installation](#installation)
|
||||
- [Yarn Commands](#yarn-commands)
|
||||
- [Pnpm Commands](#pnpm-commands)
|
||||
- [Contributing](#contributing)
|
||||
- [Branching](#branching)
|
||||
- [Building ToC](#building-toc)
|
||||
@@ -33,8 +33,8 @@ subproject, e.g. running or testing.
|
||||
- [Building with a Container](#building-with-a-container)
|
||||
- [Building Desktop UI for Production](#building-desktop-ui-for-production)
|
||||
- [Connect Boundary UI to Boundary local instance](#connect-boundary-ui-to-boundary-local-instance)
|
||||
- [For admin](#for-admin)
|
||||
- [For desktop](#for-desktop)
|
||||
- [For Admin](#for-admin)
|
||||
- [For Desktop](#for-desktop)
|
||||
- [Committing](#committing)
|
||||
- [License Checking](#license-checking)
|
||||
|
||||
@@ -67,8 +67,8 @@ You will need the following things properly installed on your computer.
|
||||
|
||||
* [Git][git]
|
||||
* [Node.js][node]
|
||||
* Supported versions: `v18` and `v20`.
|
||||
* [Yarn][yarn] installed globally
|
||||
* Supported versions: `v20` and `v22`.
|
||||
* [Pnpm][pnpm] installed globally
|
||||
* [Google Chrome][chrome]
|
||||
* [Firefox][firefox]
|
||||
|
||||
@@ -90,18 +90,18 @@ that best suit you and your personal style.
|
||||
|
||||
* `git clone <repository-url>` this repository
|
||||
* `cd boundary-ui`
|
||||
* `yarn install`
|
||||
* `pnpm install`
|
||||
|
||||
## Yarn Commands
|
||||
## Pnpm Commands
|
||||
|
||||
List of available project commands. `yarn run <command-name>`
|
||||
List of available project commands. `pnpm run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|---|
|
||||
| build | Builds Admin UI for production. |
|
||||
| test | Runs tests in CI mode. |
|
||||
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
|
||||
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
|
||||
| compliance:audit | Checks for issues using `pnpm audit` with moderate and above criticality.|
|
||||
| compliance:licenses | Checks that all dependencies have OSS-compatible licenses. |
|
||||
| doc:toc | Automatically generates a table of contents for the README file. |
|
||||
|
||||
@@ -114,7 +114,7 @@ We follow [GitHub Flow][github-flow].
|
||||
### Building ToC
|
||||
|
||||
To autogenerate a ToC (table of contents) for this README,
|
||||
run `yarn doc:toc`. Please update the ToC whenever editing the structure
|
||||
run `pnpm doc:toc`. Please update the ToC whenever editing the structure
|
||||
of README.
|
||||
|
||||
### Building Admin UI for Production
|
||||
@@ -126,8 +126,8 @@ necessary for your target environment, as described in the [Environment Variable
|
||||
To quickly produce a production build of Admin UI, run from this folder:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Assets are saved to `ui/admin/dist/`.
|
||||
@@ -151,20 +151,20 @@ necessary for your target environment, as described in the [Environment Variable
|
||||
To quickly produce a production build of Desktop UI, run from this folder:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build:ui:desktop
|
||||
pnpm install
|
||||
pnpm build:ui:desktop
|
||||
```
|
||||
|
||||
In windows, UI is generated using docker to provide a stable UI across platforms.
|
||||
|
||||
```cmd
|
||||
yarn install
|
||||
pnpm install
|
||||
docker-compose -f docker-compose-embedding.yml run build-desktop-production
|
||||
yarn build:ui:desktop:app
|
||||
pnpm build:ui:desktop:app
|
||||
```
|
||||
|
||||
To provide a signing identify for macOS build, use `BOUNDARY_DESKTOP_SIGNING_IDENTITY`
|
||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `yarn build:ui:desktop:app`.
|
||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `pnpm build:ui:desktop:app`.
|
||||
|
||||
Assets are saved to `ui/desktop/electron-app/out/make/`.
|
||||
DMG packaged desktop UI is available at asset location as `boundary.dmg`.
|
||||
@@ -180,7 +180,7 @@ This assumes you already have up and running a Boundary dev instance in your loc
|
||||
|
||||
This assumes you are within `boundary-ui/ui/admin`.
|
||||
|
||||
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 yarn start`.
|
||||
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 pnpm start`.
|
||||
|
||||
Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
||||
|
||||
@@ -190,7 +190,7 @@ Once you open the UI you will see the login screen, authenticate using generated
|
||||
|
||||
This assumes you are within `boundary-ui/ui/desktop`.
|
||||
|
||||
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false yarn start:desktop`.
|
||||
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false pnpm start:desktop`.
|
||||
|
||||
In login screen, authenticate using generated admin login name and password.
|
||||
|
||||
@@ -199,7 +199,7 @@ Enter the cluster URL of your Boundary dev instance, by default is: `http://loca
|
||||
### Committing
|
||||
|
||||
We use `husky` and `lint-staged` to ensure linting and other checks pass before a
|
||||
commit is completed. Simply do a `yarn install` to make sure the hooks are installed.
|
||||
commit is completed. Simply do a `pnpm install` to make sure the hooks are installed.
|
||||
|
||||
Use a normal `git commit` to go through the checks, if you need to skip these checks,
|
||||
use `git commit --no-verify` to bypass them. However, a consistent commit message will
|
||||
@@ -218,10 +218,10 @@ will not be accepted.
|
||||
|
||||
[git]: https://git-scm.com/
|
||||
[node]: https://nodejs.org/
|
||||
[yarn]: https://classic.yarnpkg.com/lang/en/
|
||||
[pnpm]: https://pnpm.io
|
||||
[chrome]: https://google.com/chrome/
|
||||
[firefox]: https://firefox.com/
|
||||
[yarn-workspaces]: https://classic.yarnpkg.com/en/docs/workspaces/
|
||||
[pnpm-workspaces]: https://pnpm.io/workspaces
|
||||
[homebrew]: https://brew.sh
|
||||
[warp]: https://www.warp.dev
|
||||
[vscode]: https://code.visualstudio.com/
|
||||
|
||||
@@ -12,8 +12,8 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.hbs]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false,
|
||||
|
||||
/**
|
||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
/vendor/
|
||||
|
||||
# compiled output
|
||||
/declarations/
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.*/
|
||||
.eslintcache
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
@@ -56,7 +56,7 @@ module.exports = {
|
||||
extends: ['plugin:n/recommended'],
|
||||
},
|
||||
{
|
||||
// Test files:
|
||||
// test files
|
||||
files: ['tests/**/*-test.{js,ts}'],
|
||||
extends: ['plugin:qunit/recommended'],
|
||||
rules: {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# compiled output
|
||||
/dist/
|
||||
/declarations/
|
||||
|
||||
# dependencies
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/.env*
|
||||
/.pnp*
|
||||
/.eslintcache
|
||||
/coverage/
|
||||
/testem.log
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/package.json.ember-try
|
||||
|
||||
# broccoli-debug
|
||||
/DEBUG/
|
||||
+2
-12
@@ -2,11 +2,7 @@
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
|
||||
# misc
|
||||
/.bowerrc
|
||||
/.editorconfig
|
||||
/.ember-cli
|
||||
/.env*
|
||||
@@ -19,21 +15,15 @@
|
||||
/.prettierignore
|
||||
/.prettierrc.js
|
||||
/.template-lintrc.js
|
||||
/.travis.yml
|
||||
/.watchmanconfig
|
||||
/bower.json
|
||||
/CONTRIBUTING.md
|
||||
/ember-cli-build.js
|
||||
/testem.js
|
||||
/tests/
|
||||
/yarn-error.log
|
||||
/yarn.lock
|
||||
/tsconfig.declarations.json
|
||||
/tsconfig.json
|
||||
.gitkeep
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
@@ -1,25 +1,13 @@
|
||||
# unconventional js
|
||||
/blueprints/*/files/
|
||||
/vendor/
|
||||
|
||||
# compiled output
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
# dependencies
|
||||
/bower_components/
|
||||
/node_modules/
|
||||
|
||||
# misc
|
||||
/coverage/
|
||||
!.*
|
||||
.eslintcache
|
||||
.lint-todo/
|
||||
.*/
|
||||
|
||||
# ember-try
|
||||
/.node_modules.ember-try/
|
||||
/bower.json.ember-try
|
||||
/npm-shrinkwrap.json.ember-try
|
||||
/package.json.ember-try
|
||||
/package-lock.json.ember-try
|
||||
/yarn.lock.ember-try
|
||||
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
"ignore_dirs": ["dist"]
|
||||
}
|
||||
+15
-15
@@ -8,7 +8,7 @@ This addon contains the API data access layer for Boundary.
|
||||
|
||||
- [Add API to an App](#add-api-to-an-app)
|
||||
- [Installation](#installation)
|
||||
- [Yarn Commands](#yarn-commands)
|
||||
- [Pnpm Commands](#pnpm-commands)
|
||||
- [Linting](#linting)
|
||||
- [Formatting](#formatting)
|
||||
- [Running tests](#running-tests)
|
||||
@@ -21,7 +21,7 @@ This addon contains the API data access layer for Boundary.
|
||||
## Add API to an App
|
||||
|
||||
Add this addon to an Ember application's `devDependencies` as:
|
||||
`"api": "*"`, for applications included in this monorepo.
|
||||
`"api": "workspace:*"`, for applications included in this monorepo.
|
||||
|
||||
Since this addon also includes Mirage mocks, be sure to install
|
||||
`ember-cli-mirage` and add the following config to your UI project:
|
||||
@@ -38,9 +38,9 @@ See monorepo README for installation instructions.
|
||||
|
||||
When manually installing addon, ensure `ember-data-fragments@5.0.0-beta.*` is installed to use api models with data fragments.
|
||||
|
||||
## Yarn Commands
|
||||
## Pnpm Commands
|
||||
|
||||
List of available project commands. `yarn run <command-name>`
|
||||
List of available project commands. `pnpm run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
@@ -64,28 +64,28 @@ Additional commands in the monorepo package may affect this projects.
|
||||
|
||||
## Linting
|
||||
|
||||
* `yarn lint:fix`
|
||||
* `yarn lint:hbs`
|
||||
* `yarn lint:hbs:fix`
|
||||
* `yarn lint:js`
|
||||
* `yarn lint:js:fix`
|
||||
* `pnpm lint:fix`
|
||||
* `pnpm lint:hbs`
|
||||
* `pnpm lint:hbs:fix`
|
||||
* `pnpm lint:js`
|
||||
* `pnpm lint:js:fix`
|
||||
|
||||
## Formatting
|
||||
|
||||
Before submitting your work, be sure to run auto-formatters
|
||||
(see commands above). This helps to ensure consistency among authors.
|
||||
|
||||
* `yarn format`
|
||||
* `pnpm format`
|
||||
|
||||
## Running tests
|
||||
|
||||
* `yarn test` – Runs the test suite on the current Ember version
|
||||
* `yarn test --server` – Runs the test suite in "watch mode"
|
||||
* `yarn test:all` – Runs the test suite against multiple Ember versions
|
||||
* `pnpm test` – Runs the test suite on the current Ember version
|
||||
* `pnpm test --server` – Runs the test suite in "watch mode"
|
||||
* `pnpm test:all` – Runs the test suite against multiple Ember versions
|
||||
|
||||
## Running the dummy application
|
||||
|
||||
* `yarn start`
|
||||
* `pnpm start`
|
||||
* Visit the dummy application at [http://localhost:4200](http://localhost:4200).
|
||||
|
||||
## Contributing
|
||||
@@ -95,5 +95,5 @@ See monorepo README for more contribution instructions.
|
||||
### Building ToC
|
||||
|
||||
To autogenerate a ToC (table of contents) for this README,
|
||||
run `yarn doc:toc`. Please update the ToC whenever editing the structure
|
||||
run `pnpm doc:toc`. Please update the ToC whenever editing the structure
|
||||
of README.
|
||||
@@ -20,9 +20,9 @@
|
||||
* @param hooks
|
||||
*/
|
||||
export function setupIndexedDb(hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
hooks.beforeEach(async function () {
|
||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
||||
indexedDb.setup(`test-indexed-db-${Date.now()}`);
|
||||
await indexedDb.setup(`test-indexed-db-${Date.now()}`);
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Provides abilities for channel recordings.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
/**
|
||||
* Provides abilities for credential store.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
class InvalidRolePrincipalTypeError extends Error {
|
||||
name = 'InvalidRolePrincipalTypeError';
|
||||
|
||||
@@ -115,7 +115,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @return {Promise} promise
|
||||
*/
|
||||
async query(store, schema, query) {
|
||||
const queryObject = structuredClone(query);
|
||||
const { batchLimit, ...queryObject } = query;
|
||||
let result;
|
||||
let data = [];
|
||||
|
||||
@@ -126,8 +126,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
result = await super.query(store, schema, query);
|
||||
return prenormalizeArrayResponse(result);
|
||||
}
|
||||
|
||||
// Run this loop as long as the response_type is delta,
|
||||
// which indicates that there are more items in the list
|
||||
// or the batch size has not been reached
|
||||
do {
|
||||
result = await super.query(store, schema, queryObject);
|
||||
//add the result items to a data array
|
||||
@@ -136,7 +138,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
//pass in the list token for subsequent calls to fetch the remaining list items
|
||||
queryObject.list_token = result.list_token;
|
||||
}
|
||||
} while (result && result.response_type === 'delta');
|
||||
} while (
|
||||
result?.response_type === 'delta' &&
|
||||
(!batchLimit || data.length < batchLimit)
|
||||
);
|
||||
|
||||
result.items = data;
|
||||
return prenormalizeArrayResponse(result);
|
||||
@@ -163,7 +168,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides default ajax method by rewritting PUT requests as PATCH.
|
||||
* Overrides default ajax method by rewriting PUT requests as PATCH.
|
||||
* PATCH is the request method used by our API for updates.
|
||||
*
|
||||
* @override
|
||||
@@ -291,9 +296,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
const errors = fieldErrors.map((error) => ({
|
||||
detail: error.description,
|
||||
// Going forward, nested attributes will be hoisted. Thus "attributes."
|
||||
// must be stripped from the error name key. Until all models are
|
||||
// transitioned away from model fragments, however, both this and the
|
||||
// prior error key mappings are necessary.
|
||||
// must be stripped from the error name key.
|
||||
source: {
|
||||
pointer: `/data/attributes/${error.name.replace('attributes.', '')}`,
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@ export default class GeneratedHostSetModel extends BaseModel {
|
||||
name: ['aws', 'azure', 'gcp'],
|
||||
},
|
||||
emptyArrayIfMissing: true,
|
||||
trimWhitespace: true,
|
||||
})
|
||||
preferred_endpoints;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The AWS region to use.',
|
||||
trimWhitespace: true,
|
||||
})
|
||||
region;
|
||||
|
||||
@@ -109,6 +110,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The role ARN to use.',
|
||||
trimWhitespace: true,
|
||||
})
|
||||
role_arn;
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/* global __electronLog */
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { generateMQLExpression } from '../utils/mql-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
const { __electronLog } = globalThis;
|
||||
|
||||
/**
|
||||
* Not all types are yet supported by the cache daemon so we'll
|
||||
* just whitelist the ones we need to start with. Keys are the resource
|
||||
@@ -42,7 +43,10 @@ const fetchControllerData = async (context, next) => {
|
||||
// If we get an error or cache daemon is unavailable, fall back to calling the API
|
||||
context.request.data.query = remainingQuery;
|
||||
const results = await next(context.request);
|
||||
const models = results.content.toArray();
|
||||
|
||||
// As a result of https://rfcs.emberjs.com/id/0846-ember-data-deprecate-proxies/ we cannot use proxy array methods like .toArray
|
||||
// use slice() instead as indicated in the RFC.
|
||||
const models = results.content.slice();
|
||||
|
||||
const paginatedResult = paginateResults(models, page, pageSize);
|
||||
paginatedResult.meta = { totalItems: models.length };
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
*/
|
||||
|
||||
import { modelIndexes } from 'api/services/indexed-db';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { queryIndexedDb } from '../utils/indexed-db-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { sortResults } from '../utils/sort-results';
|
||||
import { hashCode } from '../utils/hash-code';
|
||||
|
||||
/**
|
||||
@@ -17,11 +18,15 @@ import { hashCode } from '../utils/hash-code';
|
||||
export default class IndexedDbHandler {
|
||||
@service indexedDb;
|
||||
|
||||
batchLimit = 10_000;
|
||||
|
||||
constructor(context) {
|
||||
setOwner(this, getOwner(context));
|
||||
}
|
||||
|
||||
async request(context, next) {
|
||||
const { batchLimit } = this;
|
||||
|
||||
switch (context.request.op) {
|
||||
case 'query': {
|
||||
const { store, data } = context.request;
|
||||
@@ -52,111 +57,89 @@ export default class IndexedDbHandler {
|
||||
|
||||
if (!peekIndexedDB) {
|
||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||
|
||||
let payload;
|
||||
let listToken;
|
||||
let writeToIndexedDbPromise;
|
||||
|
||||
if (storeToken) {
|
||||
listToken = await indexedDb.token.get(tokenKey);
|
||||
const tokenObj = await indexedDb.token.get(tokenKey);
|
||||
listToken = tokenObj?.token;
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
list_token: listToken?.token,
|
||||
});
|
||||
} catch (e) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
e?.errors[0].status === 400 &&
|
||||
e?.errors[0].code === 'invalid list token'
|
||||
) {
|
||||
await indexedDb[type].clear();
|
||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||
// Even if other tokens may still be valid, we might have cleared data that they
|
||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||
const tokenKeys = await indexedDb.token
|
||||
.where(':id')
|
||||
.startsWith(type)
|
||||
.primaryKeys();
|
||||
|
||||
await indexedDb.token.bulkDelete(tokenKeys);
|
||||
|
||||
do {
|
||||
try {
|
||||
payload = await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
list_token: listToken,
|
||||
batchLimit: batchLimit,
|
||||
});
|
||||
|
||||
// await the previous writeToIndexedDbPromise before writing to indexedDb again
|
||||
if (writeToIndexedDbPromise) {
|
||||
await writeToIndexedDbPromise;
|
||||
}
|
||||
} catch (err) {
|
||||
payload = await this.retryQueryFailure({
|
||||
err,
|
||||
indexedDb,
|
||||
type,
|
||||
adapter,
|
||||
store,
|
||||
schema,
|
||||
remainingQuery,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
listToken = payload.list_token;
|
||||
|
||||
// Store the token we just got back from the payload if it exists
|
||||
if (payload.list_token && storeToken) {
|
||||
await indexedDb.token.put({
|
||||
id: tokenKey,
|
||||
token: payload.list_token,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
await indexedDb[type].bulkDelete(payload.removed_ids);
|
||||
}
|
||||
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
||||
// since we are not storing the token we do not get back a list of removed_ids
|
||||
// from the API call and we do not want deleted items from showing in list view.
|
||||
if (!storeToken) {
|
||||
await indexedDb[type].clear();
|
||||
}
|
||||
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
payload,
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
const { data: payloadData } = normalizedPayload;
|
||||
|
||||
// Store the new data we just got back from the API refresh
|
||||
const items = payloadData.map((datum) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: datum,
|
||||
cleanData: true,
|
||||
schema,
|
||||
// Don't await immediately and let the next query run while we write to indexedDb
|
||||
writeToIndexedDbPromise = this.writeToIndexedDb({
|
||||
payload,
|
||||
storeToken,
|
||||
indexedDb,
|
||||
tokenKey,
|
||||
type,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
store,
|
||||
schema,
|
||||
});
|
||||
} while (payload.response_type === 'delta');
|
||||
|
||||
await indexedDb[type].bulkPut(items);
|
||||
await writeToIndexedDbPromise;
|
||||
}
|
||||
|
||||
// Results returned in JSON API reponse, but in a format optimized for indexedDb
|
||||
const indexedDbResults = await queryIndexedDb(
|
||||
indexedDb,
|
||||
type,
|
||||
queryObj,
|
||||
);
|
||||
|
||||
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
|
||||
(item) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: item,
|
||||
cleanData: false,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
// Normalize the results and clean up indexedDb format, ready for ember data consumption
|
||||
const normalizedResults = indexedDbResults.map((item) => {
|
||||
return this.indexedDb.normalizeData({
|
||||
data: item,
|
||||
cleanData: false,
|
||||
schema,
|
||||
serializer,
|
||||
});
|
||||
});
|
||||
|
||||
// Return the raw data if we don't push to the store.
|
||||
let records = dbRecords.map((record) => ({
|
||||
...record.attributes,
|
||||
id: record.id,
|
||||
}));
|
||||
// Sort the results
|
||||
const sortedResults = sortResults(normalizedResults, {
|
||||
querySort: queryObj?.sort,
|
||||
schema,
|
||||
});
|
||||
|
||||
if (pushToStore) {
|
||||
records = store.push({ data: dbRecords });
|
||||
}
|
||||
// Paginate the results
|
||||
const paginatedResults = paginateResults(sortedResults, page, pageSize);
|
||||
|
||||
// If we are not pushing to the store, use the raw data with id property
|
||||
const records = pushToStore
|
||||
? store.push({ data: paginatedResults })
|
||||
: paginatedResults.map((record) => ({
|
||||
...record.attributes,
|
||||
id: record.id,
|
||||
}));
|
||||
|
||||
// Set a meta property on the array to store the total items of the results.
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
@@ -169,4 +152,91 @@ export default class IndexedDbHandler {
|
||||
return next(context.request);
|
||||
}
|
||||
}
|
||||
|
||||
async retryQueryFailure({
|
||||
err,
|
||||
indexedDb,
|
||||
type,
|
||||
adapter,
|
||||
store,
|
||||
schema,
|
||||
remainingQuery,
|
||||
}) {
|
||||
// If we get an invalid list token, we'll delete the token and
|
||||
// clear the DB and try again without a token.
|
||||
if (
|
||||
err?.errors[0].status === 400 &&
|
||||
err?.errors[0].code === 'invalid list token'
|
||||
) {
|
||||
await indexedDb[type].clear();
|
||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||
// Even if other tokens may still be valid, we might have cleared data that they
|
||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||
const tokenKeys = await indexedDb.token
|
||||
.where(':id')
|
||||
.startsWith(type)
|
||||
.primaryKeys();
|
||||
|
||||
await indexedDb.token.bulkDelete(tokenKeys);
|
||||
|
||||
return await adapter.query(store, schema, {
|
||||
...remainingQuery,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async writeToIndexedDb({
|
||||
payload,
|
||||
storeToken,
|
||||
indexedDb,
|
||||
tokenKey,
|
||||
type,
|
||||
serializer,
|
||||
store,
|
||||
schema,
|
||||
}) {
|
||||
// Store the token we just got back from the payload if it exists
|
||||
if (payload.list_token && storeToken) {
|
||||
await indexedDb.token.put({
|
||||
id: tokenKey,
|
||||
token: payload.list_token,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove any records from the DB if the API indicates they've been deleted
|
||||
if (payload.removed_ids?.length > 0) {
|
||||
await indexedDb[type].bulkDelete(payload.removed_ids);
|
||||
}
|
||||
|
||||
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
||||
// since we are not storing the token we do not get back a list of removed_ids
|
||||
// from the API call and we do not want deleted items from showing in list view.
|
||||
if (!storeToken) {
|
||||
await indexedDb[type].clear();
|
||||
}
|
||||
|
||||
const normalizedPayload = serializer.normalizeResponse(
|
||||
store,
|
||||
schema,
|
||||
payload,
|
||||
null,
|
||||
'query',
|
||||
);
|
||||
|
||||
const { data: payloadData } = normalizedPayload;
|
||||
|
||||
// Store the new data we just got back from the API refresh
|
||||
const items = payloadData.map((datum) =>
|
||||
this.indexedDb.normalizeData({
|
||||
data: datum,
|
||||
cleanData: true,
|
||||
schema,
|
||||
serializer,
|
||||
}),
|
||||
);
|
||||
|
||||
await indexedDb[type].bulkPut(items);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import Mixin from '@ember/object/mixin';
|
||||
|
||||
/**
|
||||
* Overrides URL-building methods in an adpater for greater flexibility.
|
||||
* Overrides URL-building methods in an adapter for greater flexibility.
|
||||
* Specifically enhances calls to `urlPrefix` with `modelName`, `id`, and
|
||||
* `snapshot`, which aren't normally passed. And adds support for a
|
||||
* `urlSuffix` method, the return value of which is appended to the end of
|
||||
@@ -22,7 +22,7 @@ export default Mixin.create({
|
||||
|
||||
/**
|
||||
* This override behaves identically to the default method by default.
|
||||
* It exists only to document the additional paramters to the method.
|
||||
* It exists only to document the additional parameters to the method.
|
||||
* @override
|
||||
* @param {string} path
|
||||
* @param {string} parentURL
|
||||
|
||||
@@ -47,8 +47,8 @@ export default class AccountModel extends GeneratedAccountModel {
|
||||
* @type {string}
|
||||
*/
|
||||
get accountName() {
|
||||
const { email, full_name, login_name } = this;
|
||||
return email || full_name || login_name;
|
||||
const { email, full_name, login_name, subject } = this;
|
||||
return email || full_name || login_name || subject;
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Model from '@ember-data/model';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Base model class provides common functionality for all models.
|
||||
|
||||
@@ -7,7 +7,7 @@ import GeneratedGroupModel from '../generated/models/group';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class GroupModel extends GeneratedGroupModel {
|
||||
// =services
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export const TYPE_HOST_CATALOG_STATIC = 'static';
|
||||
export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin';
|
||||
@@ -40,7 +41,8 @@ export const STATIC_CREDENTIAL_FIELDS = ['access_key_id, secret_access_key'];
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
|
||||
#credentialType;
|
||||
@tracked _credentialTypeValue;
|
||||
|
||||
/**
|
||||
* True if the host catalog is static.
|
||||
* @type {boolean}
|
||||
@@ -62,14 +64,14 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {string}
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (!this._credentialTypeValue) {
|
||||
if (DYNAMIC_CREDENTIAL_FIELDS.some((field) => this[field])) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
return TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
return TYPE_CREDENTIAL_STATIC;
|
||||
}
|
||||
}
|
||||
return this.#credentialType;
|
||||
return this._credentialTypeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +79,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
* @type {string}
|
||||
*/
|
||||
set credentialType(type) {
|
||||
this.#credentialType = type;
|
||||
this._credentialTypeValue = type;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import GeneratedRoleModel from '../generated/models/role';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export const GRANT_SCOPE_THIS = 'this';
|
||||
export const GRANT_SCOPE_CHILDREN = 'children';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import GeneratedSessionRecordingModel from '../generated/models/session-recording';
|
||||
import { hasMany } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export const TYPE_SESSION_RECORDING_SSH = 'ssh';
|
||||
export const TYPES_SESSION_RECORDING = Object.freeze([
|
||||
@@ -71,7 +71,15 @@ export default class SessionRecordingModel extends GeneratedSessionRecordingMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply policy dates via the `reapply-staorage-policy` method.
|
||||
* Returns true if retention of session-recording is forever.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get retainForever() {
|
||||
return this.retain_until?.toISOString() === '9999-12-31T23:23:23.999Z';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reapply policy dates via the `reapply-storage-policy` method.
|
||||
* @param {object} options
|
||||
* @param {object} options.adapterOptions
|
||||
* @return {Promise}
|
||||
|
||||
@@ -7,7 +7,7 @@ import GeneratedSessionModel from '../generated/models/session';
|
||||
import { equal } from '@ember/object/computed';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { 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';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import GeneratedStorageBucketModel from '../generated/models/storage-bucket';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
|
||||
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
|
||||
@@ -26,7 +27,8 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
||||
export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
// attributes
|
||||
|
||||
#credentialType;
|
||||
@tracked _credentialTypeValue;
|
||||
|
||||
/**
|
||||
* True if the storage bucket is a plugin.
|
||||
* @type {boolean}
|
||||
@@ -40,14 +42,14 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
* @type {string}
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (!this._credentialTypeValue) {
|
||||
if (this.role_arn) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
return TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
return TYPE_CREDENTIAL_STATIC;
|
||||
}
|
||||
}
|
||||
return this.#credentialType;
|
||||
return this._credentialTypeValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,8 +57,9 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||
* @type {string}
|
||||
*/
|
||||
set credentialType(type) {
|
||||
this.#credentialType = type;
|
||||
this._credentialTypeValue = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the storage bucket is an unknown type.
|
||||
* @type {boolean}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import GeneratedTargetModel from '../generated/models/target';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export const TYPE_TARGET_TCP = 'tcp';
|
||||
export const TYPE_TARGET_SSH = 'ssh';
|
||||
|
||||
@@ -60,6 +60,18 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
// Do not serialize `disabled` fields.
|
||||
// TODO: disabled is temporarily disabled
|
||||
if (key === 'disabled') delete json[key];
|
||||
|
||||
// Before we transform our json
|
||||
// Let's trim the values in the json obj
|
||||
// Currently, json values will only ever be a string or an array
|
||||
if (options.trimWhitespace && json[key]) {
|
||||
if (typeOf(json[key]) === 'array') {
|
||||
json[key] = json[key].map((str) => str.trim());
|
||||
} else if (typeOf(json[key] === 'string')) {
|
||||
json[key] = json[key].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Push nested attributes down into the attributes key
|
||||
if (options.isNestedAttribute && json[key] !== undefined) {
|
||||
if (!json.attributes) json.attributes = {};
|
||||
@@ -232,12 +244,10 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeMissingArrays(store, primaryModelClass, payload) {
|
||||
const attrDefs = store
|
||||
.getSchemaDefinitionService()
|
||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
||||
const attrDefs = store.schema.fields({ type: primaryModelClass.modelName });
|
||||
if (attrDefs) {
|
||||
Object.keys(attrDefs).forEach((key) => {
|
||||
if (!payload[key] && attrDefs[key]?.options?.emptyArrayIfMissing) {
|
||||
attrDefs.keys().forEach((key) => {
|
||||
if (!payload[key] && attrDefs.get(key)?.options?.emptyArrayIfMissing) {
|
||||
payload[key] = [];
|
||||
}
|
||||
});
|
||||
@@ -256,12 +266,10 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeMissingObjects(store, primaryModelClass, payload) {
|
||||
const attrDefs = store
|
||||
.getSchemaDefinitionService()
|
||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
||||
const attrDefs = store.schema.fields({ type: primaryModelClass.modelName });
|
||||
if (attrDefs) {
|
||||
Object.keys(attrDefs).forEach((key) => {
|
||||
if (!payload[key] && attrDefs[key]?.options?.emptyObjectIfMissing) {
|
||||
attrDefs.keys().forEach((key) => {
|
||||
if (!payload[key] && attrDefs.get(key)?.options?.emptyObjectIfMissing) {
|
||||
payload[key] = {};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC:
|
||||
return this.serializeVaultGeneric(...arguments);
|
||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
|
||||
return this.serializeVaultSshCertificate(...arguments);
|
||||
default:
|
||||
return super.serialize(...arguments);
|
||||
}
|
||||
@@ -61,4 +62,16 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
|
||||
serializeVaultSshCertificate() {
|
||||
const serialized = super.serialize(...arguments);
|
||||
if (serialized.attributes) {
|
||||
const { key_type } = serialized.attributes;
|
||||
if (key_type !== 'rsa' && key_type !== 'ecdsa') {
|
||||
// If the key type is not RSA or ECDSA, set key_bits to null
|
||||
serialized.attributes.key_bits = null;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
) {
|
||||
if (options.isNestedAttribute) {
|
||||
json.attributes[key] = null;
|
||||
} else if (options.isNestedSecret && json.secrets[key]) {
|
||||
} else if (options.isNestedSecret && json.secrets?.[key]) {
|
||||
delete json.secrets[key];
|
||||
if (Object.keys(json.secrets).length === 0) {
|
||||
delete json.secrets;
|
||||
|
||||
@@ -95,4 +95,18 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
||||
|
||||
return super.normalize(typeClass, normalizedHash, ...rest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts errors from the payload and transforms specific keys if needed.
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
extractErrors() {
|
||||
const errors = super.extractErrors(...arguments);
|
||||
if (errors?.filter) {
|
||||
errors.filter_string = errors.filter;
|
||||
delete errors.filter;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,18 @@ export default class ManagedGroupSerializer extends ApplicationSerializer {
|
||||
|
||||
return super.normalize(typeClass, normalizedHash, ...rest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts errors from the payload and transforms specific keys if needed.
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
extractErrors() {
|
||||
const errors = super.extractErrors(...arguments);
|
||||
if (errors?.filter) {
|
||||
errors.filter_string = errors.filter;
|
||||
delete errors.filter;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { credentialType } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
|
||||
// This deletes any fields that don't belong to the record's credential type
|
||||
if (options.isNestedAttribute && json.attributes) {
|
||||
// The key must be included in the fieldsByType list above
|
||||
|
||||
@@ -97,23 +97,42 @@ const unconvertFields = (result, object, schema, serializer) => {
|
||||
|
||||
/**
|
||||
* Service to encapsulate the IndexedDB implementation. To use this service, call
|
||||
* `setup` from the application root and
|
||||
* `setup` from the application root. This will create the database and the necessary indexes.
|
||||
*
|
||||
* Make sure to increment the version number of the database whenever the indexes change.
|
||||
* This will delete the old database and create a new one with the new indexes.
|
||||
*/
|
||||
export default class IndexedDbService extends Service {
|
||||
// =attributes
|
||||
#db;
|
||||
#version = 2;
|
||||
|
||||
get db() {
|
||||
return this.#db;
|
||||
}
|
||||
|
||||
setup(dbName) {
|
||||
async setup(dbName) {
|
||||
// Don't run setup again if we already have one or if we didn't get a name
|
||||
if (this.#db || !dbName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doesDbExist = await Dexie.exists(dbName);
|
||||
|
||||
// If the database already exists, open the database and
|
||||
// check if the version is a lower version than the current one
|
||||
if (doesDbExist) {
|
||||
const dbVerifier = new Dexie(dbName);
|
||||
await dbVerifier.open();
|
||||
if (dbVerifier.verno < this.#version) {
|
||||
await dbVerifier.delete();
|
||||
} else {
|
||||
dbVerifier.close();
|
||||
}
|
||||
}
|
||||
|
||||
this.#db = new Dexie(dbName);
|
||||
this.#db.version(2).stores(modelIndexes);
|
||||
this.#db.version(this.#version).stores(modelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Takes a POJO representing a query and converts it to a boolean expression
|
||||
|
||||
@@ -56,7 +56,7 @@ class MemoryStorage {
|
||||
*
|
||||
* @example
|
||||
* import Route from '@ember/routing/route';
|
||||
* import { inject as service } from '@ember/service';
|
||||
* import { service } from '@ember/service';
|
||||
* export default class ApplicationRoute extends Route {
|
||||
* @service storage;
|
||||
* afterModel() {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Store, { CacheHandler } from '@ember-data/store';
|
||||
// Importing 'Store' from 'ember-data/store' to get default configuration automatically.
|
||||
// TODO: Remove eslint-disable after upgrading to eslint-plugin-ember >= v12.2.1
|
||||
// eslint-disable-next-line ember/use-ember-data-rfc-395-imports
|
||||
import Store from 'ember-data/store';
|
||||
import { CacheHandler } from '@ember-data/store';
|
||||
import RequestManager from '@ember-data/request';
|
||||
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
|
||||
import IndexedDbHandler from 'api/handlers/indexed-db-handler';
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class AccountValueMapArrayTransform extends Transform {
|
||||
export default class AccountValueMapArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const accountValues = serialized || [];
|
||||
return accountValues.map((accountValueString) => ({
|
||||
@@ -18,4 +16,8 @@ export default class AccountValueMapArrayTransform extends Transform {
|
||||
const accountValues = deserialized || [];
|
||||
return accountValues.map(({ key, value }) => `${key}=${value}`);
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { TrackedArray } from 'tracked-built-ins';
|
||||
|
||||
export default class ArrayTransform extends Transform {
|
||||
export default class ArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const arr = new TrackedArray(serialized || []);
|
||||
return arr;
|
||||
@@ -16,4 +15,8 @@ export default class ArrayTransform extends Transform {
|
||||
const arr = deserialized || [];
|
||||
return arr;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class DurationTransform extends Transform {
|
||||
export default class DurationTransform {
|
||||
/**
|
||||
* Normalize a duration string in the format "3.0001s" to milliseconds.
|
||||
* The backend returns a precision of nanoseconds but we round to whole
|
||||
@@ -37,4 +35,8 @@ export default class DurationTransform extends Transform {
|
||||
maximumFractionDigits: 9,
|
||||
})}s`;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class HostSourceIdArrayTransform extends Transform {
|
||||
export default class HostSourceIdArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const stringValues = serialized || [];
|
||||
return stringValues.map(({ id: host_source_id, ...obj }) => ({
|
||||
@@ -18,4 +16,8 @@ export default class HostSourceIdArrayTransform extends Transform {
|
||||
const strings = deserialized || [];
|
||||
return strings.map(({ host_source_id: id, ...obj }) => ({ id, ...obj }));
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,12 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class ObjectAsArrayTransform extends Transform {
|
||||
export default class ObjectAsArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const obj = serialized || {};
|
||||
return Object.entries(obj).map(([key, value]) => ({ key, value }));
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
const array = deserialized || [];
|
||||
|
||||
@@ -24,4 +23,8 @@ export default class ObjectAsArrayTransform extends Transform {
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { TrackedObject } from 'tracked-built-ins';
|
||||
|
||||
export default class ObjectTransform extends Transform {
|
||||
export default class ObjectTransform {
|
||||
/**
|
||||
* @param serialized
|
||||
* @returns {object}
|
||||
@@ -26,4 +25,8 @@ export default class ObjectTransform extends Transform {
|
||||
}
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
|
||||
export default class StringArrayTransform extends Transform {
|
||||
export default class StringArrayTransform {
|
||||
deserialize(serialized) {
|
||||
const stringValues = serialized || [];
|
||||
return stringValues.map((value) => ({ value }));
|
||||
@@ -15,4 +13,8 @@ export default class StringArrayTransform extends Transform {
|
||||
const strings = deserialized || [];
|
||||
return strings.map(({ value }) => value);
|
||||
}
|
||||
|
||||
static create() {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,9 @@ export async function queryIndexedDb(indexedDb, resource, query) {
|
||||
);
|
||||
}
|
||||
|
||||
// OrderBy has to be used on where clauses while if it's a
|
||||
// collection already we have to use sortBy. We'll also sort by
|
||||
// descending order based on created time to match the API as any
|
||||
// "or" clause can change the order
|
||||
if (filterCollection.orderBy) {
|
||||
return filterCollection.orderBy(getKey('created_time')).reverse().toArray();
|
||||
}
|
||||
return filterCollection.reverse().sortBy(getKey('created_time'));
|
||||
// filterCollection can be Dexie Table or a Dexie Collection.
|
||||
// This is an important detail you need to keep in mind when working on it.
|
||||
return filterCollection.toArray();
|
||||
}
|
||||
|
||||
const buildInitialWhereClause = ({ filterArrayOrObject, table, key }) => {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// created_time is not an attribute defined on all models, but is present in all the resources
|
||||
// coming from API
|
||||
const SORT_DEFAULT_ATTRIBUTE = 'created_time';
|
||||
const SORT_DIRECTION_ASCENDING = 'asc';
|
||||
const SORT_DIRECTION_DESCENDING = 'desc';
|
||||
|
||||
const sortFunctions = {
|
||||
string: (a, b) => String(a).localeCompare(String(b)),
|
||||
date: (a, b) => new Date(a).getTime() - new Date(b).getTime(),
|
||||
number: (a, b) => Number(a) - Number(b),
|
||||
boolean: (a, b) => Number(a) - Number(b),
|
||||
};
|
||||
|
||||
function getSortableValue(schema, record, attribute) {
|
||||
if (attribute === 'id') {
|
||||
return record.id;
|
||||
}
|
||||
|
||||
// all models have a created_time attribute but not all models have it defined
|
||||
if (attribute === 'created_time') {
|
||||
return record.attributes.created_time;
|
||||
}
|
||||
|
||||
if (schema.attributes.has(attribute)) {
|
||||
return record.attributes[attribute];
|
||||
}
|
||||
|
||||
const attributes = Array.from(schema.attributes.keys());
|
||||
throw new Error(
|
||||
`The attribute "${attribute}" does not map to the model definition of type "${record.type}". Supported sortable attributes are 'id', 'created_time', or ${attributes.map((attr) => `'${attr}'`).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
export const sortResults = (results, { querySort, schema }) => {
|
||||
querySort = querySort ?? {};
|
||||
const sortAttribute = querySort.attribute || SORT_DEFAULT_ATTRIBUTE;
|
||||
|
||||
// Default sort direction is ascending unless we are sorting by `created_time` (default sort attribute)
|
||||
const defaultSortDirection =
|
||||
sortAttribute === SORT_DEFAULT_ATTRIBUTE && !querySort.sortFunction
|
||||
? SORT_DIRECTION_DESCENDING
|
||||
: SORT_DIRECTION_ASCENDING;
|
||||
const sortDirection = querySort.direction || defaultSortDirection;
|
||||
|
||||
if (
|
||||
![SORT_DIRECTION_ASCENDING, SORT_DIRECTION_DESCENDING].includes(
|
||||
sortDirection,
|
||||
)
|
||||
) {
|
||||
throw new Error('Invalid sort direction');
|
||||
}
|
||||
|
||||
// Execute custom sort function if one is provided.
|
||||
if (querySort.sortFunction) {
|
||||
return results.toSorted((a, b) => {
|
||||
const sortResult = querySort.sortFunction(a, b);
|
||||
|
||||
return sortDirection === SORT_DIRECTION_ASCENDING
|
||||
? sortResult
|
||||
: -1 * sortResult;
|
||||
});
|
||||
}
|
||||
|
||||
const sortAttributeDataType = schema.attributes.get(sortAttribute)?.type;
|
||||
|
||||
const sortFunction =
|
||||
sortFunctions[sortAttributeDataType] ?? sortFunctions.string;
|
||||
|
||||
return results.toSorted((a, b) => {
|
||||
// Extract the values to sort and sort them.
|
||||
const sortValueA = getSortableValue(schema, a, sortAttribute);
|
||||
const sortValueB = getSortableValue(schema, b, sortAttribute);
|
||||
const sortResult = sortFunction(sortValueA, sortValueB);
|
||||
|
||||
return sortDirection === SORT_DIRECTION_ASCENDING
|
||||
? sortResult
|
||||
: -1 * sortResult;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// Ember-data no longer automatically installs BooleanTransform for apps.
|
||||
export { BooleanTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'rose/components/rose/dialog/body';
|
||||
export { DateTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default, initialize } from 'core/initializers/deprecations';
|
||||
export { NumberTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// Ember-data no longer automatically installs StringTransform for apps.
|
||||
export { StringTransform as default } from '@ember-data/serializer/transform';
|
||||
@@ -6,7 +6,6 @@
|
||||
'use strict';
|
||||
|
||||
const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
|
||||
const { maybeEmbroider } = require('@embroider/test-setup');
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const app = new EmberAddon(defaults, {
|
||||
@@ -19,6 +18,8 @@ module.exports = function (defaults) {
|
||||
This build file does *not* influence how the addon or the app using it
|
||||
behave. You most likely want to be modifying `./index.js` or app's build file
|
||||
*/
|
||||
|
||||
const { maybeEmbroider } = require('@embroider/test-setup');
|
||||
return maybeEmbroider(app, {
|
||||
skipBabel: [
|
||||
{
|
||||
|
||||
@@ -50,7 +50,8 @@ export default factory.extend({
|
||||
}
|
||||
case TYPE_AUTH_METHOD_OIDC:
|
||||
return {
|
||||
issuer: faker.internet.ip(),
|
||||
issuer:
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
subject: 'sub',
|
||||
email: faker.internet.email(),
|
||||
full_name: faker.person.fullName(),
|
||||
|
||||
@@ -39,7 +39,8 @@ export default factory.extend({
|
||||
switch (this.type) {
|
||||
case 'vault':
|
||||
return {
|
||||
address: faker.internet.ip(),
|
||||
address:
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
namespace: faker.word.words(1),
|
||||
ca_cert: 'ca-cert-123',
|
||||
tls_server_name: faker.internet.domainName(),
|
||||
|
||||
@@ -26,8 +26,6 @@ const pluginTypes = [
|
||||
'foobar',
|
||||
];
|
||||
|
||||
let pluginTypeCounter = 1;
|
||||
|
||||
export default factory.extend({
|
||||
id: () => generateId('hc_'),
|
||||
|
||||
@@ -64,7 +62,7 @@ export default factory.extend({
|
||||
if (this.type === TYPE_HOST_CATALOG_DYNAMIC) {
|
||||
return {
|
||||
id: `plugin-id-${i}`,
|
||||
name: pluginTypes[pluginTypeCounter++ % pluginTypes.length],
|
||||
name: pluginTypes[i % pluginTypes.length],
|
||||
description: faker.word.words(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,10 +43,12 @@ export default factory.extend({
|
||||
},
|
||||
preferred_endpoints() {
|
||||
if (this.type === 'plugin') {
|
||||
const enpointsAmount = faker.number.int({ min: 1, max: 5 });
|
||||
const endpointsAmount = faker.number.int({ min: 1, max: 5 });
|
||||
let result = [];
|
||||
for (let i = 0; i < enpointsAmount; ++i) {
|
||||
result.push(faker.internet.ip());
|
||||
for (let i = 0; i < endpointsAmount; ++i) {
|
||||
result.push(
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ export default factory.extend({
|
||||
const addressesAmount = Math.floor(Math.random() * 6) + 1;
|
||||
let result = [];
|
||||
for (let i = 0; i < addressesAmount; ++i) {
|
||||
result.push(faker.internet.ip());
|
||||
result.push(
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -103,6 +103,31 @@ export default factory.extend({
|
||||
},
|
||||
}),
|
||||
|
||||
withManyHosts: trait({
|
||||
afterCreate(target, server) {
|
||||
const { scope } = target;
|
||||
const hostCatalog = server.create('host-catalog', { scope });
|
||||
const hosts = server.createList('host', 15, {
|
||||
scope,
|
||||
hostCatalog,
|
||||
type: hostCatalog.type,
|
||||
});
|
||||
const hostSets = [
|
||||
server.create('host-set', {
|
||||
scope,
|
||||
hostCatalog,
|
||||
hosts,
|
||||
type: hostCatalog.type,
|
||||
}),
|
||||
];
|
||||
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
|
||||
hosts.forEach((host) => {
|
||||
host.update({ hostSetIds });
|
||||
});
|
||||
target.update({ hostSets });
|
||||
},
|
||||
}),
|
||||
|
||||
withTwoHosts: trait({
|
||||
afterCreate(target, server) {
|
||||
const { scope } = target;
|
||||
@@ -120,6 +145,10 @@ export default factory.extend({
|
||||
type: hostCatalog.type,
|
||||
}),
|
||||
];
|
||||
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
|
||||
hosts.forEach((host) => {
|
||||
host.update({ hostSetIds });
|
||||
});
|
||||
target.update({ hostSets });
|
||||
},
|
||||
}),
|
||||
@@ -139,6 +168,10 @@ export default factory.extend({
|
||||
type: hostCatalog.type,
|
||||
}),
|
||||
];
|
||||
const hostSetIds = hostSets.map((hostSet) => hostSet.id);
|
||||
hosts.forEach((host) => {
|
||||
host.update({ hostSetIds });
|
||||
});
|
||||
target.update({ hostSets });
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -16,6 +16,9 @@ export default Factory.extend({
|
||||
updated_time: () => faker.date.recent(),
|
||||
version: () => faker.number.int(),
|
||||
attributes: () => {
|
||||
return { address: faker.internet.ipv6() };
|
||||
return {
|
||||
address:
|
||||
Math.random() > 0.5 ? faker.internet.ip() : faker.internet.ipv6(),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -3,30 +3,8 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// import config from '../../config/environment';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
// /**
|
||||
// * Simulates a cookie-based response to authentication requests. Any user/pass
|
||||
// * values are allowed *except* when `username` equals `error`, in which case a
|
||||
// * simulated error response is returned.
|
||||
// * @param {object} schema
|
||||
// * @param {object} request
|
||||
// * @return {Response}
|
||||
// */
|
||||
// export default function authenticateHandler(schema, request) {
|
||||
// const payload = JSON.parse(request.requestBody);
|
||||
// if (payload.attributes.login_name === 'error') {
|
||||
// return new Response(400);
|
||||
// } else {
|
||||
// const cookieName = config.auth.passwordCookieName;
|
||||
// const cookieValue = v1();
|
||||
// const cookiePath = config.rootURL;
|
||||
// document.cookie = `${cookieName}=${cookieValue};path=${cookiePath}`;
|
||||
// return new Response(200);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Quick and dirty counter system to simulate the polling behavior
|
||||
// of the API. If the OIDC flow is pending, the route returns 100.
|
||||
// If complete it returns 200 with the token JSON.
|
||||
|
||||
@@ -110,7 +110,18 @@ export default function (server) {
|
||||
|
||||
// Other resources
|
||||
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
|
||||
server.createList('host-catalog', 8, { scope }, 'withChildren');
|
||||
server.createList(
|
||||
'host-catalog',
|
||||
2,
|
||||
{ scope, type: 'static' },
|
||||
'withChildren',
|
||||
);
|
||||
server.createList(
|
||||
'host-catalog',
|
||||
8,
|
||||
{ scope, type: 'plugin' },
|
||||
'withChildren',
|
||||
);
|
||||
server.createList('credential-store', 3, { scope }, 'withAssociations');
|
||||
server.createList('target', 2, { scope }, 'withAssociations');
|
||||
server.create('target', { scope, address: '0.0.0.0' });
|
||||
|
||||
@@ -199,6 +199,9 @@ export default function initializeMockIPC(server, config) {
|
||||
* Check for OS chrome state
|
||||
*/
|
||||
showWindowActions() {
|
||||
if (navigator.userAgent.includes('Mac')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+41
-28
@@ -10,8 +10,10 @@
|
||||
"scripts": {
|
||||
"build:development": "ember build",
|
||||
"build": "ember build --environment=production",
|
||||
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
|
||||
"lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
|
||||
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
|
||||
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"",
|
||||
"lint:hbs": "ember-template-lint .",
|
||||
"lint:hbs:fix": "ember-template-lint . --fix",
|
||||
"lint:js": "eslint .",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"format": "concurrently \"npm:format:*\" --names \"format:\"",
|
||||
@@ -19,61 +21,69 @@
|
||||
"start": "ember serve",
|
||||
"test": "ember exam --test-port 0 --split=4 --parallel=1 --random",
|
||||
"test:ember-compatibility": "ember try:each",
|
||||
"precommit": "yarn lint && yarn format && yarn test",
|
||||
"precommit": "pnpm lint && pnpm format && pnpm test",
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.10",
|
||||
"@ember/string": "^4.0.0",
|
||||
"dexie": "^3.2.4",
|
||||
"ember-auto-import": "^2.8.1",
|
||||
"ember-auto-import": "^2.10.0",
|
||||
"ember-can": "^4.2.0",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-htmlbars": "^6.3.0",
|
||||
"ember-cli-mirage": "^3.0.3",
|
||||
"ember-data": "~5.3.12",
|
||||
"miragejs": "^0.1.48",
|
||||
"ember-data": "~4.12.0",
|
||||
"tracked-built-ins": "^3.1.1"
|
||||
},
|
||||
"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": "^4.0.4",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@babel/eslint-parser": "^7.25.1",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@ember/optional-features": "^2.1.0",
|
||||
"@ember/test-helpers": "^5.2.1",
|
||||
"@ember/test-waiters": "^4.1.0",
|
||||
"@embroider/test-setup": "^4.0.0",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^9.1.0",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-cli": "^5.12.0",
|
||||
"ember-cli": "~5.12.0",
|
||||
"ember-cli-clean-css": "^3.0.0",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-exam": "^8.0.0",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-page-title": "^8.2.3",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-resolver": "^12.0.1",
|
||||
"ember-source": "~5.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
"ember-template-lint": "^5.7.2",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-ember": "^11.11.1",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-qunit": "^8.0.1",
|
||||
"ember-template-lint": "^6.0.0",
|
||||
"ember-try": "^3.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-ember": "^12.2.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-qunit": "^8.1.2",
|
||||
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"qunit": "^2.22.0",
|
||||
"qunit-dom": "^3.2.1",
|
||||
"webpack": "^5.94.0"
|
||||
"sinon": "^19.0.2",
|
||||
"webpack": "^5.95.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": ">= 4.0.0",
|
||||
"sinon": "^19.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
"node": "20.* || 22.*"
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
@@ -82,8 +92,11 @@
|
||||
"configPath": "tests/dummy/config"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": "eslint --fix",
|
||||
"*.js": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.hbs": "ember-template-lint --fix",
|
||||
"*.{js,scss,yaml}": "prettier --write"
|
||||
"*.{scss,yaml}": "prettier --write"
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,12 @@ module.exports = {
|
||||
// --no-sandbox is needed when running Chrome inside a container
|
||||
process.env.CI ? '--no-sandbox' : null,
|
||||
'--headless',
|
||||
// Disable backgrounding renders for occluded windows
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
// Disable renderer process backgrounding
|
||||
'--disable-renderer-backgrounding',
|
||||
// Disable task throttling of timer tasks from background pages
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
//'--disable-software-rasterizer',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class ApplicationRoute extends Route {
|
||||
// =services
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
|
||||
{{page-title "Dummy"}}
|
||||
|
||||
<h2 id="title">Welcome to Ember</h2>
|
||||
<Hds::Text::Display @tag="h2" id="title">Welcome to Ember</Hds::Text::Display>
|
||||
|
||||
{{outlet}}
|
||||
@@ -3,7 +3,7 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "ember-cli",
|
||||
"version": "4.12.1",
|
||||
"version": "5.12.0",
|
||||
"blueprints": [
|
||||
{
|
||||
"name": "addon",
|
||||
@@ -11,7 +11,8 @@
|
||||
"codemodsSource": "ember-addon-codemods-manifest@1",
|
||||
"isBaseBlueprint": true,
|
||||
"options": [
|
||||
"--no-welcome"
|
||||
"--no-welcome",
|
||||
"--pnpm"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,28 +10,21 @@ const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup');
|
||||
|
||||
module.exports = async function () {
|
||||
return {
|
||||
usePnpm: true,
|
||||
scenarios: [
|
||||
{
|
||||
name: 'ember-lts-4.4',
|
||||
name: 'ember-lts-4.12',
|
||||
npm: {
|
||||
devDependencies: {
|
||||
'ember-source': '~4.4.0',
|
||||
'ember-source': '~4.12.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ember-lts-4.8',
|
||||
name: 'ember-lts-5.4',
|
||||
npm: {
|
||||
devDependencies: {
|
||||
'ember-source': '~4.8.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ember-lts-4.4',
|
||||
npm: {
|
||||
devDependencies: {
|
||||
'ember-source': '~4.4.0',
|
||||
'ember-source': '~5.4.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
setupTest as upstreamSetupTest,
|
||||
} from 'ember-qunit';
|
||||
|
||||
// This file exists to provide wrappers around ember-qunit's / ember-mocha's
|
||||
// This file exists to provide wrappers around ember-qunit's
|
||||
// test setup functions. This way, you can easily extend the setup that is
|
||||
// needed per test type.
|
||||
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import IndexedDbHandler from 'api/handlers/indexed-db-handler';
|
||||
import RequestManager from '@ember-data/request';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import sinon from 'sinon';
|
||||
import { assert } from '@ember/debug';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import TargetModel from 'api/models/target';
|
||||
|
||||
function createPaginatedResponseHandler(mirageRecords, { pageSize }) {
|
||||
assert('pageSize is required', pageSize);
|
||||
const pagesCount = mirageRecords.length / pageSize;
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < pagesCount; i++) {
|
||||
const items = mirageRecords.slice(i * pageSize, (i + 1) * pageSize);
|
||||
const isLastPage = i === pagesCount - 1;
|
||||
const list_token = `list-token-${faker.string.nanoid({ min: 5, max: 5 })}`;
|
||||
|
||||
results.push({
|
||||
items,
|
||||
list_token,
|
||||
response_type: !isLastPage ? 'delta' : 'complete',
|
||||
});
|
||||
}
|
||||
|
||||
function nextResult(listToken) {
|
||||
const index = results.findIndex(
|
||||
({ list_token }) => list_token === listToken,
|
||||
);
|
||||
const nextResult = index === -1 ? null : results[index + 1];
|
||||
return nextResult;
|
||||
}
|
||||
|
||||
// mirage handler compatible with paginated responses
|
||||
return function (_schema, request) {
|
||||
const listToken = request.queryParams.list_token;
|
||||
if (!listToken) {
|
||||
return results[0];
|
||||
}
|
||||
|
||||
const result = nextResult(listToken);
|
||||
|
||||
if (!result) {
|
||||
return new Response(
|
||||
404,
|
||||
{},
|
||||
`No results found for list token "${listToken}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
module('Unit | Handler | indexed-db-handler', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
let manager,
|
||||
indexeddbHandler,
|
||||
store,
|
||||
applicationAdapter,
|
||||
indexedbHandlerSpy,
|
||||
applicationAdapterSpy;
|
||||
hooks.beforeEach(async function setupIndexHandler() {
|
||||
store = this.owner.lookup('service:store');
|
||||
manager = new RequestManager();
|
||||
indexeddbHandler = new IndexedDbHandler(store);
|
||||
applicationAdapter = store.adapterFor('application');
|
||||
|
||||
// spy on all handler and application adapter methods
|
||||
indexedbHandlerSpy = sinon.spy(indexeddbHandler);
|
||||
applicationAdapterSpy = sinon.spy(applicationAdapter);
|
||||
|
||||
store.requestManager = manager;
|
||||
manager.use([indexeddbHandler]);
|
||||
});
|
||||
|
||||
module('fetching and batch loading', function (hooks) {
|
||||
// `testBatchLimit` and `serverResultPageLimit` are intentionally difference
|
||||
// so that multiple calls to the server are required to fetch a single batch,
|
||||
// and these multiples can be asserted in the tests individually
|
||||
const testBatchLimit = 10;
|
||||
const serverResultPageLimit = 5;
|
||||
let aliases, aliasResponseHandlerSpy;
|
||||
|
||||
hooks.beforeEach(function setupMirageData() {
|
||||
indexeddbHandler.batchLimit = testBatchLimit;
|
||||
|
||||
aliases = this.server.createList('alias', 50);
|
||||
|
||||
const aliasResponseHandler = createPaginatedResponseHandler(
|
||||
this.server.schema.aliases.all().models,
|
||||
{ pageSize: serverResultPageLimit },
|
||||
);
|
||||
|
||||
aliasResponseHandlerSpy = sinon.spy(aliasResponseHandler);
|
||||
this.server.get('aliases', aliasResponseHandlerSpy);
|
||||
});
|
||||
|
||||
test('it returns all data for a resource from the server', async function (assert) {
|
||||
const results = await store.query('alias', {});
|
||||
|
||||
assert.strictEqual(
|
||||
aliases.length,
|
||||
results.length,
|
||||
'all records are returned',
|
||||
);
|
||||
|
||||
const aliasesSortedByCreatedTime = [...aliases].sort(
|
||||
(a, b) => new Date(b.created_time) - new Date(a.created_time),
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
aliasesSortedByCreatedTime.map(({ name }) => name),
|
||||
results.map(({ name }) => name),
|
||||
'`name` property matches between the returned results and the mirage records sorted by created_time',
|
||||
);
|
||||
});
|
||||
|
||||
test('supports batching requests and writes to indexedDB', async function (assert) {
|
||||
await store.query('alias', {});
|
||||
|
||||
assert.strictEqual(
|
||||
indexedbHandlerSpy.writeToIndexedDb.callCount,
|
||||
aliases.length / testBatchLimit,
|
||||
'handler #writeToIndexedDb is called once for each batch',
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
applicationAdapterSpy.query.callCount,
|
||||
aliases.length / testBatchLimit,
|
||||
'application adapter #query is called once for each batch',
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
aliasResponseHandlerSpy.callCount,
|
||||
aliases.length / serverResultPageLimit,
|
||||
'server handler should be called once for each page',
|
||||
);
|
||||
});
|
||||
|
||||
test('it loads from indexeddb only and does not fetch from api, when peekIndexedDB option is used', async function (assert) {
|
||||
const callCounts = {
|
||||
get writeToIndexedDb() {
|
||||
return indexedbHandlerSpy.writeToIndexedDb.callCount;
|
||||
},
|
||||
get aliasResponseHandler() {
|
||||
return aliasResponseHandlerSpy.callCount;
|
||||
},
|
||||
};
|
||||
|
||||
// before first query, no data has been fetched or written to indexeddb
|
||||
// the call counts should therefore be 0
|
||||
assert.strictEqual(callCounts.aliasResponseHandler, 0);
|
||||
assert.strictEqual(callCounts.writeToIndexedDb, 0);
|
||||
|
||||
const results = await store.query('alias', {});
|
||||
assert.strictEqual(results.length, 50);
|
||||
|
||||
// call counts expected after first query
|
||||
const expectedWriteToIndexedDbCallCount = 5;
|
||||
const expectedResponseHandlerCallCount = 10;
|
||||
|
||||
// first `store.query()` calls api and writes batches to indexededdb
|
||||
assert.strictEqual(
|
||||
callCounts.aliasResponseHandler,
|
||||
expectedResponseHandlerCallCount,
|
||||
);
|
||||
assert.strictEqual(
|
||||
callCounts.writeToIndexedDb,
|
||||
expectedWriteToIndexedDbCallCount,
|
||||
);
|
||||
|
||||
await store.query('alias', {}, { peekIndexedDB: true });
|
||||
assert.strictEqual(results.length, 50);
|
||||
|
||||
// second run does not increase call counts to api or writes to indexeddb
|
||||
assert.strictEqual(
|
||||
callCounts.aliasResponseHandler,
|
||||
expectedResponseHandlerCallCount,
|
||||
);
|
||||
assert.strictEqual(
|
||||
callCounts.writeToIndexedDb,
|
||||
expectedWriteToIndexedDbCallCount,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('sorting', function () {
|
||||
test('it sorts by descending `created_time` by default', async function (assert) {
|
||||
const target2020 = this.server.create('target', {
|
||||
name: 'Target 2020',
|
||||
created_time: '2020-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
const target2021 = this.server.create('target', {
|
||||
name: 'Target 2021',
|
||||
created_time: '2021-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
const target2022 = this.server.create('target', {
|
||||
name: 'Target 2022',
|
||||
created_time: '2022-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
const target2023 = this.server.create('target', {
|
||||
name: 'Target 2023',
|
||||
created_time: '2023-01-01T00:00:00Z',
|
||||
});
|
||||
|
||||
const shuffledTargets = faker.helpers.shuffle([
|
||||
target2020,
|
||||
target2021,
|
||||
target2022,
|
||||
target2023,
|
||||
]);
|
||||
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(shuffledTargets, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await store.query('target', {});
|
||||
assert.deepEqual(
|
||||
results.map(({ name }) => name),
|
||||
['Target 2023', 'Target 2022', 'Target 2021', 'Target 2020'],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by ascending (default) `name` using string sorting', async function (assert) {
|
||||
const target001 = this.server.create('target', { name: 'Target 001' });
|
||||
const target002 = this.server.create('target', { name: 'Target 002' });
|
||||
const target003 = this.server.create('target', { name: 'Target 003' });
|
||||
const target004 = this.server.create('target', { name: 'Target 004' });
|
||||
const target005 = this.server.create('target', { name: 'Target 005' });
|
||||
|
||||
const shuffledTargets = faker.helpers.shuffle([
|
||||
target001,
|
||||
target002,
|
||||
target003,
|
||||
target004,
|
||||
target005,
|
||||
]);
|
||||
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(shuffledTargets, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await store.query('target', {
|
||||
query: { sort: { attribute: 'name' } },
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
results.map(({ name }) => name),
|
||||
['Target 001', 'Target 002', 'Target 003', 'Target 004', 'Target 005'],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by descending, then ascending `name` using string sorting', async function (assert) {
|
||||
const alias001 = this.server.create('alias', { name: 'Alias 001' });
|
||||
const alias002 = this.server.create('alias', { name: 'Alias 002' });
|
||||
const alias003 = this.server.create('alias', { name: 'Alias 003' });
|
||||
const alias004 = this.server.create('alias', { name: 'Alias 004' });
|
||||
|
||||
const shuffledAliases = faker.helpers.shuffle([
|
||||
alias001,
|
||||
alias002,
|
||||
alias003,
|
||||
alias004,
|
||||
]);
|
||||
|
||||
this.server.get(
|
||||
'aliases',
|
||||
createPaginatedResponseHandler(shuffledAliases, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const resultsDesc = await store.query('alias', {
|
||||
query: { sort: { attribute: 'name', direction: 'desc' } },
|
||||
});
|
||||
const resultsAsc = await store.query('alias', {
|
||||
query: { sort: { attribute: 'name', direction: 'asc' } },
|
||||
});
|
||||
assert.deepEqual(
|
||||
resultsDesc.map(({ name }) => name),
|
||||
['Alias 004', 'Alias 003', 'Alias 002', 'Alias 001'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
resultsAsc.map(({ name }) => name),
|
||||
['Alias 001', 'Alias 002', 'Alias 003', 'Alias 004'],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by descending, then ascending `name` using custom string sorting', async function (assert) {
|
||||
const alias1 = this.server.create('alias', { name: 'alias1' });
|
||||
const alias2 = this.server.create('alias', { name: 'alias2' });
|
||||
const alias3 = this.server.create('alias', { name: 'alias3' });
|
||||
const alias4 = this.server.create('alias', { name: 'alias4' });
|
||||
|
||||
const sortName = (recordA, recordB) => {
|
||||
const nameMap = {
|
||||
alias3: 'Alpha',
|
||||
alias4: 'Beta',
|
||||
alias1: 'Delta',
|
||||
alias2: 'Epsilon',
|
||||
};
|
||||
return String(nameMap[recordA.attributes.name]).localeCompare(
|
||||
String(nameMap[recordB.attributes.name]),
|
||||
);
|
||||
};
|
||||
|
||||
const shuffledAliases = faker.helpers.shuffle([
|
||||
alias1,
|
||||
alias2,
|
||||
alias3,
|
||||
alias4,
|
||||
]);
|
||||
|
||||
this.server.get(
|
||||
'aliases',
|
||||
createPaginatedResponseHandler(shuffledAliases, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const resultsDesc = await store.query('alias', {
|
||||
query: { sort: { sortFunction: sortName, direction: 'desc' } },
|
||||
});
|
||||
const resultsAsc = await store.query('alias', {
|
||||
query: { sort: { sortFunction: sortName, direction: 'asc' } },
|
||||
});
|
||||
assert.deepEqual(
|
||||
resultsDesc.map(({ name }) => name),
|
||||
['alias2', 'alias1', 'alias4', 'alias3'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
resultsAsc.map(({ name }) => name),
|
||||
['alias3', 'alias4', 'alias1', 'alias2'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('option: pushToStore', function (hooks) {
|
||||
let mirageTargets;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
mirageTargets = this.server.createList('target', 5);
|
||||
});
|
||||
|
||||
test('it pushes fetched results to the store by default', async function (assert) {
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(mirageTargets, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.strictEqual(mirageTargets.length, 5);
|
||||
assert.strictEqual(store.peekAll('target').length, 0);
|
||||
const results = await store.query('target', {});
|
||||
assert.strictEqual(results.length, 5);
|
||||
assert.ok(
|
||||
results.every((result) => result instanceof TargetModel),
|
||||
'results are ember data models',
|
||||
);
|
||||
assert.strictEqual(store.peekAll('target').length, 5);
|
||||
});
|
||||
|
||||
test('it pushes fethed results to the store when pushToStore is true', async function (assert) {
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(mirageTargets, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.strictEqual(mirageTargets.length, 5);
|
||||
assert.strictEqual(store.peekAll('target').length, 0);
|
||||
const results = await store.query('target', {}, { pushToStore: true });
|
||||
assert.strictEqual(results.length, 5);
|
||||
assert.ok(
|
||||
results.every((result) => result instanceof TargetModel),
|
||||
'results are ember data models',
|
||||
);
|
||||
assert.strictEqual(store.peekAll('target').length, 5);
|
||||
});
|
||||
|
||||
test('it does not push fetched results to the store when pushToStore is false', async function (assert) {
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(mirageTargets, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.strictEqual(mirageTargets.length, 5);
|
||||
assert.strictEqual(store.peekAll('target').length, 0);
|
||||
const results = await store.query('target', {}, { pushToStore: false });
|
||||
assert.strictEqual(results.length, 5);
|
||||
assert.ok(
|
||||
results.every((result) => !(result instanceof TargetModel)),
|
||||
'results are not ember data models',
|
||||
);
|
||||
assert.strictEqual(store.peekAll('target').length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('it supports searching', async function (assert) {
|
||||
this.server.create('target', {
|
||||
name: 'Magical Target',
|
||||
description: 'A target',
|
||||
});
|
||||
|
||||
this.server.create('target', {
|
||||
name: 'Target 2',
|
||||
description: 'Target with magic',
|
||||
});
|
||||
|
||||
this.server.createList('target', 4);
|
||||
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(this.server.schema.targets.all().models, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await store.query('target', {
|
||||
query: { search: 'magic' },
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
results
|
||||
// search results aren't returning in a consistent order so
|
||||
// the sorting is used to be able to assert
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(({ name, description }) => ({ name, description })),
|
||||
[
|
||||
{
|
||||
description: 'A target',
|
||||
name: 'Magical Target',
|
||||
},
|
||||
{
|
||||
description: 'Target with magic',
|
||||
name: 'Target 2',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('it supports filtering', async function (assert) {
|
||||
this.server.create('target', {
|
||||
name: 'specific-target',
|
||||
});
|
||||
|
||||
this.server.createList('target', 5);
|
||||
|
||||
this.server.get(
|
||||
'targets',
|
||||
createPaginatedResponseHandler(this.server.schema.targets.all().models, {
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await store.query('target', {
|
||||
query: { filters: { name: [{ equals: 'specific-target' }] } },
|
||||
});
|
||||
|
||||
assert.strictEqual(results[0].name, 'specific-target');
|
||||
});
|
||||
});
|
||||
@@ -381,4 +381,29 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// JSON values will only be trimmed if the model attr
|
||||
// includes the trimWhitespace property set to true
|
||||
// See preferred_endpoints attr on host-set model
|
||||
test('it trims whitespace on json values', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-set', {
|
||||
name: 'Host Set 1',
|
||||
compositeType: 'azure',
|
||||
description: 'Description',
|
||||
host_catalog_id: '123',
|
||||
version: 1,
|
||||
preferred_endpoints: [{ value: ' option 1' }, { value: 'option 2 ' }],
|
||||
sync_interval_seconds: 1,
|
||||
});
|
||||
assert.deepEqual(record.serialize(), {
|
||||
name: 'Host Set 1',
|
||||
description: 'Description',
|
||||
host_catalog_id: '123',
|
||||
version: 1,
|
||||
preferred_endpoints: ['option 1', 'option 2'],
|
||||
sync_interval_seconds: 1,
|
||||
attributes: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -231,4 +231,52 @@ module('Unit | Serializer | credential library', function (hooks) {
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes vault_ssh_certificate correctly when changing type to ed25519', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('credential-library');
|
||||
store.push({
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'credential-library',
|
||||
attributes: {
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
version: 1,
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
path: '/vault/path',
|
||||
username: 'user',
|
||||
key_type: 'rsa',
|
||||
key_bits: 100,
|
||||
ttl: '100',
|
||||
key_id: 'id',
|
||||
extensions: [{ key: 'key', value: 'value' }],
|
||||
critical_options: [{ key: 'key', value: 'value' }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const record = store.peekRecord('credential-library', '1');
|
||||
record.key_type = 'ed25519';
|
||||
|
||||
const snapshot = record._createSnapshot();
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
|
||||
assert.deepEqual(serializedRecord, {
|
||||
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||
credential_store_id: null,
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
attributes: {
|
||||
path: '/vault/path',
|
||||
username: 'user',
|
||||
key_type: 'ed25519',
|
||||
key_bits: null,
|
||||
ttl: '100',
|
||||
key_id: 'id',
|
||||
extensions: { key: 'value' },
|
||||
critical_options: { key: 'value' },
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -54,7 +54,7 @@ module('Unit | Serializer | host set', function (hooks) {
|
||||
description: 'Description',
|
||||
host_catalog_id: '123',
|
||||
version: 1,
|
||||
preferred_endpoints: [{ value: 'option 1' }, { value: 'option 2' }],
|
||||
preferred_endpoints: [{ value: ' option 1' }, { value: 'option 2 ' }],
|
||||
sync_interval_seconds: 1,
|
||||
filters: [{ value: 'filter 1' }, { value: 'filter 2' }],
|
||||
});
|
||||
@@ -208,4 +208,22 @@ module('Unit | Serializer | host set', function (hooks) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it handles errors correctly', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('host-set');
|
||||
const schema = store.modelFor('host-set');
|
||||
|
||||
const errors = {
|
||||
base: ['Error in provided request.'],
|
||||
filter: ['This field is required.'],
|
||||
};
|
||||
|
||||
const result = serializer.extractErrors(store, schema, errors, null);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
base: ['Error in provided request.'],
|
||||
filter_string: ['This field is required.'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -183,7 +183,8 @@ module('Unit | Serializer | Managed group', function (hooks) {
|
||||
test('it normalizes correctly', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('managed-group');
|
||||
const hostSetModelClass = store.createRecord('managed-group').constructor;
|
||||
const managedGroupModelClass =
|
||||
store.createRecord('managed-group').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
description: 'Description for the test',
|
||||
@@ -196,7 +197,7 @@ module('Unit | Serializer | Managed group', function (hooks) {
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
hostSetModelClass,
|
||||
managedGroupModelClass,
|
||||
payload,
|
||||
);
|
||||
|
||||
@@ -219,4 +220,22 @@ module('Unit | Serializer | Managed group', function (hooks) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it handles errors correctly', function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('managed-group');
|
||||
const schema = store.modelFor('managed-group');
|
||||
|
||||
const errors = {
|
||||
base: ['Error in provided request.'],
|
||||
filter: ['This field is required.'],
|
||||
};
|
||||
|
||||
const result = serializer.extractErrors(store, schema, errors, null);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
base: ['Error in provided request.'],
|
||||
filter_string: ['This field is required.'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'eu-west-1',
|
||||
region: 'eu-west-1 ',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
disable_credential_rotation: true,
|
||||
@@ -117,7 +117,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
access_key_id: '',
|
||||
secret_access_key: '',
|
||||
disable_credential_rotation: true,
|
||||
role_arn: 'arn',
|
||||
role_arn: 'arn ',
|
||||
role_external_id: 'Example987',
|
||||
role_session_name: 'my-session',
|
||||
role_tags: [
|
||||
@@ -160,12 +160,12 @@ module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
plugin: { name: 'aws' },
|
||||
region: 'eu-west-1',
|
||||
region: ' eu-west-1 ',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
disable_credential_rotation: true,
|
||||
version: 1,
|
||||
role_arn: 'role_arn_test',
|
||||
role_arn: ' role_arn_test ',
|
||||
role_external_id: 'role_external_id_test',
|
||||
role_session_name: 'role_session_test',
|
||||
role_tags: [
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { assert, module, test } from 'qunit';
|
||||
import { sortResults } from 'api/utils/sort-results';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
function createJsonApiMockRecord(id, type, attributes) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
|
||||
module('Unit | Utility | sortResults', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
module('Defaults', function () {
|
||||
test("throws an error when an attribute is not id, created_time or defined as a schema attribute, even when it exists on the record's attribute object", function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
name: 'Result 01',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
name: 'Result 02',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
name: 'Result 03',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
]);
|
||||
|
||||
const querySort = { attribute: 'name' };
|
||||
|
||||
// Note: `name` is not defined as a schema attribute but exists on the record
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('date', { type: 'date' });
|
||||
|
||||
assert.throws(
|
||||
() => sortResults(shuffledResults, { querySort, schema }),
|
||||
/Error: The attribute "name" does not map to the model definition of type "target". Supported sortable attributes are 'id', 'created_time', or 'date'/,
|
||||
);
|
||||
});
|
||||
|
||||
test('throws an error when results do not contain a valid sort direction', function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
name: 'Result 01',
|
||||
});
|
||||
|
||||
const querySort = {
|
||||
attribute: 'name',
|
||||
direction: 'invalid sort direction',
|
||||
};
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('name', { type: 'string' });
|
||||
assert.throws(
|
||||
() => sortResults([result01], { querySort, schema }),
|
||||
/Error: Invalid sort direction/,
|
||||
);
|
||||
});
|
||||
|
||||
test('when passing no querySort, sorts using default created_time, descending order and string sorting', async function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
created_time: '2021',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
created_time: '2022',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
created_time: '2023',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
created_time: '2024',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySort = {};
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('created_time', { type: 'string' });
|
||||
const sortedResults = sortResults(shuffledResults, {
|
||||
querySort,
|
||||
schema,
|
||||
});
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ attributes: { created_time } }) => created_time),
|
||||
['2024', '2023', '2022', '2021'],
|
||||
);
|
||||
});
|
||||
|
||||
test('when sorting by id, without being on the results attributes it sorts as expected ', function () {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
name: 'Result 01',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
name: 'Result 02',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
name: 'Result 03',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
name: 'Result 04',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySort = { attribute: 'id' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('id', { type: 'string' });
|
||||
const sortedResults = sortResults(shuffledResults, { querySort, schema });
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ id }) => id),
|
||||
['id_01', 'id_02', 'id_03', 'id_04'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('string sorting', function () {
|
||||
test('it sorts by ascending (default) `name` using string sorting', async function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
name: 'Result 01',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
name: 'Result 02',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
name: 'Result 03',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
name: 'Result 04',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySort = { attribute: 'name' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('name', { type: 'string' });
|
||||
const sortedResults = sortResults(shuffledResults, { querySort, schema });
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ attributes: { name } }) => name),
|
||||
['Result 01', 'Result 02', 'Result 03', 'Result 04'],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by ascending then descending `name` using string sorting', async function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
name: 'Result 01',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
name: 'Result 02',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
name: 'Result 03',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
name: 'Result 04',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySortAsc = { attribute: 'name', direction: 'asc' };
|
||||
const querySortDesc = { attribute: 'name', direction: 'desc' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('name', { type: 'string' });
|
||||
const sortedResultsAsc = sortResults(shuffledResults, {
|
||||
querySort: querySortAsc,
|
||||
schema,
|
||||
});
|
||||
const sortedResultsDesc = sortResults(shuffledResults, {
|
||||
querySort: querySortDesc,
|
||||
schema,
|
||||
});
|
||||
assert.deepEqual(
|
||||
sortedResultsAsc.map(({ attributes: { name } }) => name),
|
||||
['Result 01', 'Result 02', 'Result 03', 'Result 04'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
sortedResultsDesc.map(({ attributes: { name } }) => name),
|
||||
['Result 04', 'Result 03', 'Result 02', 'Result 01'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('date sorting', function () {
|
||||
test('it sorts by ascending (default) `date` using date sorting', function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
date: '2021',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
date: '2022',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
date: '2023',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
date: '2024',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySort = { attribute: 'date' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('date', { type: 'date' });
|
||||
|
||||
const sortedResults = sortResults(shuffledResults, { querySort, schema });
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ attributes: { date } }) => date),
|
||||
['2021', '2022', '2023', '2024'],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by ascending then descending `date` using date sorting', function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
date: '2021',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
date: '2022',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
date: '2023',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
date: '2024',
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySortAsc = { attribute: 'date', direction: 'asc' };
|
||||
const querySortDesc = { attribute: 'date', direction: 'desc' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('date', { type: 'date' });
|
||||
|
||||
const sortedResultsAsc = sortResults(shuffledResults, {
|
||||
querySort: querySortAsc,
|
||||
schema,
|
||||
});
|
||||
const sortedResultsDesc = sortResults(shuffledResults, {
|
||||
querySort: querySortDesc,
|
||||
schema,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResultsAsc.map(({ attributes: { date } }) => date),
|
||||
['2021', '2022', '2023', '2024'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
sortedResultsDesc.map(({ attributes: { date } }) => date),
|
||||
['2024', '2023', '2022', '2021'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('number sorting', function () {
|
||||
test('it sorts by ascending (default) `sessions`', function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 10,
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 200,
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 3333,
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 40444,
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySort = { attribute: 'sessions' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('sessions', { type: 'number' });
|
||||
|
||||
const sortedResults = sortResults(shuffledResults, { querySort, schema });
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ attributes: { sessions } }) => sessions),
|
||||
[10, 200, 3333, 40444],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by ascending then descending `sessions`', function (assert) {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 10,
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 200,
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 3333,
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_01', 'target', {
|
||||
sessions: 40444,
|
||||
});
|
||||
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySortAsc = { attribute: 'sessions', direction: 'asc' };
|
||||
const querySortDesc = { attribute: 'sessions', direction: 'desc' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('sessions', { type: 'number' });
|
||||
|
||||
const sortedResultsAsc = sortResults(shuffledResults, {
|
||||
querySort: querySortAsc,
|
||||
schema,
|
||||
});
|
||||
const sortedResultsDesc = sortResults(shuffledResults, {
|
||||
querySort: querySortDesc,
|
||||
schema,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResultsAsc.map(({ attributes: { sessions } }) => sessions),
|
||||
[10, 200, 3333, 40444],
|
||||
);
|
||||
assert.deepEqual(
|
||||
sortedResultsDesc.map(({ attributes: { sessions } }) => sessions),
|
||||
[40444, 3333, 200, 10],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('boolean sorting', function () {
|
||||
test('it sorts by ascending (default) `active`', function (assert) {
|
||||
const result01 = { attributes: { active: true } };
|
||||
const result02 = { attributes: { active: false } };
|
||||
const result03 = { attributes: { active: false } };
|
||||
const result04 = { attributes: { active: true } };
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySort = { attribute: 'active' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('active', { type: 'boolean' });
|
||||
|
||||
const sortedResults = sortResults(shuffledResults, { querySort, schema });
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ attributes: { active } }) => active),
|
||||
[false, false, true, true],
|
||||
);
|
||||
});
|
||||
|
||||
test('it sorts by ascending then descending `active`', function (assert) {
|
||||
const result01 = { attributes: { active: true } };
|
||||
const result02 = { attributes: { active: false } };
|
||||
const result03 = { attributes: { active: false } };
|
||||
const result04 = { attributes: { active: true } };
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
const querySortAsc = { attribute: 'active', direction: 'asc' };
|
||||
const querySortDesc = { attribute: 'active', direction: 'desc' };
|
||||
const schema = { attributes: new Map() };
|
||||
schema.attributes.set('active', { type: 'boolean' });
|
||||
|
||||
const sortedResultsAsc = sortResults(shuffledResults, {
|
||||
querySort: querySortAsc,
|
||||
schema,
|
||||
});
|
||||
const sortedResultsDesc = sortResults(shuffledResults, {
|
||||
querySort: querySortDesc,
|
||||
schema,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResultsAsc.map(({ attributes: { active } }) => active),
|
||||
[false, false, true, true],
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResultsDesc.map(({ attributes: { active } }) => active),
|
||||
[true, true, false, false],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('custom function sorting', function () {
|
||||
const result01 = createJsonApiMockRecord('id_01', 'target', {
|
||||
name: 'target1',
|
||||
});
|
||||
const result02 = createJsonApiMockRecord('id_02', 'target', {
|
||||
name: 'target2',
|
||||
});
|
||||
const result03 = createJsonApiMockRecord('id_03', 'target', {
|
||||
name: 'target3',
|
||||
});
|
||||
const result04 = createJsonApiMockRecord('id_04', 'target', {
|
||||
name: 'target4',
|
||||
});
|
||||
|
||||
const sortFunction = (recordA, recordB) => {
|
||||
const nameMap = {
|
||||
target3: 'Alpha',
|
||||
target4: 'Beta',
|
||||
target1: 'Delta',
|
||||
target2: 'Epsilon',
|
||||
};
|
||||
return String(nameMap[recordA.attributes.name]).localeCompare(
|
||||
String(nameMap[recordB.attributes.name]),
|
||||
);
|
||||
};
|
||||
const schema = { attributes: new Map() };
|
||||
|
||||
test.each(
|
||||
'it sorts by `name` using custom sorting function',
|
||||
{
|
||||
'ascending (default)': {
|
||||
querySort: { sortFunction },
|
||||
expectedResults: ['target3', 'target4', 'target1', 'target2'],
|
||||
},
|
||||
descending: {
|
||||
querySort: { sortFunction, direction: 'desc' },
|
||||
expectedResults: ['target2', 'target1', 'target4', 'target3'],
|
||||
},
|
||||
ascending: {
|
||||
querySort: { sortFunction, direction: 'asc' },
|
||||
expectedResults: ['target3', 'target4', 'target1', 'target2'],
|
||||
},
|
||||
},
|
||||
function (assert, input) {
|
||||
const shuffledResults = faker.helpers.shuffle([
|
||||
result01,
|
||||
result02,
|
||||
result03,
|
||||
result04,
|
||||
]);
|
||||
schema.attributes.set('name', { type: 'string' });
|
||||
const sortedResults = sortResults(shuffledResults, {
|
||||
querySort: input.querySort,
|
||||
schema,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
sortedResults.map(({ attributes: { name } }) => name),
|
||||
input.expectedResults,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,8 +12,8 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.hbs]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false,
|
||||
|
||||
/**
|
||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||
|
||||
Loaded 100 of 1016 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user