Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde2f259e8 | ||
|
|
e38c28c288 | ||
|
|
0ff91f9182 | ||
|
|
857bf01596 | ||
|
|
965e82473d | ||
|
|
0d55812f64 | ||
|
|
e807e1a3af | ||
|
|
64e568bd72 | ||
|
|
401ff354e8 | ||
|
|
9c31154941 | ||
|
|
1947365c91 | ||
|
|
be528dae47 | ||
|
|
d3c789c66c | ||
|
|
15727f61fd | ||
|
|
486d8b116a | ||
|
|
91ffbfb315 | ||
|
|
9b33812f9e | ||
|
|
b40c53fb45 | ||
|
|
97258f7525 | ||
|
|
ace65eab3a | ||
|
|
20e6df522a | ||
|
|
c8abc3845a | ||
|
|
6cd2f0f7d8 | ||
|
|
1266930647 | ||
|
|
695d9d413d | ||
|
|
edaf19f59b | ||
|
|
de5defeab2 | ||
|
|
63ec418cf0 | ||
|
|
e4f4394197 | ||
|
|
0b9357e9b3 | ||
|
|
777380e19f | ||
|
|
25b3a940a3 | ||
|
|
a97f66e011 | ||
|
|
fac7d2b128 | ||
|
|
ad1bb34453 | ||
|
|
8605a54221 | ||
|
|
98cd9dc1ec | ||
|
|
bd857836ee | ||
|
|
4974273bd0 | ||
|
|
c4e58fb780 | ||
|
|
2515efead6 | ||
|
|
fda3ca9d4b | ||
|
|
9648e1fa3b | ||
|
|
e395b1196c | ||
|
|
b7e6492d26 | ||
|
|
6311d7587e | ||
|
|
07573d6b2c | ||
|
|
90890441e1 | ||
|
|
1430adce7e | ||
|
|
89afee7297 | ||
|
|
78f89da348 | ||
|
|
9bacb55e87 | ||
|
|
dea4a62955 | ||
|
|
ef163709f2 | ||
|
|
0cfa814cdd | ||
|
|
694c43296a | ||
|
|
c98bfb5622 | ||
|
|
7cebece600 | ||
|
|
a9cfc43404 | ||
|
|
c38ba294cc | ||
|
|
ce2649272f | ||
|
|
3d60d047e3 | ||
|
|
a3082e97e6 | ||
|
|
333eebd731 | ||
|
|
320af9f6f7 | ||
|
|
af9b832d38 | ||
|
|
bea791b2e6 | ||
|
|
e31ac72531 | ||
|
|
075d8c6d8f | ||
|
|
56f42c3f5a | ||
|
|
af7196215b | ||
|
|
80d2898e56 | ||
|
|
f607e77274 | ||
|
|
007f5d8b46 | ||
|
|
a9d110a1b6 | ||
|
|
79b06ee565 | ||
|
|
0af81d9bc1 | ||
|
|
9e668e8467 | ||
|
|
69da1fa04c | ||
|
|
392792eb92 | ||
|
|
9c018f131f | ||
|
|
0542218537 | ||
|
|
7ac904a31f | ||
|
|
a7d9a7e348 | ||
|
|
0d0dadf398 | ||
|
|
45c16252a7 | ||
|
|
9b3cbaa6b3 | ||
|
|
89cfb14d6e | ||
|
|
a68d7d103a | ||
|
|
4dc5e13f24 |
No files matched your search
@@ -19,7 +19,7 @@ jobs:
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
@@ -37,17 +37,17 @@ jobs:
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- name: Build
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:oss
|
||||
- name: Upload artifact admin-ui
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: admin-ui
|
||||
path: ./ui/admin/dist/
|
||||
- name: Upload artifact OSS admin UI
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: admin-ui-oss
|
||||
path: ./ui/admin/dist/
|
||||
@@ -65,12 +65,12 @@ jobs:
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- name: Build
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:enterprise
|
||||
- name: Upload artifact enterprise admin UI
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: admin-ui-ent
|
||||
path: ./ui/admin/dist/
|
||||
@@ -88,12 +88,12 @@ jobs:
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- name: Build
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run build:ui:admin:hcp
|
||||
- name: Upload artifact HCP admin UI
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: admin-ui-hcp
|
||||
path: ./ui/admin/dist/
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8f1a6fed33af5212fab8a999d004627ae8901d1b # codeql-bundle-v2.17.5
|
||||
uses: github/codeql-action/init@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8f1a6fed33af5212fab8a999d004627ae8901d1b # codeql-bundle-v2.17.5
|
||||
uses: github/codeql-action/analyze@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
||||
@@ -1,10 +1,15 @@
|
||||
name: Check Copyright Headers
|
||||
name: Add Copyright Headers
|
||||
on: pull_request
|
||||
jobs:
|
||||
copywrite:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
||||
- name: Check Header Compliance
|
||||
run: copywrite headers --plan
|
||||
- name: Add missing Copyright Headers
|
||||
run: copywrite headers
|
||||
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
||||
with:
|
||||
commit_message: Add missing copyright headers
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn install
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn run compliance:licenses
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
node-modules-
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn lint
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: yarn test
|
||||
@@ -98,7 +98,7 @@ List of available project commands. `yarn run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|---|
|
||||
| build | Builds Admin UI. |
|
||||
| build | Builds Admin UI for production. |
|
||||
| test | Runs tests in CI mode. |
|
||||
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
|
||||
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
|
||||
@@ -119,9 +119,9 @@ of README.
|
||||
|
||||
### Building Admin UI for Production
|
||||
|
||||
See ui/admin/README.md Building for Production for more information on how to
|
||||
build the Admin UI. Be sure to set build-related environment variables as
|
||||
necessary for your target environment, as described in the Admin UI readme.
|
||||
See [Admin UI README](ui/admin/README.md#building-for-production) for more information on how to
|
||||
build the Admin UI for production. Be sure to set build-related environment variables as
|
||||
necessary for your target environment, as described in the [Environment Variables](ui/admin/README.md#environment-variables) section of README.
|
||||
|
||||
To quickly produce a production build of Admin UI, run from this folder:
|
||||
|
||||
@@ -144,9 +144,9 @@ Assets are saved to `ui/admin/dist/`.
|
||||
|
||||
### Building Desktop UI for Production
|
||||
|
||||
See ui/desktop/README.md Building for Production for more information on how to
|
||||
See [Desktop UI README](ui/desktop/README.md#building-for-production) for more information on how to
|
||||
build the Desktop UI. Be sure to set build-related environment variables as
|
||||
necessary for your target environment, as described in the Desktop UI readme.
|
||||
necessary for your target environment, as described in the [Environment Variables](ui/desktop/README.md#environment-variables-prod) section of README.
|
||||
|
||||
To quickly produce a production build of Desktop UI, run from this folder:
|
||||
|
||||
@@ -174,8 +174,9 @@ EXE archived desktop UI is available at asset location under `zip` folder.
|
||||
|
||||
This describes how to connect local Boundary UI to your local instance of Boundary.
|
||||
|
||||
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://learn.hashicorp.com/tutorials/boundary/getting-started-dev?in=boundary/getting-started#start-a-dev-environment).
|
||||
#### For admin
|
||||
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://developer.hashicorp.com/boundary/tutorials/get-started-community/community-get-started-dev)
|
||||
|
||||
#### For Admin
|
||||
|
||||
This assumes you are within `boundary-ui/ui/admin`.
|
||||
|
||||
@@ -185,7 +186,7 @@ Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
||||
|
||||
Once you open the UI you will see the login screen, authenticate using generated admin login name and password.
|
||||
|
||||
#### For desktop
|
||||
#### For Desktop
|
||||
|
||||
This assumes you are within `boundary-ui/ui/desktop`.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import sinon from 'sinon';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { resourceNames } from 'api/handlers/cache-daemon-handler';
|
||||
import { singularize } from 'ember-inflector';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
/**
|
||||
* This test helper can be used to help setup your sinon stubs in your tests.
|
||||
@@ -69,16 +70,18 @@ export default function setupStubs(hooks) {
|
||||
.withArgs('searchCacheDaemon')
|
||||
.onCall(i)
|
||||
.returns({
|
||||
[resourceName]: models.map((model) => {
|
||||
// Use internal serializer to serialize the model correctly
|
||||
// according to our mirage serializers
|
||||
const modelData =
|
||||
this.server.serializerOrRegistry.serialize(model);
|
||||
[underscore(resourceNames[singularize(resourceName)])]: models.map(
|
||||
(model) => {
|
||||
// Use internal serializer to serialize the model correctly
|
||||
// according to our mirage serializers
|
||||
const modelData =
|
||||
this.server.serializerOrRegistry.serialize(model);
|
||||
|
||||
// Serialize the data properly to standard JSON as that is what
|
||||
// we're expecting from the cache daemon response
|
||||
return JSON.parse(JSON.stringify(modelData));
|
||||
}),
|
||||
// Serialize the data properly to standard JSON as that is what
|
||||
// we're expecting from the cache daemon response
|
||||
return JSON.parse(JSON.stringify(modelData));
|
||||
},
|
||||
),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -65,12 +65,19 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
|
||||
// AWS specific
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
||||
})
|
||||
worker_filter;
|
||||
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: '',
|
||||
})
|
||||
region;
|
||||
|
||||
// AWS static credentials
|
||||
@attr('string', {
|
||||
isNestedSecret: true,
|
||||
description: '',
|
||||
@@ -83,6 +90,31 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
||||
})
|
||||
secret_access_key;
|
||||
|
||||
// AWS dynamic credentials
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The role ARN to use.',
|
||||
})
|
||||
role_arn;
|
||||
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The role external ID to use.',
|
||||
})
|
||||
role_external_id;
|
||||
|
||||
@attr('string', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The role session to use.',
|
||||
})
|
||||
role_session_name;
|
||||
|
||||
@attr('object-as-array', {
|
||||
isNestedAttribute: true,
|
||||
description: 'The role tags to use.',
|
||||
})
|
||||
role_tags;
|
||||
|
||||
// Azure specific
|
||||
|
||||
@attr('string', {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { inject as service } from '@ember/service';
|
||||
import { getOwner, setOwner } from '@ember/application';
|
||||
import { generateMQLExpression } from '../utils/mql-query';
|
||||
import { paginateResults } from '../utils/paginate-results';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
/**
|
||||
* Not all types are yet supported by the cache daemon so we'll
|
||||
@@ -94,12 +95,14 @@ export default class CacheDaemonHandler {
|
||||
const sessionData = this.session.data?.authenticated;
|
||||
const auth_token_id = sessionData?.id;
|
||||
const token = sessionData?.token;
|
||||
const resourceName = resourceNames[type];
|
||||
|
||||
remainingQuery = {
|
||||
...remainingQuery,
|
||||
query: searchQuery,
|
||||
auth_token_id,
|
||||
token,
|
||||
resource: resourceNames[type],
|
||||
resource: resourceName,
|
||||
};
|
||||
|
||||
let cacheDaemonResults = {};
|
||||
@@ -130,7 +133,7 @@ export default class CacheDaemonHandler {
|
||||
}
|
||||
} else {
|
||||
__electronLog?.error(
|
||||
'Failed to search cache daemon; falling back to search controller',
|
||||
`Failed to search cache daemon for ${resourceName}. Error:`,
|
||||
e.message,
|
||||
);
|
||||
|
||||
@@ -138,10 +141,7 @@ export default class CacheDaemonHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Currently returns with a singular top level field with resource name
|
||||
// e.g. { targets: [...] } or { sessions: [..] }
|
||||
// So this just unwraps to the array, or undefined
|
||||
const [results] = Object.values(cacheDaemonResults);
|
||||
const results = cacheDaemonResults[underscore(resourceName)];
|
||||
const payload = { items: paginateResults(results, page, pageSize) };
|
||||
|
||||
const schema = store.modelFor(type);
|
||||
@@ -167,7 +167,16 @@ export default class CacheDaemonHandler {
|
||||
// This isn't conventional but is better than returning an ArrayProxy
|
||||
// or EmberArray since the ember store query method asserts it has to be an array
|
||||
// so we can't just return an object.
|
||||
records.meta = { totalItems: results?.length ?? 0 };
|
||||
// Also include whether the results are incomplete or is still refreshing
|
||||
// as part of the response
|
||||
records.meta = {
|
||||
totalItems: results?.length ?? 0,
|
||||
isLoadIncomplete: cacheDaemonResults.incomplete ?? false,
|
||||
// Sometimes the refresh status returns an error status if it takes too long but it's still refreshing
|
||||
isCacheRefreshing:
|
||||
cacheDaemonResults.refresh_status !== 'not-refreshing',
|
||||
};
|
||||
|
||||
return records;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { registerDeprecationHandler } from '@ember/debug';
|
||||
|
||||
const deprecationList = ['ember-data:deprecate-early-static'];
|
||||
|
||||
export function initialize() {
|
||||
registerDeprecationHandler((message, options, next) => {
|
||||
// TODO: Remove deprecation handling for `deprecate-early-static` warning
|
||||
// once we upgrade to ember-cli-mirage v3.0.0 which fixes this issue.
|
||||
// More info about this issue here: https://github.com/miragejs/ember-cli-mirage/issues/2441
|
||||
if (options?.id && deprecationList.includes(options.id)) {
|
||||
return;
|
||||
}
|
||||
if (options?.url) {
|
||||
message += options.url;
|
||||
}
|
||||
next(message, options);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -19,9 +19,18 @@ export const TYPES_HOST_CATALOG_PLUGIN = [
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
];
|
||||
|
||||
export const TYPE_CREDENTIAL_STATIC = 'static-credential';
|
||||
export const TYPE_CREDENTIAL_DYNAMIC = 'dynamic-credential';
|
||||
|
||||
export const TYPES_CREDENTIALS = Object.freeze([
|
||||
TYPE_CREDENTIAL_STATIC,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
]);
|
||||
|
||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
// =attributes
|
||||
|
||||
#credentialType;
|
||||
/**
|
||||
* True if the host catalog is static.
|
||||
* @type {boolean}
|
||||
@@ -38,6 +47,29 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||
return this.type === 'plugin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from credentialType
|
||||
* @type {string}
|
||||
*/
|
||||
get credentialType() {
|
||||
if (!this.#credentialType) {
|
||||
if (this.role_arn) {
|
||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
||||
} else {
|
||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
||||
}
|
||||
}
|
||||
return this.#credentialType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value to credentialType
|
||||
* @type {string}
|
||||
*/
|
||||
set credentialType(type) {
|
||||
this.#credentialType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the host catalog is an unknown type.
|
||||
* @type {boolean}
|
||||
|
||||
@@ -4,29 +4,59 @@
|
||||
*/
|
||||
|
||||
import ApplicationSerializer from './application';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
} from '../models/host-catalog';
|
||||
|
||||
const fieldsByType = {
|
||||
aws: [
|
||||
'disable_credential_rotation',
|
||||
'region',
|
||||
// These fields are Azure specific
|
||||
const azureFields = [
|
||||
'disable_credential_rotation',
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'subscription_id',
|
||||
'secret_id',
|
||||
'secret_value',
|
||||
];
|
||||
|
||||
// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs]
|
||||
const AWSfieldsWithCredentialType = {
|
||||
'static-credential': [
|
||||
'access_key_id',
|
||||
'secret_access_key',
|
||||
],
|
||||
azure: [
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'subscription_id',
|
||||
'secret_id',
|
||||
'secret_value',
|
||||
'worker_filter',
|
||||
],
|
||||
'dynamic-credential': [
|
||||
'role_arn',
|
||||
'role_external_id',
|
||||
'role_session_name',
|
||||
'role_tags',
|
||||
'region',
|
||||
'disable_credential_rotation',
|
||||
'worker_filter',
|
||||
],
|
||||
};
|
||||
|
||||
export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
// =methods
|
||||
serialize(snapshot) {
|
||||
const { compositeType, credentialType } = snapshot.record;
|
||||
const serialized = super.serialize(...arguments);
|
||||
switch (snapshot.record.compositeType) {
|
||||
|
||||
// This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS
|
||||
// Ideally we should refactor the model to include the compositeType using `for` attribute
|
||||
if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) {
|
||||
delete serialized.worker_filter;
|
||||
}
|
||||
|
||||
// By default, disable credential rotation for dynamic credentials
|
||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
serialized.attributes.disable_credential_rotation = true;
|
||||
}
|
||||
|
||||
switch (compositeType) {
|
||||
case 'static':
|
||||
return this.serializeStatic(...arguments);
|
||||
default:
|
||||
@@ -36,21 +66,30 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const { isPlugin, compositeType } = snapshot.record;
|
||||
const { compositeType, credentialType, isPlugin } = snapshot.record;
|
||||
const { options } = attribute;
|
||||
// Nested conditionals for readability
|
||||
// This deletes any fields that don't belong to the record type
|
||||
|
||||
const fields =
|
||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE
|
||||
? azureFields
|
||||
: AWSfieldsWithCredentialType[credentialType];
|
||||
|
||||
// Delete any nested attribute fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedAttribute && json.attributes) {
|
||||
// The key must be included in the fieldsByType list above
|
||||
if (!fieldsByType[compositeType].includes(key))
|
||||
if (!fields.includes(key)) {
|
||||
delete json.attributes[key];
|
||||
}
|
||||
// This deletes any fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedSecret && json.secrets) {
|
||||
if (!fieldsByType[compositeType].includes(key)) delete json.secrets[key];
|
||||
}
|
||||
}
|
||||
|
||||
// new one for isNestedSecret
|
||||
// Delete any secret fields that don't belong to the record type
|
||||
if (isPlugin && options.isNestedSecret && json.secrets) {
|
||||
if (!fields.includes(key)) {
|
||||
delete json.secrets[key];
|
||||
if (json['secrets'] && Object.keys(json.secrets).length === 0) {
|
||||
delete json.secrets;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -59,6 +98,7 @@ export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||
// Delete unnecessary fields for static host-catalog
|
||||
delete serialized.attributes;
|
||||
delete serialized.secrets;
|
||||
delete serialized.worker_filter;
|
||||
return serialized;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ export const modelIndexes = {
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
|
||||
'auth-method':
|
||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
|
||||
'session-recording':
|
||||
'&id, attributes.created_time, attributes.type, attributes.state, attributes.start_time, attributes.end_time, attributes.duration, attributes.scope.scope_id, attributes.create_time_values.user.id, attributes.create_time_values.user.name, attributes.create_time_values.target.id, attributes.create_time_values.target.name, attributes.create_time_values.target.scope.id, attributes.create_time_values.target.scope.name',
|
||||
alias:
|
||||
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
|
||||
};
|
||||
|
||||
export const formatDbName = (userId, clusterUrl) =>
|
||||
@@ -109,7 +113,7 @@ export default class IndexedDbService extends Service {
|
||||
}
|
||||
|
||||
this.#db = new Dexie(dbName);
|
||||
this.#db.version(1).stores(modelIndexes);
|
||||
this.#db.version(2).stores(modelIndexes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,10 @@ const isTesting = environmentConfig.environment === 'test';
|
||||
export default function (mirageConfig) {
|
||||
let finalConfig = {
|
||||
...mirageConfig,
|
||||
models: { ...discoverEmberDataModels(), ...mirageConfig.models },
|
||||
models: {
|
||||
...discoverEmberDataModels(mirageConfig.store),
|
||||
...mirageConfig.models,
|
||||
},
|
||||
serializers: applyEmberDataSerializers(mirageConfig.serializers),
|
||||
routes,
|
||||
};
|
||||
@@ -775,18 +778,7 @@ function routes() {
|
||||
});
|
||||
|
||||
// session recordings
|
||||
this.get(
|
||||
'/session-recordings',
|
||||
(
|
||||
{ sessionRecordings },
|
||||
{ queryParams: { scope_id: scopeId, recursive } },
|
||||
) => {
|
||||
if (recursive && scopeId === 'global') {
|
||||
return sessionRecordings.all();
|
||||
}
|
||||
return sessionRecordings.where({ scopeId });
|
||||
},
|
||||
);
|
||||
this.get('/session-recordings');
|
||||
this.get(
|
||||
'/session-recordings/:idMethod',
|
||||
async ({ sessionRecordings }, { params: { idMethod } }) => {
|
||||
|
||||
@@ -38,6 +38,12 @@ export default factory.extend({
|
||||
};
|
||||
},
|
||||
|
||||
worker_filter: function () {
|
||||
if (this.type === 'plugin' && this.plugin?.name === 'aws') {
|
||||
return `"${faker.word.noun()}" in "${faker.system.directoryPath()}"`;
|
||||
}
|
||||
},
|
||||
|
||||
plugin: function (i) {
|
||||
if (this.type === 'plugin') {
|
||||
return {
|
||||
|
||||
@@ -190,6 +190,27 @@ export default function initializeMockIPC(server, config) {
|
||||
isClientAgentRunning() {
|
||||
return false;
|
||||
}
|
||||
cacheDaemonStatus() {
|
||||
return { version: 'Boundary v1.0.0' };
|
||||
}
|
||||
getCliVersion() {
|
||||
return { versionNumber: '1.0.0' };
|
||||
}
|
||||
getDesktopVersion() {
|
||||
return { desktopVersion: '1.0.0' };
|
||||
}
|
||||
getLogLevel() {
|
||||
return 'info';
|
||||
}
|
||||
getLogPath() {
|
||||
return '~/.config/Boundary/logs/desktop-client.log';
|
||||
}
|
||||
setLogLevel() {}
|
||||
clientAgentStatus() {
|
||||
return { version: '0.0.1-dev', status: 'running' };
|
||||
}
|
||||
pauseClientAgent() {}
|
||||
resumeClientAgent() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-10
@@ -26,35 +26,36 @@
|
||||
"dexie": "^3.2.4",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-can": "^4.2.0",
|
||||
"ember-cli-babel": "^7.26.10",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-mirage": "^2.4.0",
|
||||
"ember-cli-mirage": "^3.0.3",
|
||||
"miragejs": "^0.1.48",
|
||||
"ember-data": "~4.12.0",
|
||||
"ember-fetch": "^8.1.1",
|
||||
"tracked-built-ins": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-cli": "^4.12.1",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"ember-cli": "^4.12.2",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-exam": "^8.0.0",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -64,13 +65,12 @@
|
||||
"eslint-plugin-ember": "^11.11.1",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-qunit": "^8.0.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"js-bexpr": "hashicorp/js-bexpr#9b4a4b54d85eba67fdfc0990133d1518d890b1e1",
|
||||
"loader.js": "^4.7.0",
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"webpack": "^5.78.0"
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": ">4.0.0"
|
||||
|
||||
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupIntl(hooks, 'en-us'); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from 'dummy/config/environment';
|
||||
import { initialize } from 'dummy/initializers/deprecations';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Initializer | deprecations', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
};
|
||||
|
||||
this.TestApplication.initializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
});
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
run(this.application, 'destroy');
|
||||
});
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', async function (assert) {
|
||||
await this.application.boot();
|
||||
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
TYPE_CREDENTIAL_STATIC,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
} from 'api/models/host-catalog';
|
||||
|
||||
module('Unit | Serializer | host catalog', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -31,13 +36,103 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws plugin as expected, ignoring azure fields', async function (assert) {
|
||||
test('it serializes a new aws static plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: 'aws',
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
credentialType: TYPE_CREDENTIAL_STATIC,
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
region: 'west',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
// these are Azure fields and should be excluded
|
||||
tenant_id: 'a1b2c3',
|
||||
client_id: 'a1b2c3',
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_tags: [
|
||||
{ key: 'Project', value: 'Automation' },
|
||||
{ key: 'foo', value: 'bar' },
|
||||
],
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'west',
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws dynamic plugin as expected', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
name: 'AWS',
|
||||
credentialType: TYPE_CREDENTIAL_DYNAMIC,
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
region: 'west',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
// these are Azure fields and should be excluded
|
||||
tenant_id: 'a1b2c3',
|
||||
client_id: 'a1b2c3',
|
||||
subscription_id: 'a1b2c3',
|
||||
secret_id: 'a1b2c3',
|
||||
secret_value: 'a1b2c3',
|
||||
role_arn: 'test',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_tags: [
|
||||
{ key: 'Project', value: 'Automation' },
|
||||
{ key: 'foo', value: 'bar' },
|
||||
],
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'west',
|
||||
role_external_id: 'test',
|
||||
role_session_name: 'test',
|
||||
role_arn: 'test',
|
||||
role_tags: { Project: 'Automation', foo: 'bar' },
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it serializes a new aws static plugin as expected, ignoring azure fields', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('host-catalog', {
|
||||
compositeType: TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||
name: 'AWS',
|
||||
credentialType: TYPE_CREDENTIAL_STATIC,
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
disable_credential_rotation: true,
|
||||
worker_filter: 'workerfilter',
|
||||
// these are AWS fields and should be included
|
||||
region: 'spain',
|
||||
access_key_id: 'foobars',
|
||||
@@ -53,6 +148,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
name: 'AWS',
|
||||
description: 'this is a Aws plugin host-catalog',
|
||||
type: 'plugin',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: true,
|
||||
region: 'spain',
|
||||
@@ -75,6 +171,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
region: 'spain',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
worker_filter: 'workerfilter',
|
||||
// these are Azure fields and should be excluded
|
||||
tenant_id: 'a1b2c3',
|
||||
client_id: 'a1b2c3',
|
||||
@@ -106,6 +203,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'host-catalog',
|
||||
credentialType: 'static',
|
||||
attributes: {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
@@ -113,6 +211,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
plugin: {
|
||||
name: 'aws',
|
||||
},
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'andorra',
|
||||
},
|
||||
},
|
||||
@@ -122,6 +221,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
type: 'plugin',
|
||||
name: 'aws',
|
||||
description: 'test description',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: false,
|
||||
region: 'andorra',
|
||||
@@ -215,6 +315,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
name: 'aws',
|
||||
description: 'aws host catalog',
|
||||
},
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
disable_credential_rotation: false,
|
||||
region: 'Illinois',
|
||||
@@ -238,6 +339,7 @@ module('Unit | Serializer | host catalog', function (hooks) {
|
||||
type: 'plugin',
|
||||
description: 'Test description',
|
||||
authorized_actions: ['no-op'],
|
||||
worker_filter: 'workerfilter',
|
||||
plugin: {
|
||||
id: 'plugin-id-5',
|
||||
name: 'aws',
|
||||
|
||||
@@ -11,6 +11,7 @@ import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
import { camelize } from '@ember/string';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const seedIndexDb = async (resource, store, indexedDb, server) => {
|
||||
const resourceData =
|
||||
@@ -529,4 +530,80 @@ module('Unit | Utility | indexed-db-query', function (hooks) {
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
|
||||
test('it filters on date type fields and returns results', async function (assert) {
|
||||
const pastDate = new Date('2024-09-19T10:00:00.000Z');
|
||||
const createdTime = new Date('2024-09-23T10:00:00.000Z');
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime,
|
||||
});
|
||||
await seedIndexDb('session-recording', store, indexedDb, this.server);
|
||||
const query = { filters: { created_time: [{ gte: pastDate }] } };
|
||||
|
||||
const result = await queryIndexedDb(
|
||||
indexedDb.db,
|
||||
'session-recording',
|
||||
query,
|
||||
);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
});
|
||||
|
||||
test('it filters on date type fields and returns results - from and to', async function (assert) {
|
||||
const from = new Date('2024-09-19T10:00:00.000Z');
|
||||
const to = new Date('2024-09-25T10:00:00.000Z');
|
||||
const createdTime = new Date('2024-09-23T10:00:00.000Z');
|
||||
const createdTime2 = new Date('2024-09-28T10:00:00.000Z');
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime,
|
||||
});
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime2,
|
||||
});
|
||||
await seedIndexDb('session-recording', store, indexedDb, this.server);
|
||||
const query = {
|
||||
filters: {
|
||||
created_time: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ gte: from }, { lte: to }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await queryIndexedDb(
|
||||
indexedDb.db,
|
||||
'session-recording',
|
||||
query,
|
||||
);
|
||||
|
||||
assert.strictEqual(result.length, 1);
|
||||
assert.strictEqual(
|
||||
result[0].attributes.created_time.toISOString(),
|
||||
createdTime.toISOString(),
|
||||
);
|
||||
});
|
||||
|
||||
test('it filters on date type fields and returns no results', async function (assert) {
|
||||
const createdTime = faker.date.between({
|
||||
from: '2024-09-19T00:00:00.000Z',
|
||||
to: '2024-09-20T00:00:00.000Z',
|
||||
});
|
||||
const pastDate = faker.date.between({
|
||||
from: '2024-09-23T00:00:00.000Z',
|
||||
to: '2024-09-24T00:00:00.000Z',
|
||||
});
|
||||
this.server.create('session-recording', {
|
||||
created_time: createdTime,
|
||||
});
|
||||
await seedIndexDb('session-recording', store, indexedDb, this.server);
|
||||
const query = { filters: { created_time: [{ gte: pastDate }] } };
|
||||
|
||||
const result = await queryIndexedDb(
|
||||
indexedDb.db,
|
||||
'session-recording',
|
||||
query,
|
||||
);
|
||||
|
||||
assert.strictEqual(result.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import SimpleAuthBaseAuthenticator from 'ember-simple-auth/authenticators/base';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
import { waitForPromise } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* Encapsulates common authenticator functionality.
|
||||
@@ -58,10 +58,14 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
*/
|
||||
async validateToken(token, tokenID) {
|
||||
const tokenValidationURL = this.buildTokenValidationEndpointURL(tokenID);
|
||||
const response = await fetch(tokenValidationURL, {
|
||||
method: 'get',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
|
||||
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
|
||||
const response = await waitForPromise(
|
||||
fetch(tokenValidationURL, {
|
||||
method: 'get',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
);
|
||||
// 401 and 404 responses mean the token is invalid, whereas other types of
|
||||
// error responses do not tell us about the validity of the token.
|
||||
if (response.status === 401 || response.status === 404) return reject();
|
||||
@@ -103,7 +107,7 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts to the `deauthEntpoint` on a best-effort basis and then returns.
|
||||
* Posts to the `deauthEndpoint` on a best-effort basis and then returns.
|
||||
* Deauthentication with the server is not guaranteed and request failures
|
||||
* are ignored.
|
||||
*
|
||||
@@ -113,12 +117,14 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
async invalidate(options) {
|
||||
const { token } = options;
|
||||
const deauthEndpointURL = this.buildDeauthEndpointURL(options);
|
||||
await fetch(deauthEndpointURL, {
|
||||
method: 'delete',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).catch(() => {
|
||||
/* no op */
|
||||
});
|
||||
await waitForPromise(
|
||||
fetch(deauthEndpointURL, {
|
||||
method: 'delete',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).catch(() => {
|
||||
/* no op */
|
||||
}),
|
||||
);
|
||||
return super.invalidate(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import BaseAuthenticator from './base';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
import { waitForPromise } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
* The OIDC base authenticator encapsulates the multistep OIDC flow.
|
||||
@@ -47,7 +47,9 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
async startAuthentication(options) {
|
||||
const url = this.buildAuthEndpointURL(options);
|
||||
const body = JSON.stringify({ command: 'start' });
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
|
||||
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
|
||||
const response = await waitForPromise(fetch(url, { method: 'post', body }));
|
||||
const json = await response.json();
|
||||
if (response.status < 400) {
|
||||
// Store meta about the pending OIDC flow
|
||||
@@ -94,7 +96,7 @@ export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
},
|
||||
});
|
||||
// Fetch the endpoint and get the response JSON
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
const response = await waitForPromise(fetch(url, { method: 'post', body }));
|
||||
if (response.status === 202) {
|
||||
// The token isn't ready yet, keep trying.
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import BaseAuthenticator from './base';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
import { waitForPromise } from '@ember/test-waiters';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -66,7 +66,11 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
attributes: { login_name, password },
|
||||
});
|
||||
const authEndpointURL = this.buildAuthEndpointURL(options);
|
||||
const response = await fetch(authEndpointURL, { method: 'post', body });
|
||||
// Note: waitForPromise is needed to provide the necessary integration with @ember/test-helpers
|
||||
// visit https://www.npmjs.com/package/@ember/test-waiters for more info.
|
||||
const response = await waitForPromise(
|
||||
fetch(authEndpointURL, { method: 'post', body }),
|
||||
);
|
||||
const json = await response.json();
|
||||
return response.status < 400
|
||||
? resolve(this.normalizeData(json, login_name))
|
||||
|
||||
@@ -21,9 +21,8 @@
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"ember-cli-babel": "^7.26.11",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-fetch": "^8.1.1",
|
||||
"ember-simple-auth": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -31,7 +30,8 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@ember/test-waiters": "^3.1.0",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
@@ -40,7 +40,7 @@
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-cli": "^4.12.2",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"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",
|
||||
@@ -49,7 +49,7 @@
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -64,7 +64,7 @@
|
||||
"prettier": "^3.0.0",
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"webpack": "^5.78.0"
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.* || 20.*"
|
||||
|
||||
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupIntl(hooks, 'en-us'); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,10 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Button
|
||||
...attributes
|
||||
@style='inline-link-action'
|
||||
@iconLeft={{if
|
||||
this.isLoading
|
||||
'flight-icons/svg/loading-16'
|
||||
'flight-icons/svg/reload-16'
|
||||
}}
|
||||
class={{if this.isLoading 'loading-button is-loading' 'loading-button'}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.refresh'}}
|
||||
@color='secondary'
|
||||
@icon={{if this.isLoading 'loading' 'reload'}}
|
||||
@disabled={{this.isLoading}}
|
||||
{{on 'click' this.toggleRefresh}}
|
||||
>
|
||||
{{yield}}
|
||||
</Rose::Button>
|
||||
/>
|
||||
@@ -3,12 +3,17 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if this.lastRefreshed}}
|
||||
<Rose::Toolbar::Info>
|
||||
{{relative-datetime-live this.lastRefreshed}}
|
||||
</Rose::Toolbar::Info>
|
||||
{{/if}}
|
||||
|
||||
<LoadingButton @onClick={{this.onClick}}>
|
||||
{{t 'actions.refresh'}}
|
||||
</LoadingButton>
|
||||
<div class='toolbar-refresher'>
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{else}}
|
||||
{{#if this.lastRefreshed}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{relative-datetime-live this.lastRefreshed}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<LoadingButton @onClick={{this.onClick}}>
|
||||
{{t 'actions.refresh'}}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@ export default class ToolbarRefresherComponent extends Component {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Executes the click handler argument and trackes the time it completed.
|
||||
* Executes the click handler argument and tracks the time it completed.
|
||||
*/
|
||||
@action
|
||||
async onClick() {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
|
||||
import { DurationFormat } from '@formatjs/intl-durationformat';
|
||||
import { secondsToDuration } from 'core/utils/seconds-to-duration';
|
||||
|
||||
export default class extends Helper {
|
||||
// =services
|
||||
@@ -19,12 +20,10 @@ export default class extends Helper {
|
||||
*/
|
||||
compute([durationInMs]) {
|
||||
const durationInSeconds = durationInMs / 1000;
|
||||
const durationFormat = new DurationUnitFormat(this.intl.primaryLocale, {
|
||||
const durationFormat = new DurationFormat(this.intl.primaryLocale, {
|
||||
style: 'narrow',
|
||||
format: '{days} {hours} {minutes} {seconds}',
|
||||
round: true,
|
||||
});
|
||||
|
||||
return durationFormat.format(durationInSeconds);
|
||||
return durationFormat.format(secondsToDuration(durationInSeconds));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
|
||||
import { DurationFormat } from '@formatjs/intl-durationformat';
|
||||
import { secondsToDuration } from 'core/utils/seconds-to-duration';
|
||||
|
||||
export default class extends Helper {
|
||||
// =services
|
||||
@@ -29,14 +30,13 @@ export default class extends Helper {
|
||||
difference > 0 ? Math.floor(difference / 1000) : 0;
|
||||
|
||||
// Format the duration
|
||||
const duration = new DurationUnitFormat(this.intl.primaryLocale, {
|
||||
style: 'timer',
|
||||
format: `{hours}:{minutes}:{seconds} ${this.intl.t(
|
||||
'resources.session.remaining',
|
||||
)}`,
|
||||
const duration = new DurationFormat(this.intl.primaryLocale, {
|
||||
style: 'digital',
|
||||
});
|
||||
const formattedTime = duration.format(
|
||||
secondsToDuration(differenceInSeconds),
|
||||
);
|
||||
|
||||
// Return the formatted duration
|
||||
return duration.format(differenceInSeconds);
|
||||
return `${formattedTime} ${this.intl.t('resources.session.remaining')}`;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.filter-tag {
|
||||
@@ -27,33 +26,20 @@
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.hds-segmented-group {
|
||||
> div,
|
||||
table {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hds-segmented-group {
|
||||
.hds-dropdown-toggle-button__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.hds-table {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.search-filtering-toolbar {
|
||||
display: flex;
|
||||
|
||||
// TODO: Remove these copied styles by migrating to HDS for the toolbar refresher component.
|
||||
// The original styles assumed we were using the old toolbar component under a "rose-toolbar" class.
|
||||
.rose-button-inline-link-action {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.8461538462;
|
||||
font-weight: 600;
|
||||
color: var(--stark);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
@@ -67,3 +53,15 @@
|
||||
color: var(--token-color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-refresher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
gap: 1rem;
|
||||
|
||||
> .hds-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Takes in the number of seconds and converts to an object with corresponding
|
||||
* values for each time duration. For example:
|
||||
* { weeks: 0, days: 0, hours: 22, minutes: 15, seconds: 11 }
|
||||
* @param {number} value
|
||||
* @returns {object}
|
||||
*/
|
||||
export const secondsToDuration = (value) => {
|
||||
const second = 1;
|
||||
const minute = second * 60;
|
||||
const hour = minute * 60;
|
||||
const day = hour * 24;
|
||||
const week = day * 7;
|
||||
|
||||
const weeks = Math.max(Math.floor(value / week), 0);
|
||||
const days = Math.max(Math.floor((value % week) / day), 0);
|
||||
const hours = Math.max(Math.floor((value % day) / hour), 0);
|
||||
const minutes = Math.max(Math.floor((value % hour) / minute), 0);
|
||||
const seconds = Math.max(Math.floor((value % minute) / second), 0);
|
||||
|
||||
return { weeks, days, hours, minutes, seconds };
|
||||
};
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default, initialize } from 'api/initializers/deprecations';
|
||||
export { default } from 'core/utils/seconds-to-duration';
|
||||
@@ -31,19 +31,19 @@
|
||||
"doc:toc": "doctoc README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-durationformat": "^0.2.4",
|
||||
"api": "*",
|
||||
"auth": "*",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-browser-services": "^4.0.3",
|
||||
"ember-cli-babel": "^7.26.11",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-sass": "^11.0.1",
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-intl": "^6.0.0-beta.6",
|
||||
"ember-intl": "^7.0.5",
|
||||
"ember-loading": "^2.0.0",
|
||||
"ember-truth-helpers": "^3.0.0",
|
||||
"filesize": "^10.0.7",
|
||||
"intl-unofficial-duration-unit-format": "^3.1.0",
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -51,7 +51,7 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@embroider/test-setup": "^2.1.1",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
@@ -60,14 +60,14 @@
|
||||
"concurrently": "^8.0.1",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-cli": "^4.12.2",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"ember-cli-dependency-checker": "^3.3.2",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-inline-svg": "^1.0.1",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -84,8 +84,8 @@
|
||||
"qunit": "^2.19.4",
|
||||
"qunit-dom": "^2.0.0",
|
||||
"rose": "*",
|
||||
"sinon": "^17.0.1",
|
||||
"webpack": "^5.78.0"
|
||||
"sinon": "^18.0.0",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": "^4.0.0",
|
||||
|
||||
@@ -28,7 +28,7 @@ function setupApplicationTest(hooks, options) {
|
||||
// This is also a good place to call test setup functions coming
|
||||
// from other addons:
|
||||
//
|
||||
// setupIntl(hooks); // ember-intl
|
||||
// setupIntl(hooks, 'en-us'); // ember-intl
|
||||
// setupMirage(hooks); // ember-cli-mirage
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | dropdown/index', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
const firstItem = {
|
||||
id: '1',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Component | error/message', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders predefined message for known error status', async function (assert) {
|
||||
await render(hbs`<Error::Message @status='401' />`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | filter-tags/index', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
this.set('filter', {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
module('Integration | Component | form | authenticate', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it shows login name and password when LDAP is selected from the list', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
@@ -7,19 +7,22 @@ import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find, click } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Component | loading-button', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`<LoadingButton />`);
|
||||
assert.ok(find('.loading-button'));
|
||||
assert.ok(find('.hds-button'));
|
||||
});
|
||||
|
||||
test('it executes a function on refresh button click', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.onClick = () => assert.ok(true, 'refresh was clicked');
|
||||
await render(hbs`<LoadingButton @onClick={{this.onClick}} />`);
|
||||
assert.ok(find('.loading-button'));
|
||||
assert.ok(find('.hds-button'));
|
||||
await click('button');
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | format-day-year', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders correctly when it is 1 year', async function (assert) {
|
||||
this.set('inputValue', 365);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | format-time-duration', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('It renders in human-readable duration strings given different durations in milliseconds', async function (assert) {
|
||||
// Set mock input (in milliseconds) durations
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | relative-datetime-live', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
let now;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | reverse-indexed-display-name', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders the resource name with its position in parent array reversed', async function (assert) {
|
||||
this.set('array', ['Boundary', 'is', 'cool!']);
|
||||
|
||||
@@ -11,11 +11,9 @@ import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | time-remaining', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
|
||||
test('it calculates the correct time remaining', async function (assert) {
|
||||
assert.expect(1);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it calculates the correct time remainin with less than 24 hours', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() + 1000 * 60 * 60 * 24));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
@@ -23,9 +21,29 @@ module('Integration | Helper | time-remaining', function (hooks) {
|
||||
assert.strictEqual(this.element.textContent.trim(), '23:59:59 remaining');
|
||||
});
|
||||
|
||||
test('it handles negative time remaining', async function (assert) {
|
||||
assert.expect(1);
|
||||
test('it calculates the correct time remaining with more than 24 hours', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() + 1001 * 60 * 60 * 24));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
|
||||
assert.strictEqual(
|
||||
this.element.textContent.trim(),
|
||||
'1 day, 0:01:26 remaining',
|
||||
);
|
||||
});
|
||||
|
||||
test('it calculates the correct time remaining with more than a week', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() + 1001 * 60 * 60 * 24 * 8));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
|
||||
assert.strictEqual(
|
||||
this.element.textContent.trim(),
|
||||
'1 wk, 1 day, 0:11:31 remaining',
|
||||
);
|
||||
});
|
||||
|
||||
test('it handles negative time remaining', async function (assert) {
|
||||
this.set('expirationTime', new Date(Date.now() - 1000 * 60 * 60 * 24));
|
||||
|
||||
await render(hbs`{{time-remaining this.expirationTime}}`);
|
||||
|
||||
@@ -22,7 +22,7 @@ const listWithOneItem = [{ key: 1, value: 'boundary' }];
|
||||
const limit = 3;
|
||||
module('Integration | Helper | truncate-list', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it renders correctly when there is list with more than the limit specificed', async function (assert) {
|
||||
this.set('inputValue', list);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { secondsToDuration } from 'core/utils/seconds-to-duration';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | seconds-to-duration', function () {
|
||||
test('it handles 0 correctly', function (assert) {
|
||||
const result = secondsToDuration(0);
|
||||
|
||||
assert.strictEqual(result.seconds, 0);
|
||||
assert.strictEqual(result.minutes, 0);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts seconds correctly', function (assert) {
|
||||
const result = secondsToDuration(45);
|
||||
|
||||
assert.strictEqual(result.seconds, 45);
|
||||
assert.strictEqual(result.minutes, 0);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts minutes correctly', function (assert) {
|
||||
const result = secondsToDuration(100);
|
||||
|
||||
assert.strictEqual(result.seconds, 40);
|
||||
assert.strictEqual(result.minutes, 1);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts hours correctly', function (assert) {
|
||||
const result = secondsToDuration(5000);
|
||||
|
||||
assert.strictEqual(result.seconds, 20);
|
||||
assert.strictEqual(result.minutes, 23);
|
||||
assert.strictEqual(result.hours, 1);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts days correctly', function (assert) {
|
||||
const result = secondsToDuration(500000);
|
||||
|
||||
assert.strictEqual(result.seconds, 20);
|
||||
assert.strictEqual(result.minutes, 53);
|
||||
assert.strictEqual(result.hours, 18);
|
||||
assert.strictEqual(result.days, 5);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
|
||||
test('it converts weeks correctly', function (assert) {
|
||||
const result = secondsToDuration(800000);
|
||||
|
||||
assert.strictEqual(result.seconds, 20);
|
||||
assert.strictEqual(result.minutes, 13);
|
||||
assert.strictEqual(result.hours, 6);
|
||||
assert.strictEqual(result.days, 2);
|
||||
assert.strictEqual(result.weeks, 1);
|
||||
});
|
||||
|
||||
test('it handles negative values', function (assert) {
|
||||
const result = secondsToDuration(-800000);
|
||||
|
||||
assert.strictEqual(result.seconds, 0);
|
||||
assert.strictEqual(result.minutes, 0);
|
||||
assert.strictEqual(result.hours, 0);
|
||||
assert.strictEqual(result.days, 0);
|
||||
assert.strictEqual(result.weeks, 0);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,8 @@ back: Back
|
||||
view: View
|
||||
retry: Retry
|
||||
play: Play
|
||||
pause: Pause
|
||||
resume: Resume
|
||||
get-topic-help: Get help for this topic
|
||||
copy-to-clipboard: Copy to Clipboard
|
||||
copy-error-detail-to-clipboard: Copy error detail to clipboard
|
||||
@@ -60,3 +62,5 @@ yes: Yes
|
||||
no: No
|
||||
narrow-results: Narrow results
|
||||
overflow-options: Overflow Options
|
||||
show-errors: Show Errors
|
||||
hide-errors: Hide Errors
|
||||
@@ -105,3 +105,31 @@ unsupported-controller:
|
||||
releases-page: Go to releases page
|
||||
change-cluster-url: Change cluster URL
|
||||
error: Unable to download supported version of desktop app. You’ll need to download version 1.7.1 from the releases page
|
||||
settings:
|
||||
application: Application
|
||||
cli: CLI
|
||||
cache-daemon: Cache Daemon
|
||||
client-agent:
|
||||
title: Boundary Client Agent
|
||||
description: Required to use Transparent Sessions
|
||||
client: Client
|
||||
version: Version
|
||||
color-theme: Color Theme
|
||||
provider: Provider
|
||||
hcp: HashiCorp Cloud Platform
|
||||
self-managed: Self-managed
|
||||
server:
|
||||
title: Server
|
||||
description: Boundary connects you to targets using this server managed by your organization.
|
||||
logs:
|
||||
title: Logs
|
||||
logging-level: Logging level
|
||||
log-location: Log location
|
||||
levels:
|
||||
error: Error
|
||||
warn: Warn
|
||||
info: Info
|
||||
debug: Debug
|
||||
alerts:
|
||||
cache-daemon: There may be a problem with the cache daemon
|
||||
client-agent: There may be a problem with the client agent
|
||||
@@ -212,12 +212,25 @@ host-catalog:
|
||||
help:
|
||||
static: Provide a list of hosts and host sets with static addresses
|
||||
plugin: Automate discovery of target hosts and services on Azure or AWS
|
||||
role_arn: The ARN (Amazon Resource Name) used by this instance.
|
||||
disable_credential_rotation: Credential rotation is automatically disabled when you use dynamic credentials.
|
||||
types:
|
||||
static: Static
|
||||
unknown: Plugin
|
||||
aws: AWS
|
||||
azure: Azure
|
||||
plugin: Dynamic
|
||||
credential:
|
||||
label: Credential type
|
||||
help: Select the credential type that you want to use.
|
||||
static-credential:
|
||||
edit: Static Credentials
|
||||
label: Use an access key (Static Credentials)
|
||||
help: Use an access key that you have generated in AWS to use with this AWS Host Catalog.
|
||||
dynamic-credential:
|
||||
edit: Dynamic Credentials
|
||||
label: Use Assume Role (Dynamic Credentials)
|
||||
help: Specify a role used to generate credentials to use with this AWS Host Catalog.
|
||||
form:
|
||||
aws_region:
|
||||
label: AWS Region
|
||||
@@ -225,6 +238,9 @@ host-catalog:
|
||||
azure_client_id:
|
||||
label: Client/Application ID
|
||||
help: The client (application) ID of an Azure service principal that Boundary will use to authenticate and discover hosts from Azure.
|
||||
worker_filter:
|
||||
label: Worker Filter
|
||||
help: Insert a worker filter in order to route requests to the correct worker.
|
||||
host-set:
|
||||
title: Host Set
|
||||
title_plural: Host Sets
|
||||
@@ -357,6 +373,12 @@ session-recording:
|
||||
duration: '{time} duration'
|
||||
questions:
|
||||
delete: Are you sure you want to delete this recording?
|
||||
filters:
|
||||
time:
|
||||
title: Time
|
||||
last-twenty-four-hours: Last 24 hours
|
||||
last-three-days: Last 3 days
|
||||
last-seven-days: Last 7 days
|
||||
connection:
|
||||
title: Connection
|
||||
title_index: Connection {index}
|
||||
@@ -384,9 +406,9 @@ session-recording:
|
||||
messages:
|
||||
none:
|
||||
title: Channel Playback
|
||||
missing:
|
||||
title: We can't play back this channel because the file is missing
|
||||
description: This likely means the connected storage bucket is misconfigured or the file was deleted.
|
||||
error:
|
||||
title: Playback error
|
||||
description: This may mean the connected storage bucket is misconfigured, the file was deleted, or integrity validation failed.
|
||||
link: Retrieving lost session recordings
|
||||
not-supported:
|
||||
title: Playback is not supported for this channel
|
||||
@@ -439,6 +461,7 @@ storage-bucket:
|
||||
plugin-types:
|
||||
aws: Amazon S3
|
||||
minio: MinIO
|
||||
unknown: Unknown
|
||||
form:
|
||||
scope:
|
||||
label: Scope
|
||||
@@ -447,7 +470,6 @@ storage-bucket:
|
||||
help_org: This storage bucket will be available for use with targets within this org.
|
||||
provider:
|
||||
label: Provider
|
||||
help: Boundary currently only supports Amazon S3 storage.
|
||||
endpoint_url:
|
||||
label: Endpoint URL
|
||||
help: Cannot be changed later.
|
||||
@@ -459,7 +481,7 @@ storage-bucket:
|
||||
help: The base path where session recordings will be stored.
|
||||
role_arn:
|
||||
label: Role ARN
|
||||
help: The ARN (Assume Role Name) used by this instance that the worker runs on.
|
||||
help: The ARN (Amazon Resource Name) used by this instance that the worker runs on.
|
||||
role_external_id:
|
||||
label: Role external ID
|
||||
help: Used to provide access to this account if you have this configured.
|
||||
@@ -615,6 +637,10 @@ role:
|
||||
help: Customize which scopes are associated with this role.
|
||||
view-projects: '{selected}/{total} total projects'
|
||||
apply: Apply changes to org
|
||||
remove-org-and-projects: Remove org and projects
|
||||
remove-org-only: Remove just the org
|
||||
remove-orgs-and-projects: Remove orgs and projects
|
||||
remove-orgs-only: Remove just the orgs
|
||||
form:
|
||||
this:
|
||||
label: Add this scope
|
||||
@@ -627,6 +653,12 @@ role:
|
||||
descendants:
|
||||
label: Add all descendants
|
||||
help: Add all of the orgs and the projects in the orgs that are underneath this scope.
|
||||
remove-org:
|
||||
title: Remove this org?
|
||||
description: You're about to remove {orgDisplayName}. Do you want to remove the projects under {orgDisplayName} as well? Or do you just want to remove {orgDisplayName} from this project?
|
||||
remove-all-orgs:
|
||||
title: Remove all selected orgs?
|
||||
description: You're about to remove all the selected orgs. Do you want to remove the projects under these orgs as well? Or do you just want to remove just the orgs?
|
||||
target:
|
||||
title: Target
|
||||
title_plural: Targets
|
||||
@@ -689,7 +721,7 @@ target:
|
||||
help: The local proxy port on which to listen by default when a session is started on a client.
|
||||
sidebar:
|
||||
label: Session Recording
|
||||
help: Record SSH sessions on this target and save them to an Amazon S3 bucket.
|
||||
help: Record SSH sessions on this target and save them to a storage bucket.
|
||||
link: Session Recording settings
|
||||
questions:
|
||||
delete-host-sources:
|
||||
@@ -873,7 +905,7 @@ credential-library:
|
||||
help: The body of the HTTP request the library sends to Vault when requesting credentials. Only allowed when HTTP method is set to POST.
|
||||
username:
|
||||
label: Username
|
||||
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principles when making the request to Vault.
|
||||
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principals when making the request to Vault.
|
||||
key_type:
|
||||
label: Key Type
|
||||
help: Specifies the desired key type to use when generating a private key.
|
||||
|
||||
@@ -14,7 +14,18 @@ active-public: Public
|
||||
recording: Recording
|
||||
completed: Completed
|
||||
failed: Failed
|
||||
installed: Installed
|
||||
not-running: Not Running
|
||||
running: Running
|
||||
paused: Paused
|
||||
authenticated: Authenticated
|
||||
unauthenticated: Unauthenticated
|
||||
loading:
|
||||
title: Loading all items...
|
||||
description: All of your {resource} are still loading. Please wait while we finish loading everything on screen.
|
||||
generic-description: All of your resources are still loading. Please wait while we finish loading everything on screen.
|
||||
incomplete-loading:
|
||||
limit: Loaded the first {resultCount} results. Please narrow down your search or filters if the result you're looking for is not shown.
|
||||
refreshing:
|
||||
description: Updating cache...
|
||||
tooltip: Some items may not appear until the cache is finished updating
|
||||
@@ -33,7 +33,7 @@
|
||||
"@hashicorp/design-system-components": "^4.4.1",
|
||||
"codemirror": "5.65.7",
|
||||
"ember-auto-import": "^2.6.3",
|
||||
"ember-cli-babel": "^7.26.11",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-clipboard": "^1.0.0",
|
||||
"ember-cli-htmlbars": "^6.2.0",
|
||||
"ember-cli-sass": "^11.0.1",
|
||||
@@ -46,12 +46,12 @@
|
||||
"sass": "^1.69.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
"@ember/string": "^3.0.1",
|
||||
"@ember/test-helpers": "^2.9.3",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@glimmer/component": "^1.1.2",
|
||||
"@glimmer/tracking": "^1.1.2",
|
||||
"babel-loader": "^8.2.5",
|
||||
@@ -61,7 +61,7 @@
|
||||
"concurrently": "^8.0.1",
|
||||
"doctoc": "^2.2.0",
|
||||
"ember-cli": "^4.12.2",
|
||||
"ember-cli-dependency-checker": "^3.3.1",
|
||||
"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",
|
||||
@@ -73,7 +73,7 @@
|
||||
"ember-maybe-import-regenerator": "^1.0.0",
|
||||
"ember-modifier": "^3.2.7",
|
||||
"ember-page-title": "^7.0.0",
|
||||
"ember-qunit": "^6.2.0",
|
||||
"ember-qunit": "^8.1.0",
|
||||
"ember-resolver": "^10.0.0",
|
||||
"ember-source": "~4.12.0",
|
||||
"ember-source-channel-url": "^3.0.0",
|
||||
@@ -84,7 +84,7 @@
|
||||
"eslint-plugin-ember": "^11.11.1",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-qunit": "^8.0.1",
|
||||
"jest": "^27.4.7",
|
||||
"jest": "^29.7.0",
|
||||
"loader.js": "^4.7.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-prettier-scss": "^0.0.1",
|
||||
"stylelint-config-standard-scss": "^11.0.0",
|
||||
"webpack": "^5.78.0"
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ember-source": "^4.0.0"
|
||||
|
||||
+9
-6
@@ -38,21 +38,22 @@
|
||||
"test:ui-desktop": "yarn --cwd ui/desktop test",
|
||||
"compliance:licenses": "license-checker --onlyAllow 'Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;BUSL-1.1;Public Domain;Python-2.0;Unicode-TOU;Unlicense;WTFPL' --excludePackages 'boundary-ui;admin@0.0.0;desktop@0.0.0;base64-js@0.0.2;cycle@1.0.3;cldr-core@43.1.0;make-plural@7.2.0;@hashicorp/ember-asciinema-player@0.0.0;path-scurry@1.10.1'",
|
||||
"doc:toc": "doctoc README.md",
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky install",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cz-conventional-changelog": "^3.1.0",
|
||||
"doctoc": "^2.2.0",
|
||||
"git-cz": "^4.8.0",
|
||||
"git-cz": "^4.9.0",
|
||||
"husky": "^8.0.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"lint-staged": "^13.0.2"
|
||||
"lint-staged": "^13.0.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"postinstall-postinstall": "^2.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"request": "^2.88.0",
|
||||
"loader-utils": "^2.0.4",
|
||||
"**/ember-electron/**/minimatch": "^3.0.5",
|
||||
"**/babel-core/json5": "^2.2.2",
|
||||
"**/react-syntax-highlighter/refractor/prismjs": "^1.27.0",
|
||||
"**/nomnom/underscore": "^1.12.1",
|
||||
"**/remark-parse/trim": "^1.0.1",
|
||||
@@ -72,7 +73,9 @@
|
||||
"**/ember-cli/**/ansi-html": "^0.0.9",
|
||||
"**/ember-cli/testem/fireworm/async": "^2.6.4",
|
||||
"**/desktop/@doyensec/electronegativity/winston/async": "^2.6.4",
|
||||
"**/@hashicorp/design-system-components/ember-stargate": "^0.6.0"
|
||||
"**/@hashicorp/design-system-components/ember-stargate": "^0.6.0",
|
||||
"micromatch": "^4.0.8",
|
||||
"send": "^0.19.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
diff --git a/node_modules/lodash.template/index.js b/node_modules/lodash.template/index.js
|
||||
index f051141..0c45a8d 100644
|
||||
--- a/node_modules/lodash.template/index.js
|
||||
+++ b/node_modules/lodash.template/index.js
|
||||
@@ -17,6 +17,9 @@ var HOT_COUNT = 800,
|
||||
var INFINITY = 1 / 0,
|
||||
MAX_SAFE_INTEGER = 9007199254740991;
|
||||
|
||||
+/** Error message constants. */
|
||||
+var INVALID_TEMPL_VAR_ERROR_TEXT = 'Invalid `variable` option passed into `_.template`';
|
||||
+
|
||||
/** `Object#toString` result references. */
|
||||
var argsTag = '[object Arguments]',
|
||||
arrayTag = '[object Array]',
|
||||
@@ -1343,6 +1346,18 @@ function keysIn(object) {
|
||||
return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
|
||||
}
|
||||
|
||||
+/**
|
||||
+ * Used to validate the `validate` option in `_.template` variable.
|
||||
+ *
|
||||
+ * Forbids characters which could potentially change the meaning of the function argument definition:
|
||||
+ * - "()," (modification of function parameters)
|
||||
+ * - "=" (default value)
|
||||
+ * - "[]{}" (destructuring of function parameters)
|
||||
+ * - "/" (beginning of a comment)
|
||||
+ * - whitespace
|
||||
+ */
|
||||
+var reForbiddenIdentifierChars = /[()=,{}\[\]\/\s]/;
|
||||
+
|
||||
/**
|
||||
* Creates a compiled template function that can interpolate data properties
|
||||
* in "interpolate" delimiters, HTML-escape interpolated data properties in
|
||||
@@ -1522,6 +1537,11 @@ function template(string, options, guard) {
|
||||
if (!variable) {
|
||||
source = 'with (obj) {\n' + source + '\n}\n';
|
||||
}
|
||||
+ // Throw an error if a forbidden character was found in `variable`, to prevent
|
||||
+ // potential command injection attacks.
|
||||
+ else if (reForbiddenIdentifierChars.test(variable)) {
|
||||
+ throw new Error(INVALID_TEMPL_VAR_ERROR_TEXT);
|
||||
+ }
|
||||
// Cleanup code by stripping empty strings.
|
||||
source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)
|
||||
.replace(reEmptyStringMiddle, '$1')
|
||||
@@ -64,5 +64,19 @@ module.exports = {
|
||||
'qunit/require-expect': [2, 'except-simple'],
|
||||
},
|
||||
},
|
||||
{
|
||||
// e2e files
|
||||
files: ['tests/e2e/**'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
rules: {
|
||||
'no-empty-pattern': [
|
||||
'error',
|
||||
{ allowObjectPatternsAsParameters: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Admin UI
|
||||
|
||||
The admin UI for Boundary.
|
||||
The Admin UI for Boundary.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
@@ -39,8 +39,8 @@ List of available project commands. `yarn run <command-name>`
|
||||
|
||||
| Command | Description |
|
||||
| ------- | ----------- |
|
||||
| build:development | Builds the UI in development mode. |
|
||||
| build | Builds the UI for production. |
|
||||
| build:development | Builds the Admin UI in development mode. |
|
||||
| build | Builds the Admin UI for production. |
|
||||
| lint | Runs all lint commands. |
|
||||
| lint:hbs | Lints `hbs` template files. |
|
||||
| lint:js | Lints `js` files. |
|
||||
|
||||
+7
-12
@@ -4,17 +4,12 @@
|
||||
}}
|
||||
|
||||
{{#if (can 'delete account' @model)}}
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<Hds::Dropdown data-test-manage-account-auth-methods as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.account.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.account.actions.delete'}}
|
||||
</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
/>
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
@@ -40,55 +40,42 @@
|
||||
</Rose::Dropdown>
|
||||
{{/if}}
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<Hds::Dropdown data-test-manage-auth-methods-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
{{#if @model.isPrimary}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
{{on 'click' (fn @removeAsPrimary @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{else}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.auth-method.actions.make-primary'}}
|
||||
{{on 'click' (fn @makePrimary @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.make-primary'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'create model' @model collection='accounts')}}
|
||||
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.account.actions.create'}}
|
||||
@route='scopes.scope.auth-methods.auth-method.accounts.new'
|
||||
@model={{@model}}
|
||||
>
|
||||
{{t 'resources.account.actions.create'}}
|
||||
</dropdown.link>
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (can 'create model' @model collection='managed-groups')}}
|
||||
{{#if (or @model.isOIDC @model.isLDAP)}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.managed-group.actions.create'}}
|
||||
@route='scopes.scope.auth-methods.auth-method.managed-groups.new'
|
||||
@model={{@model}}
|
||||
>
|
||||
{{t 'resources.managed-group.actions.create'}}
|
||||
</dropdown.link>
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (can 'delete auth-method' @model)}}
|
||||
<dropdown.separator />
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Separator />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.auth-method.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.delete'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</Hds::Dropdown>
|
||||
+7
-12
@@ -4,17 +4,12 @@
|
||||
}}
|
||||
|
||||
{{#if (can 'delete model' @model)}}
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<Hds::Dropdown data-test-managed-group-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.managed-group.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.managed-group.actions.delete'}}
|
||||
</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
/>
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
@@ -2,37 +2,26 @@
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<Hds::Dropdown data-test-manage-credential-stores-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
{{#if (can 'create model' @model collection='credential-libraries')}}
|
||||
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.credential-stores.credential-store.credential-libraries.new'
|
||||
@model={{@model}}
|
||||
>
|
||||
{{t 'resources.credential-library.actions.create'}}
|
||||
</dropdown.link>
|
||||
@text={{t 'resources.credential-library.actions.create'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'create model' @model collection='credentials')}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.credential-stores.credential-store.credentials.new'
|
||||
@model={{@model}}
|
||||
>
|
||||
{{t 'resources.credential.actions.create'}}
|
||||
</dropdown.link>
|
||||
@text={{t 'resources.credential.actions.create'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'delete model' @model)}}
|
||||
<dropdown.separator />
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Separator />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.credential-store.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' @delete}}
|
||||
>
|
||||
{{t 'resources.credential-store.actions.delete'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</Hds::Dropdown>
|
||||
@@ -19,12 +19,18 @@
|
||||
{{on 'input' (set-from-event @model 'name')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.name.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
|
||||
<F.Label>
|
||||
{{t 'form.name.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.name.help'}}
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.name}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
@@ -38,21 +44,28 @@
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.description.label'}}</F.Label>
|
||||
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
|
||||
<F.Label>
|
||||
{{t 'form.description.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.description.help'}}
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.description}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
<E.Message>{{error.message}}</E.Message>
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Textarea::Field>
|
||||
|
||||
{{#if @model.isNew}}
|
||||
|
||||
<Hds::Form::RadioCard::Group @name='type' as |G|>
|
||||
<G.Legend>{{t 'form.type.label'}}</G.Legend>
|
||||
<G.Legend>
|
||||
{{t 'form.type.label'}}
|
||||
</G.Legend>
|
||||
{{#each this.hostCatalogTypes as |hostCatalogType|}}
|
||||
<G.RadioCard
|
||||
@value={{hostCatalogType}}
|
||||
@@ -61,19 +74,25 @@
|
||||
{{on 'input' (fn @changeType hostCatalogType)}}
|
||||
as |R|
|
||||
>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' hostCatalogType)
|
||||
}}</R.Label>
|
||||
<R.Description>{{t
|
||||
(concat 'resources.host-catalog.help.' hostCatalogType)
|
||||
}}</R.Description>
|
||||
<R.Label>
|
||||
{{t (concat 'resources.host-catalog.types.' hostCatalogType)}}
|
||||
</R.Label>
|
||||
<R.Description>
|
||||
{{t (concat 'resources.host-catalog.help.' hostCatalogType)}}
|
||||
</R.Description>
|
||||
</G.RadioCard>
|
||||
{{/each}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
<Hds::Form::RadioCard::Group @name='plugin_type' @alignment='center' as |G|>
|
||||
<G.Legend>{{t 'titles.provider'}}</G.Legend>
|
||||
<G.Legend>{{t 'titles.choose-a-provider'}}</G.Legend>
|
||||
<G.HelperText>{{t 'descriptions.choose-a-provider'}}</G.HelperText>
|
||||
<G.Legend>
|
||||
{{t 'titles.provider'}}
|
||||
</G.Legend>
|
||||
<G.Legend>
|
||||
{{t 'titles.choose-a-provider'}}
|
||||
</G.Legend>
|
||||
<G.HelperText>
|
||||
{{t 'descriptions.choose-a-provider'}}
|
||||
</G.HelperText>
|
||||
|
||||
{{#each-in this.mapResourceTypeWithIcon as |pluginType icon|}}
|
||||
<G.RadioCard
|
||||
@@ -83,11 +102,10 @@
|
||||
{{on 'input' (fn @changeType pluginType)}}
|
||||
as |R|
|
||||
>
|
||||
<R.Label>{{t
|
||||
(concat 'resources.host-catalog.types.' pluginType)
|
||||
}}</R.Label>
|
||||
<R.Label>
|
||||
{{t (concat 'resources.host-catalog.types.' pluginType)}}
|
||||
</R.Label>
|
||||
<R.Icon @name={{icon}} />
|
||||
|
||||
</G.RadioCard>
|
||||
{{/each-in}}
|
||||
</Hds::Form::RadioCard::Group>
|
||||
@@ -98,8 +116,11 @@
|
||||
readonly={{true}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'titles.provider'}}</F.Label>
|
||||
<F.HelperText>{{t 'descriptions.provider'}}
|
||||
<F.Label>
|
||||
{{t 'titles.provider'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'descriptions.provider'}}
|
||||
</F.HelperText>
|
||||
</InfoField>
|
||||
{{/if}}
|
||||
@@ -112,7 +133,9 @@
|
||||
{{on 'input' (set-from-event @model 'region')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'resources.host-catalog.form.aws_region.label'}}</F.Label>
|
||||
<F.Label>
|
||||
{{t 'resources.host-catalog.form.aws_region.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.form.aws_region.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws.region'}}>
|
||||
@@ -121,50 +144,289 @@
|
||||
</F.HelperText>
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='access_key_id'
|
||||
@value={{@model.access_key_id}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'access_key_id')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.access_key_id.label'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.access_key_id.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
</F.HelperText>
|
||||
</Hds::Form::TextInput::Field>
|
||||
{{#if @model.isNew}}
|
||||
<Hds::Form::Radio::Group
|
||||
class='dynamic-credential-selection'
|
||||
@layout='vertical'
|
||||
@name='credential_type'
|
||||
as |G|
|
||||
>
|
||||
<G.Legend>
|
||||
{{t 'resources.host-catalog.types.credential.label'}}
|
||||
</G.Legend>
|
||||
<G.HelperText>
|
||||
{{t 'resources.host-catalog.types.credential.help'}}
|
||||
</G.HelperText>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='secret_access_key'
|
||||
@value={{@model.secret_access_key}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'secret_access_key')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.secret_access_key.label'}}</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.secret_access_key.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
</F.HelperText>
|
||||
</Hds::Form::TextInput::Field>
|
||||
{{#each this.credentials as |credentialType|}}
|
||||
<G.RadioField
|
||||
name={{credentialType}}
|
||||
@value={{credentialType}}
|
||||
checked={{eq credentialType this.selectedCredentialType}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (fn this.updateCredentialTypeSelection credentialType)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.'
|
||||
credentialType
|
||||
'.label'
|
||||
)
|
||||
}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.'
|
||||
credentialType
|
||||
'.help'
|
||||
)
|
||||
}}
|
||||
</F.HelperText>
|
||||
</G.RadioField>
|
||||
{{/each}}
|
||||
</Hds::Form::Radio::Group>
|
||||
{{else}}
|
||||
<Hds::Form::Fieldset @layout='horizontal' as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.host-catalog.types.credential.label'}}
|
||||
</F.Legend>
|
||||
<F.Control>
|
||||
<Hds::Badge
|
||||
@text={{t
|
||||
(concat
|
||||
'resources.host-catalog.types.credential.'
|
||||
this.selectedCredentialType
|
||||
'.edit'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</F.Control>
|
||||
</Hds::Form::Fieldset>
|
||||
{{/if}}
|
||||
|
||||
<Hds::Form::Checkbox::Field
|
||||
name='disable_credential_rotation'
|
||||
checked={{@model.disable_credential_rotation}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'change' (fn this.toggleDisableCredentialRotation @model)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{t 'form.disable_credential_rotation.label'}}</F.Label>
|
||||
</Hds::Form::Checkbox::Field>
|
||||
{{#if this.showDynamicCredentials}}
|
||||
{{! role_arn }}
|
||||
<Hds::Form::TextInput::Field
|
||||
name='role_arn'
|
||||
@isRequired={{true}}
|
||||
@value={{@model.role_arn}}
|
||||
@isInvalid={{@model.errors.role_arn}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'role_arn')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'resources.storage-bucket.form.role_arn.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.help.role_arn'}}
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.role_arn}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.role_arn as |error|}}
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{! role_external_id }}
|
||||
<Hds::Form::TextInput::Field
|
||||
name='role_external_id'
|
||||
@isOptional={{true}}
|
||||
@value={{@model.role_external_id}}
|
||||
@isInvalid={{@model.errors.role_external_id}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'role_external_id')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'resources.storage-bucket.form.role_external_id.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.role_external_id.help'}}
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.role_external_id}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.role_external_id as |error|}}
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{! role_session_name }}
|
||||
<Hds::Form::TextInput::Field
|
||||
name='role_session_name'
|
||||
@isOptional={{true}}
|
||||
@value={{@model.role_session_name}}
|
||||
@isInvalid={{@model.errors.role_session_name}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'role_session_name')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'resources.storage-bucket.form.role_session_name.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.role_session_name.help'}}
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.role_session_name}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.role_session_name as |error|}}
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
{{! role_tags }}
|
||||
<Form::Field::ListWrapper
|
||||
@layout='horizontal'
|
||||
@isOptional={{true}}
|
||||
@disabled={{form.disabled}}
|
||||
>
|
||||
<:fieldset as |F|>
|
||||
<F.Legend>
|
||||
{{t 'resources.storage-bucket.form.role_tags.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.role_tags.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.role_tags}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.role_tags as |error|}}
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<F.KeyValue
|
||||
@name='role_tags'
|
||||
@options={{@model.role_tags}}
|
||||
@model={{@model}}
|
||||
>
|
||||
<:key as |K|>
|
||||
<K.text />
|
||||
</:key>
|
||||
<:value as |V|>
|
||||
<V.text />
|
||||
</:value>
|
||||
</F.KeyValue>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
|
||||
{{else}}
|
||||
<Hds::Form::TextInput::Field
|
||||
name='access_key_id'
|
||||
@value={{@model.access_key_id}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'access_key_id')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'form.access_key_id.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.access_key_id.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
</F.HelperText>
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<Hds::Form::TextInput::Field
|
||||
name='secret_access_key'
|
||||
@value={{@model.secret_access_key}}
|
||||
@isRequired={{true}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'input' (set-from-event @model 'secret_access_key')}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'form.secret_access_key.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'form.secret_access_key.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'host-catalog.aws'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
</F.HelperText>
|
||||
</Hds::Form::TextInput::Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if (feature-flag 'dynamic-credentials-worker-filter')}}
|
||||
<Hds::Form::Textarea::Field
|
||||
data-test-dynamic-credential-worker-filter
|
||||
name='worker_filter'
|
||||
@value={{@model.worker_filter}}
|
||||
@isOptional={{true}}
|
||||
@isInvalid={{@model.errors.worker_filter}}
|
||||
disabled={{form.disabled}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'resources.host-catalog.form.worker_filter.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.form.worker_filter.help'}}
|
||||
<Hds::Link::Inline @href={{doc-url 'worker.create-tags'}}>
|
||||
{{t 'actions.learn-more'}}
|
||||
</Hds::Link::Inline>
|
||||
</F.HelperText>
|
||||
{{#if @model.errors.worker_filter}}
|
||||
<F.Error as |E|>
|
||||
{{#each @model.errors.worker_filter as |error|}}
|
||||
<E.Message>
|
||||
{{error.message}}
|
||||
</E.Message>
|
||||
{{/each}}
|
||||
</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Textarea::Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showDynamicCredentials}}
|
||||
<Hds::Form::Checkbox::Field
|
||||
name='disable_credential_rotation'
|
||||
checked={{true}}
|
||||
disabled={{true}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'form.disable_credential_rotation.label'}}
|
||||
</F.Label>
|
||||
<F.HelperText>
|
||||
{{t 'resources.host-catalog.help.disable_credential_rotation'}}
|
||||
</F.HelperText>
|
||||
</Hds::Form::Checkbox::Field>
|
||||
{{else}}
|
||||
<Hds::Form::Checkbox::Field
|
||||
name='disable_credential_rotation'
|
||||
checked={{@model.disable_credential_rotation}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'change' (fn this.toggleDisableCredentialRotation @model)}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'form.disable_credential_rotation.label'}}
|
||||
</F.Label>
|
||||
</Hds::Form::Checkbox::Field>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if (can 'save model' @model)}}
|
||||
|
||||
@@ -8,7 +8,10 @@ import { action } from '@ember/object';
|
||||
import {
|
||||
TYPES_HOST_CATALOG,
|
||||
TYPES_HOST_CATALOG_PLUGIN,
|
||||
TYPES_CREDENTIALS,
|
||||
TYPE_CREDENTIAL_DYNAMIC,
|
||||
} from 'api/models/host-catalog';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
//Note: this is a temporary solution till we have resource type helper in place
|
||||
const icons = ['aws-color', 'azure-color'];
|
||||
@@ -16,6 +19,21 @@ const icons = ['aws-color', 'azure-color'];
|
||||
export default class FormHostCatalogAwsComponent extends Component {
|
||||
// =properties
|
||||
hostCatalogTypes = TYPES_HOST_CATALOG;
|
||||
|
||||
// =attributes
|
||||
@tracked selectedCredentialType = this.args.model.credentialType;
|
||||
|
||||
@tracked showDynamicCredentials =
|
||||
this.args.model.credentialType === TYPE_CREDENTIAL_DYNAMIC;
|
||||
|
||||
/**
|
||||
* returns an array of available credential types
|
||||
* @type {array}
|
||||
*/
|
||||
get credentials() {
|
||||
return TYPES_CREDENTIALS;
|
||||
}
|
||||
|
||||
/**
|
||||
* maps resource type with icon
|
||||
* @type {object}
|
||||
@@ -32,4 +50,19 @@ export default class FormHostCatalogAwsComponent extends Component {
|
||||
toggleDisableCredentialRotation(model) {
|
||||
model.disable_credential_rotation = !model.disable_credential_rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to update the credential type
|
||||
* @param type {string}
|
||||
*/
|
||||
@action
|
||||
updateCredentialTypeSelection(type) {
|
||||
this.selectedCredentialType = type;
|
||||
if (type === TYPE_CREDENTIAL_DYNAMIC) {
|
||||
this.showDynamicCredentials = true;
|
||||
} else {
|
||||
this.showDynamicCredentials = false;
|
||||
}
|
||||
this.args.changeCredentialType(type);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@
|
||||
submit=@submit
|
||||
cancel=@cancel
|
||||
changeType=@changeType
|
||||
changeCredentialType=@changeCredentialType
|
||||
}}
|
||||
@@ -7,6 +7,7 @@
|
||||
class='full-width'
|
||||
@cancel={{@cancel}}
|
||||
@onSubmit={{this.submit}}
|
||||
@disabled={{this.isSaving}}
|
||||
as |form|
|
||||
>
|
||||
<div class='create-resources-form'>
|
||||
|
||||
@@ -9,12 +9,26 @@ import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class FormOnboardingComponent extends Component {
|
||||
// =properties
|
||||
|
||||
@tracked targetAddress;
|
||||
@tracked targetAddressInvalid = false;
|
||||
@tracked targetPort;
|
||||
@tracked targetPortInvalid = false;
|
||||
|
||||
/**
|
||||
* Returns true if any of the resources that are being created are still saving.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSaving() {
|
||||
const { org, project, target, role } = this.args.model;
|
||||
return org.isSaving || project.isSaving || target.isSaving || role.isSaving;
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Passes in target address and port to submit function for further proccessing.
|
||||
*/
|
||||
@action
|
||||
submit() {
|
||||
// Check targetAddress and targetPort are valid before submit
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{#if @model.totalItemsCount}}
|
||||
<Rose::Form
|
||||
class='full-width role-manage-custom-scopes-form'
|
||||
@onSubmit={{fn @submit this.allGrantScopes}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.role.isSaving}}
|
||||
as |form|
|
||||
@@ -21,17 +21,17 @@
|
||||
/>
|
||||
</Hds::SegmentedGroup>
|
||||
{{#if @model.orgScopes}}
|
||||
{{#if this.selectedItems.length}}
|
||||
<div class='table-selected-items-text'>
|
||||
<div class='table-selected-items-text'>
|
||||
{{#if @model.role.grantScopeOrgIDs.length}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{t
|
||||
'resources.role.scope.titles.selected'
|
||||
items=this.selectedItems.length
|
||||
items=@model.role.grantScopeOrgIDs.length
|
||||
total=@model.totalItemsCount
|
||||
}}
|
||||
</Hds::Text::Body>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::Table
|
||||
@columns={{array
|
||||
(hash label=(t 'resources.role.scope.title'))
|
||||
@@ -46,7 +46,7 @@
|
||||
<:body as |B|>
|
||||
<B.Tr
|
||||
@selectionKey={{B.data.id}}
|
||||
@isSelected={{includes B.data.id this.selectedItems}}
|
||||
@isSelected={{includes B.data.id @model.role.grantScopeOrgIDs}}
|
||||
@selectionAriaLabelSuffix='row {{B.data.id}}'
|
||||
data-test-org-scopes-table-row={{B.data.id}}
|
||||
>
|
||||
@@ -69,7 +69,7 @@
|
||||
@iconPosition='trailing'
|
||||
@text={{t
|
||||
'resources.role.scope.actions.view-projects'
|
||||
selected=projectTotals.selected
|
||||
selected=projectTotals.selected.length
|
||||
total=projectTotals.total
|
||||
}}
|
||||
@route='scopes.scope.roles.role.manage-scopes.manage-org-projects'
|
||||
@@ -127,4 +127,63 @@
|
||||
/>
|
||||
</A.Footer>
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.selectedOrg}}
|
||||
<Hds::Modal
|
||||
@onClose={{this.toggleRemoveOrgModal}}
|
||||
data-test-manage-scopes-remove-org-modal
|
||||
as |M|
|
||||
>
|
||||
<M.Header>{{t 'resources.role.scope.remove-org.title'}}</M.Header>
|
||||
<M.Body>
|
||||
<Hds::Text::Body @tag='p' @color='primary'>
|
||||
{{t
|
||||
'resources.role.scope.remove-org.description'
|
||||
orgDisplayName=this.orgDisplayName
|
||||
}}
|
||||
</Hds::Text::Body>
|
||||
</M.Body>
|
||||
<M.Footer as |F|>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{t 'resources.role.scope.actions.remove-org-and-projects'}}
|
||||
{{on 'click' (fn this.removeProjects false)}}
|
||||
/>
|
||||
<Hds::Button
|
||||
@color='secondary'
|
||||
@text={{t 'resources.role.scope.actions.remove-org-only'}}
|
||||
{{on 'click' F.close}}
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</M.Footer>
|
||||
</Hds::Modal>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.selectedOrgs}}
|
||||
<Hds::Modal
|
||||
@onClose={{this.toggleRemoveAllModal}}
|
||||
data-test-manage-scopes-remove-all-orgs-modal
|
||||
as |M|
|
||||
>
|
||||
<M.Header>{{t 'resources.role.scope.remove-all-orgs.title'}}</M.Header>
|
||||
<M.Body>
|
||||
<Hds::Text::Body @tag='p' @color='primary'>
|
||||
{{t 'resources.role.scope.remove-all-orgs.description'}}
|
||||
</Hds::Text::Body>
|
||||
</M.Body>
|
||||
<M.Footer as |F|>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{t 'resources.role.scope.actions.remove-orgs-and-projects'}}
|
||||
{{on 'click' (fn this.removeProjects true)}}
|
||||
/>
|
||||
<Hds::Button
|
||||
@color='secondary'
|
||||
@text={{t 'resources.role.scope.actions.remove-orgs-only'}}
|
||||
{{on 'click' F.close}}
|
||||
/>
|
||||
</Hds::ButtonSet>
|
||||
</M.Footer>
|
||||
</Hds::Modal>
|
||||
{{/if}}
|
||||
@@ -10,14 +10,18 @@ import { tracked } from '@glimmer/tracking';
|
||||
export default class FormRoleManageCustomScopesIndexComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
@tracked selectedItems = [...this.args.model.role.grantScopeOrgIDs];
|
||||
@tracked selectedOrgs = [];
|
||||
@tracked selectedOrg = '';
|
||||
|
||||
get allGrantScopes() {
|
||||
return [
|
||||
...this.args.model.role.grantScopeKeywords,
|
||||
...this.args.model.role.grantScopeProjectIDs,
|
||||
...this.selectedItems,
|
||||
];
|
||||
/**
|
||||
* Returns the display name of the selectedOrg.
|
||||
* @type {string}
|
||||
*/
|
||||
get orgDisplayName() {
|
||||
const org = this.args.model.orgScopes.find(
|
||||
({ id }) => id === this.selectedOrg,
|
||||
);
|
||||
return org.displayName;
|
||||
}
|
||||
|
||||
// =actions
|
||||
@@ -25,19 +29,84 @@ export default class FormRoleManageCustomScopesIndexComponent extends Component
|
||||
/**
|
||||
* Handles the selection changes for the paginated table.
|
||||
* @param {object} selectableRowsStates
|
||||
* @param {object} selectionKey
|
||||
*/
|
||||
@action
|
||||
selectionChange({ selectableRowsStates }) {
|
||||
selectableRowsStates.forEach((row) => {
|
||||
const { isSelected, selectionKey: key } = row;
|
||||
const includesId = this.selectedItems.includes(key);
|
||||
if (isSelected) {
|
||||
if (!includesId) this.selectedItems = [...this.selectedItems, key];
|
||||
} else if (includesId) {
|
||||
this.selectedItems = this.selectedItems.filter(
|
||||
(item) => item !== key,
|
||||
);
|
||||
selectionChange({ selectableRowsStates, selectionKey }) {
|
||||
const { role, projectTotals } = this.args.model;
|
||||
|
||||
const addOrRemoveValues = (add, remove, orgId) => {
|
||||
let selectedOrg;
|
||||
const includesId = role.grant_scope_ids.includes(orgId);
|
||||
const selected = projectTotals[orgId]?.selected;
|
||||
if (add && !includesId) {
|
||||
role.grant_scope_ids = [...role.grant_scope_ids, orgId];
|
||||
} else if (remove && includesId) {
|
||||
if (selected?.length) {
|
||||
selectedOrg = orgId;
|
||||
}
|
||||
role.grant_scope_ids = role.grant_scope_ids.filter(
|
||||
(item) => item !== orgId,
|
||||
);
|
||||
}
|
||||
return selectedOrg;
|
||||
};
|
||||
|
||||
if (selectionKey === 'all') {
|
||||
const selectedOrgs = [];
|
||||
selectableRowsStates.forEach(({ selectionKey, isSelected }) => {
|
||||
const selectedOrg = addOrRemoveValues(
|
||||
isSelected,
|
||||
!isSelected,
|
||||
selectionKey,
|
||||
);
|
||||
if (selectedOrg) {
|
||||
selectedOrgs.push(selectedOrg);
|
||||
}
|
||||
});
|
||||
this.selectedOrgs = selectedOrgs;
|
||||
} else {
|
||||
this.selectedOrg = addOrRemoveValues(true, true, selectionKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes projects from an org that was deselected and toggles the correct modal off.
|
||||
* @param {boolean} toggleRemoveAllModal
|
||||
*/
|
||||
@action
|
||||
removeProjects(toggleRemoveAllModal) {
|
||||
const selectedOrgs = toggleRemoveAllModal
|
||||
? this.selectedOrgs
|
||||
: [this.selectedOrg];
|
||||
const { role, projectTotals } = this.args.model;
|
||||
selectedOrgs.forEach((orgId) => {
|
||||
const { selected, total } = projectTotals[orgId];
|
||||
role.grant_scope_ids = role.grant_scope_ids.filter(
|
||||
(item) => !selected.includes(item),
|
||||
);
|
||||
projectTotals[orgId] = { selected: [], total };
|
||||
});
|
||||
if (toggleRemoveAllModal) {
|
||||
this.toggleRemoveAllModal();
|
||||
} else {
|
||||
this.toggleRemoveOrgModal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the modal to remove an org and projects off.
|
||||
*/
|
||||
@action
|
||||
toggleRemoveOrgModal() {
|
||||
this.selectedOrg = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the modal to remove orgs and projects off.
|
||||
*/
|
||||
@action
|
||||
toggleRemoveAllModal() {
|
||||
this.selectedOrgs = [];
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
{{#if @model.totalItemsCount}}
|
||||
<Rose::Form
|
||||
class='full-width role-manage-custom-scopes-form'
|
||||
@onSubmit={{fn @submit this.allGrantScopes}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.role.isSaving}}
|
||||
as |form|
|
||||
@@ -21,17 +21,17 @@
|
||||
/>
|
||||
</Hds::SegmentedGroup>
|
||||
{{#if @model.projectScopes}}
|
||||
{{#if this.selectedItems.length}}
|
||||
<div class='table-selected-items-text'>
|
||||
<div class='table-selected-items-text'>
|
||||
{{#if this.selectedProjectsCount}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{t
|
||||
'resources.role.scope.titles.selected'
|
||||
items=this.selectedItems.length
|
||||
items=this.selectedProjectsCount
|
||||
total=@model.totalItemsCount
|
||||
}}
|
||||
</Hds::Text::Body>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::Table
|
||||
@columns={{array
|
||||
(hash label=(t 'resources.project.title'))
|
||||
@@ -45,7 +45,7 @@
|
||||
<:body as |B|>
|
||||
<B.Tr
|
||||
@selectionKey={{B.data.id}}
|
||||
@isSelected={{includes B.data.id this.selectedItems}}
|
||||
@isSelected={{includes B.data.id @model.role.grant_scope_ids}}
|
||||
@selectionAriaLabelSuffix='row {{B.data.id}}'
|
||||
data-test-project-scopes-table-row={{B.data.id}}
|
||||
>
|
||||
|
||||
@@ -5,20 +5,13 @@
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class FormRoleManageOrgProjectsIndexComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
@tracked selectedItems = [...this.args.model.selectedProjectIDs];
|
||||
|
||||
get allGrantScopes() {
|
||||
return [
|
||||
...this.args.model.role.grantScopeKeywords,
|
||||
...this.args.model.role.grantScopeOrgIDs,
|
||||
...this.args.model.remainingProjectIDs,
|
||||
...this.selectedItems,
|
||||
];
|
||||
get selectedProjectsCount() {
|
||||
const { role, remainingProjectIDs } = this.args.model;
|
||||
return role.grantScopeProjectIDs.length - remainingProjectIDs.length;
|
||||
}
|
||||
|
||||
// =actions
|
||||
@@ -29,16 +22,16 @@ export default class FormRoleManageOrgProjectsIndexComponent extends Component {
|
||||
*/
|
||||
@action
|
||||
selectionChange({ selectableRowsStates }) {
|
||||
const { role } = this.args.model;
|
||||
selectableRowsStates.forEach((row) => {
|
||||
const { isSelected, selectionKey: key } = row;
|
||||
const includesId = this.selectedItems.includes(key);
|
||||
if (isSelected) {
|
||||
if (!includesId) this.selectedItems = [...this.selectedItems, key];
|
||||
} else {
|
||||
if (includesId)
|
||||
this.selectedItems = this.selectedItems.filter(
|
||||
(item) => item !== key,
|
||||
);
|
||||
const includesId = role.grant_scope_ids.includes(key);
|
||||
if (isSelected && !includesId) {
|
||||
role.grant_scope_ids = [...role.grant_scope_ids, key];
|
||||
} else if (!isSelected && includesId) {
|
||||
role.grant_scope_ids = role.grant_scope_ids.filter(
|
||||
(item) => item !== key,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<Rose::Form
|
||||
class='full-width role-manage-scopes-form'
|
||||
@onSubmit={{fn @submit this.selectedItems}}
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
as |form|
|
||||
@@ -14,7 +14,7 @@
|
||||
<Hds::Form::Toggle::Field
|
||||
@value={{this.keywords.keyThis}}
|
||||
name={{this.keywords.keyThis}}
|
||||
checked={{includes this.keywords.keyThis this.selectedItems}}
|
||||
checked={{includes this.keywords.keyThis @model.grant_scope_ids}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'change' this.toggleField}}
|
||||
as |F|
|
||||
@@ -31,7 +31,7 @@
|
||||
<Hds::Form::Toggle::Field
|
||||
@value={{this.keywords.keyChildren}}
|
||||
name={{this.keywords.keyChildren}}
|
||||
checked={{includes this.keywords.keyChildren this.selectedItems}}
|
||||
checked={{includes this.keywords.keyChildren @model.grant_scope_ids}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'change' this.toggleField}}
|
||||
as |F|
|
||||
@@ -48,7 +48,7 @@
|
||||
<Hds::Form::Toggle::Field
|
||||
@value={{this.keywords.keyDescendants}}
|
||||
name={{this.keywords.keyDescendants}}
|
||||
checked={{includes this.keywords.keyDescendants this.selectedItems}}
|
||||
checked={{includes this.keywords.keyDescendants @model.grant_scope_ids}}
|
||||
disabled={{form.disabled}}
|
||||
{{on 'change' this.toggleField}}
|
||||
as |F|
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import {
|
||||
GRANT_SCOPE_THIS,
|
||||
GRANT_SCOPE_CHILDREN,
|
||||
@@ -21,8 +20,6 @@ export default class FormRoleManageScopesIndexComponent extends Component {
|
||||
keyDescendants: GRANT_SCOPE_DESCENDANTS,
|
||||
};
|
||||
|
||||
@tracked selectedItems = [...this.args.model.grant_scope_ids];
|
||||
|
||||
/**
|
||||
* Returns true if role is global level and either
|
||||
* "children" or "descendants" is toggled on.
|
||||
@@ -31,8 +28,8 @@ export default class FormRoleManageScopesIndexComponent extends Component {
|
||||
get showAlert() {
|
||||
return (
|
||||
this.args.model.scope.isGlobal &&
|
||||
(this.selectedItems.includes(GRANT_SCOPE_CHILDREN) ||
|
||||
this.selectedItems.includes(GRANT_SCOPE_DESCENDANTS))
|
||||
(this.args.model.grant_scope_ids.includes(GRANT_SCOPE_CHILDREN) ||
|
||||
this.args.model.grant_scope_ids.includes(GRANT_SCOPE_DESCENDANTS))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,10 +43,15 @@ export default class FormRoleManageScopesIndexComponent extends Component {
|
||||
toggleField(event) {
|
||||
const { checked, value } = event.target;
|
||||
const removeValue = (value) => {
|
||||
this.selectedItems = this.selectedItems.filter((item) => item !== value);
|
||||
this.args.model.grant_scope_ids = this.args.model.grant_scope_ids.filter(
|
||||
(item) => item !== value,
|
||||
);
|
||||
};
|
||||
if (checked) {
|
||||
this.selectedItems = [...this.selectedItems, value];
|
||||
this.args.model.grant_scope_ids = [
|
||||
...this.args.model.grant_scope_ids,
|
||||
value,
|
||||
];
|
||||
if (value === GRANT_SCOPE_CHILDREN) {
|
||||
removeValue(GRANT_SCOPE_DESCENDANTS);
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@
|
||||
{{t 'resources.storage-bucket.form.role_tags.label'}}
|
||||
</F.Legend>
|
||||
<F.HelperText>
|
||||
{{t 'resources.storage-bucket.form.role_tags.label'}}
|
||||
{{t 'resources.storage-bucket.form.role_tags.help'}}
|
||||
</F.HelperText>
|
||||
|
||||
{{#if @model.errors.role_tags}}
|
||||
@@ -436,11 +436,18 @@
|
||||
{{/if}}
|
||||
</:fieldset>
|
||||
<:field as |F|>
|
||||
<F.TextInput
|
||||
<F.KeyValue
|
||||
@name='role_tags'
|
||||
@options={{@model.role_tags}}
|
||||
@model={{@model}}
|
||||
/>
|
||||
>
|
||||
<:key as |K|>
|
||||
<K.text />
|
||||
</:key>
|
||||
<:value as |V|>
|
||||
<V.text />
|
||||
</:value>
|
||||
</F.KeyValue>
|
||||
</:field>
|
||||
</Form::Field::ListWrapper>
|
||||
{{/if}}
|
||||
|
||||
@@ -3,35 +3,28 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<Hds::Dropdown data-test-manage-host-catalogs-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
{{#if (can 'create model' @model collection='hosts')}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.host-catalogs.host-catalog.hosts.new'
|
||||
@model={{@model}}
|
||||
>
|
||||
{{t 'resources.host.actions.create'}}
|
||||
</dropdown.link>
|
||||
@text={{t 'resources.host.actions.create'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'create model' @model collection='host-sets')}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.host-catalogs.host-catalog.host-sets.new'
|
||||
@model={{@model}}
|
||||
>
|
||||
{{t 'resources.host-set.actions.create'}}
|
||||
</dropdown.link>
|
||||
@text={{t 'resources.host-set.actions.create'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'delete model' @model)}}
|
||||
<dropdown.separator />
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Separator />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.host-catalog.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.host-catalog.actions.delete'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</Hds::Dropdown>
|
||||
+15
-21
@@ -3,31 +3,25 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<Hds::Dropdown data-test-manage-dropdown-host-sets as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
|
||||
{{#if (can 'addHosts hostSet' @model)}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.host-set.actions.create-and-add-host'}}
|
||||
@route='scopes.scope.host-catalogs.host-catalog.host-sets.host-set.create-and-add-host'
|
||||
>
|
||||
{{t 'resources.host-set.actions.create-and-add-host'}}
|
||||
</dropdown.link>
|
||||
<dropdown.link
|
||||
/>
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.host-set.actions.add-hosts'}}
|
||||
@route='scopes.scope.host-catalogs.host-catalog.host-sets.host-set.add-hosts'
|
||||
>
|
||||
{{t 'resources.host-set.actions.add-hosts'}}
|
||||
</dropdown.link>
|
||||
<dropdown.separator />
|
||||
/>
|
||||
<dd.Separator />
|
||||
{{/if}}
|
||||
{{#if (can 'delete model' @model)}}
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.host-set.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.host-set.actions.delete'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</Hds::Dropdown>
|
||||
@@ -4,17 +4,12 @@
|
||||
}}
|
||||
|
||||
{{#if (can 'delete model' @model)}}
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<Hds::Dropdown data-test-manage-hosts-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.host.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.host.actions.delete'}}
|
||||
</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
/>
|
||||
</Hds::Dropdown>
|
||||
{{/if}}
|
||||
@@ -2,44 +2,39 @@
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Hds::Dropdown data-test-manage-targets-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t 'actions.manage'}}
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
{{#if (can 'addHostSources target' @model)}}
|
||||
<dropdown.link @route='scopes.scope.targets.target.add-host-sources'>
|
||||
{{t 'resources.target.actions.add-host-sources'}}
|
||||
</dropdown.link>
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.targets.target.add-host-sources'
|
||||
@text={{t 'resources.target.actions.add-host-sources'}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if (can 'addBrokeredCredentialSources target' @model)}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.targets.target.add-brokered-credential-sources'
|
||||
@text={{t 'resources.target.actions.add-brokered-credential-sources'}}
|
||||
data-test-add-brokered-cred-sources-action
|
||||
>
|
||||
{{t 'resources.target.actions.add-brokered-credential-sources'}}
|
||||
</dropdown.link>
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (can 'addInjectedApplicationCredentialSources target' @model)}}
|
||||
<dropdown.link
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.targets.target.add-injected-application-credential-sources'
|
||||
data-test-add-injected-cred-sources-action
|
||||
>
|
||||
{{t
|
||||
@text={{t
|
||||
'resources.target.actions.add-injected-application-credential-sources'
|
||||
}}
|
||||
</dropdown.link>
|
||||
data-test-add-injected-cred-sources-action
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if (can 'delete model' @model)}}
|
||||
<dropdown.separator />
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
<dd.Separator />
|
||||
<dd.Interactive
|
||||
@text={{t 'resources.target.actions.delete'}}
|
||||
@color='critical'
|
||||
{{on 'click' (fn @delete @model)}}
|
||||
>
|
||||
{{t 'resources.target.actions.delete'}}
|
||||
</dropdown.button>
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
|
||||
</Hds::Dropdown>
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
|
||||
export default class OnboardingIndexController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@service router;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Creates resources then redirects to success page.
|
||||
* @param {object} model
|
||||
* @param {string} targetAddress
|
||||
* @param {string} targetPort
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(function () {
|
||||
return this.intl.t('errors.onboarding-failed.description');
|
||||
})
|
||||
async createResources(model, targetAddress, targetPort) {
|
||||
const { org, project, target, role } = model;
|
||||
|
||||
await this.createOrg(org);
|
||||
await this.createProject(org, project);
|
||||
await this.createTarget(project, target, targetAddress, targetPort);
|
||||
await this.createGrants(org, project, target, role);
|
||||
|
||||
// Redirect to success
|
||||
this.router.transitionTo('onboarding.success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Org, if fails, redirects to Orgs empty list
|
||||
* @param {ScopeModel} org
|
||||
*/
|
||||
async createOrg(org) {
|
||||
try {
|
||||
await org.save();
|
||||
} catch (error) {
|
||||
console.log('error ', error);
|
||||
// Redirects to Orgs screen
|
||||
this.router.transitionTo('scopes.scope', 'global');
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Project, if fails redirects to Projects from Org empty list.
|
||||
* @param {ScopeModel} org
|
||||
* @param {ScopeModel} project
|
||||
*/
|
||||
async createProject(org, project) {
|
||||
project.scopeID = org.id;
|
||||
|
||||
try {
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
// Redirects to Project screen
|
||||
this.router.transitionTo('scopes.scope.scopes', project.scopeID);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Target, if fails redirects to Targets empty list.
|
||||
* @param {ScopeModel} project
|
||||
* @param {TargetModel} target
|
||||
* @param {string} targetAddress
|
||||
* @param {string} targetPort
|
||||
*/
|
||||
async createTarget(project, target, targetAddress, targetPort) {
|
||||
target.scopeID = project.id;
|
||||
target.address = targetAddress;
|
||||
target.default_port = targetPort;
|
||||
|
||||
try {
|
||||
await target.save();
|
||||
} catch (error) {
|
||||
// Redirect to targets global
|
||||
this.router.replaceWith('scopes.scope.targets', project.scopeID);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns default grants and scope to a role.
|
||||
* @param {ScopeModel} org
|
||||
* @param {ScopeModel} project
|
||||
* @param {TargetModel} target
|
||||
* @param {RoleModel} role
|
||||
*/
|
||||
async createGrants(org, project, target, role) {
|
||||
role.scopeID = org.id;
|
||||
role.grant_scope_id = project.id;
|
||||
try {
|
||||
await role.save();
|
||||
await role.saveGrantStrings([
|
||||
`type=target;actions=list`,
|
||||
`ids=${target.id};actions=authorize-session`,
|
||||
]);
|
||||
} catch (error) {
|
||||
// Redirect to targets that belong to scope
|
||||
this.router.replaceWith('scopes.scope.targets', project.scopeID);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects user to index route for further processing.
|
||||
*/
|
||||
@action
|
||||
doLater() {
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class OnboardingSuccessController extends Controller {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Redirect user to target created during onboarding.
|
||||
* @param {object} model
|
||||
*/
|
||||
@action
|
||||
showTargetList(model) {
|
||||
const {
|
||||
project: { id: scopeID },
|
||||
target: { id: targetID },
|
||||
} = model;
|
||||
this.router.transitionTo('scopes.scope.targets.target', scopeID, targetID);
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,26 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { loading } from 'ember-loading';
|
||||
import { debounce } from 'core/decorators/debounce';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
|
||||
export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
// =services
|
||||
|
||||
@service can;
|
||||
@service router;
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = ['search', 'page', 'pageSize'];
|
||||
|
||||
@tracked search;
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
@@ -22,10 +33,10 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
* @param {AliasModel} alias
|
||||
*/
|
||||
@action
|
||||
async cancel(alias) {
|
||||
cancel(alias) {
|
||||
const { isNew } = alias;
|
||||
alias.rollbackAttributes();
|
||||
if (isNew) await this.router.transitionTo('scopes.scope.aliases');
|
||||
if (isNew) this.router.transitionTo('scopes.scope.aliases');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +60,7 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove destaination_id from alias
|
||||
* Remove destination_id from alias
|
||||
* @param {AliasModel} alias
|
||||
*/
|
||||
@action
|
||||
@@ -77,4 +88,16 @@ export default class ScopesScopeAliasesIndexController extends Controller {
|
||||
await this.router.replaceWith('scopes.scope.aliases');
|
||||
await this.router.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input on each keystroke and the search queryParam
|
||||
* @param {object} event
|
||||
*/
|
||||
@action
|
||||
@debounce(250)
|
||||
handleSearchInput(event) {
|
||||
const { value } = event.target;
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
import {
|
||||
TYPE_AUTH_METHOD_PASSWORD,
|
||||
TYPE_AUTH_METHOD_OIDC,
|
||||
TYPE_AUTH_METHOD_LDAP,
|
||||
} from 'api/models/auth-method';
|
||||
|
||||
export default class ScopesScopeAuthenticateMethodIndexController extends Controller {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
@service windowManager;
|
||||
@service router;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Delegates authentication to an authenticator of type matching the current
|
||||
* auth method. If authentication fails, notifies with an alert.
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(() => 'errors.authentication-failed.title', {
|
||||
sticky: false,
|
||||
catch: true,
|
||||
})
|
||||
async authenticate(authMethod, creds) {
|
||||
const scope = authMethod.scope;
|
||||
const authenticatorName = `authenticator:${authMethod.type}`;
|
||||
const requestCookies = false;
|
||||
switch (authMethod.type) {
|
||||
case TYPE_AUTH_METHOD_PASSWORD:
|
||||
case TYPE_AUTH_METHOD_LDAP:
|
||||
await this.session.authenticate(
|
||||
authenticatorName,
|
||||
creds,
|
||||
requestCookies,
|
||||
{ scope, authMethod },
|
||||
);
|
||||
this.router.transitionTo('index');
|
||||
break;
|
||||
case TYPE_AUTH_METHOD_OIDC:
|
||||
await this.startOIDCAuthentication(authenticatorName, {
|
||||
scope,
|
||||
authMethod,
|
||||
});
|
||||
this.router.transitionTo('scopes.scope.authenticate.method.oidc');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off the OIDC auth flow by opening a new window to the provider.
|
||||
* @param {string} authenticatorName
|
||||
* @param {object} options
|
||||
*/
|
||||
async startOIDCAuthentication(authenticatorName, options) {
|
||||
const oidc = getOwner(this).lookup(authenticatorName);
|
||||
// TODO: delegate this call from the session service so that we don't have
|
||||
// to look up the authenticator directly
|
||||
const json = await oidc.startAuthentication(options);
|
||||
await this.windowManager.open(json.attributes.auth_url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeEditController extends Controller {
|
||||
@controller('scopes/scope/index') scopes;
|
||||
}
|
||||
@@ -99,4 +99,14 @@ export default class ScopesScopeHostCatalogsIndexController extends Controller {
|
||||
if (type === 'plugin') type = 'aws';
|
||||
this.router.replaceWith({ queryParams: { type } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates credential type
|
||||
* @param {model} hostcatalog
|
||||
* @param {string} credentialType
|
||||
*/
|
||||
@action
|
||||
changeCredentialType(hostCatalog, credentialType) {
|
||||
hostCatalog.credentialType = credentialType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
|
||||
export default class ScopesScopeIndexController extends Controller {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
|
||||
//= actions
|
||||
|
||||
/**
|
||||
* Rollback changes on scope.
|
||||
* @param {ScopeModel} scope
|
||||
*/
|
||||
@action
|
||||
cancel(scope) {
|
||||
const { isNew } = scope;
|
||||
scope.rollbackAttributes();
|
||||
if (isNew) this.router.transitionTo('scopes.scope');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save scope.
|
||||
* @param {ScopeModel} scope
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message)
|
||||
@notifySuccess(({ isNew }) =>
|
||||
isNew ? 'notifications.create-success' : 'notifications.save-success',
|
||||
)
|
||||
async save(scope) {
|
||||
const { isNew } = scope;
|
||||
await scope.save();
|
||||
await this.router.transitionTo('scopes.scope.edit', scope);
|
||||
if (isNew) this.router.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the scope and redirects to index.
|
||||
* @param {ScopeModel} scope
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('questions.delete-confirm')
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(scope) {
|
||||
const { scopeID } = scope;
|
||||
await scope.destroyRecord();
|
||||
this.router.replaceWith('scopes.scope.scopes', scopeID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the scope and redirects to index.
|
||||
* @param {ScopeModel} scope
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.policy.questions.detach')
|
||||
@notifyError(({ message }) => message)
|
||||
@notifySuccess('resources.policy.messages.detach')
|
||||
async detachStoragePolicy(scope) {
|
||||
const { storage_policy_id } = scope;
|
||||
scope.storage_policy_id = '';
|
||||
await scope.detachStoragePolicy(storage_policy_id);
|
||||
}
|
||||
}
|
||||
@@ -23,22 +23,23 @@ export default class ScopesScopeRolesRoleManageScopesIndexController extends Con
|
||||
/**
|
||||
* Save grant scope IDs to current role via the API.
|
||||
* @param {RoleModel} role
|
||||
* @param {[string]} grantScopeIDs
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('resources.role.scope.messages.manage-scopes.success')
|
||||
async setGrantScopes(role, grantScopeIDs) {
|
||||
await role.setGrantScopes(grantScopeIDs);
|
||||
async setGrantScopes(role) {
|
||||
await role.setGrantScopes(role.grant_scope_ids);
|
||||
this.router.replaceWith('scopes.scope.roles.role.scopes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to role scopes as if nothing ever happened.
|
||||
* @param {RoleModel} role
|
||||
*/
|
||||
@action
|
||||
cancel() {
|
||||
cancel(role) {
|
||||
role.rollbackAttributes();
|
||||
this.router.replaceWith('scopes.scope.roles.role.scopes');
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -43,23 +43,24 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesControlle
|
||||
/**
|
||||
* Save grant scope IDs to current role via the API.
|
||||
* @param {RoleModel} role
|
||||
* @param {[string]} grantScopeIDs
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('resources.role.scope.messages.manage-custom-scopes.success')
|
||||
async setGrantScopes(role, grantScopeIDs) {
|
||||
await role.setGrantScopes(grantScopeIDs);
|
||||
async setGrantScopes(role) {
|
||||
await role.setGrantScopes(role.grant_scope_ids);
|
||||
this.manageScopes.showCheckIcon = true;
|
||||
await this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
|
||||
this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to manage scopes as if nothing ever happened.
|
||||
* @param {RoleModel} role
|
||||
*/
|
||||
@action
|
||||
async cancel() {
|
||||
await this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
|
||||
cancel(role) {
|
||||
role.rollbackAttributes();
|
||||
this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -43,14 +43,13 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController
|
||||
/**
|
||||
* Save grant scope IDs to current role via the API.
|
||||
* @param {RoleModel} role
|
||||
* @param {[string]} grantScopeIDs
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('resources.role.scope.messages.manage-org-projects.success')
|
||||
async setGrantScopes(role, grantScopeIDs) {
|
||||
await role.setGrantScopes(grantScopeIDs);
|
||||
async setGrantScopes(role) {
|
||||
await role.setGrantScopes(role.grant_scope_ids);
|
||||
this.manageScopes.showCheckIcon = true;
|
||||
if (role.scope.isGlobal) {
|
||||
this.router.replaceWith(
|
||||
@@ -63,9 +62,11 @@ export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsController
|
||||
|
||||
/**
|
||||
* Redirect to previous as if nothing ever happened.
|
||||
* @param {RoleModel} role
|
||||
*/
|
||||
@action
|
||||
cancel(role) {
|
||||
role.rollbackAttributes();
|
||||
if (role.scope.isGlobal) {
|
||||
this.router.replaceWith(
|
||||
'scopes.scope.roles.role.manage-scopes.manage-custom-scopes',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeScopesNewController extends Controller {
|
||||
@controller('scopes/scope/index') scopes;
|
||||
}
|
||||
@@ -4,21 +4,192 @@
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { debounce } from 'core/decorators/debounce';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeSessionRecordingsIndexController extends Controller {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service intl;
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = [
|
||||
'search',
|
||||
'time',
|
||||
{ users: { type: 'array' } },
|
||||
{ scopes: { type: 'array' } },
|
||||
{ targets: { type: 'array' } },
|
||||
'page',
|
||||
'pageSize',
|
||||
];
|
||||
|
||||
now = new Date();
|
||||
|
||||
@tracked search = '';
|
||||
@tracked time = null;
|
||||
@tracked users = [];
|
||||
@tracked scopes = [];
|
||||
@tracked targets = [];
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
|
||||
/**
|
||||
* Returns true if any session recordings exist
|
||||
* @type {boolean}
|
||||
* Returns object of filters to be used for displaying selected filters
|
||||
* @returns {object}
|
||||
*/
|
||||
get hasSessionRecordings() {
|
||||
return this.model?.sessionRecordings?.length;
|
||||
get filters() {
|
||||
return {
|
||||
allFilters: {
|
||||
time: this.timeOptions,
|
||||
users: this.filterOptions('user'),
|
||||
scopes: this.projectScopes,
|
||||
targets: this.filterOptions('target'),
|
||||
},
|
||||
selectedFilters: {
|
||||
time: [this.time],
|
||||
users: this.users,
|
||||
scopes: this.scopes,
|
||||
targets: this.targets,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any storage buckets exist
|
||||
* @type {boolean}
|
||||
* Returns array of time options for time filter
|
||||
* @returns {[object]}
|
||||
*/
|
||||
get hasSessionRecordingsConfigured() {
|
||||
return this.model?.storageBuckets?.length;
|
||||
get timeOptions() {
|
||||
const last24Hours = new Date(this.now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const last3Days = new Date(this.now);
|
||||
last3Days.setDate(this.now.getDate() - 3);
|
||||
const last7Days = new Date(this.now);
|
||||
last7Days.setDate(this.now.getDate() - 7);
|
||||
|
||||
return [
|
||||
{
|
||||
id: last24Hours.toISOString(),
|
||||
name: this.intl.t(
|
||||
'resources.session-recording.filters.time.last-twenty-four-hours',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: last3Days.toISOString(),
|
||||
name: this.intl.t(
|
||||
'resources.session-recording.filters.time.last-three-days',
|
||||
),
|
||||
},
|
||||
{
|
||||
id: last7Days.toISOString(),
|
||||
name: this.intl.t(
|
||||
'resources.session-recording.filters.time.last-seven-days',
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unique project scopes from targets
|
||||
* linked to the session recordings
|
||||
* @returns {[object]}
|
||||
*/
|
||||
get projectScopes() {
|
||||
const uniqueMap = new Map();
|
||||
this.model.allSessionRecordings.forEach(
|
||||
({
|
||||
create_time_values: {
|
||||
target: {
|
||||
scope: { id, name, parent_scope_id },
|
||||
},
|
||||
},
|
||||
}) => {
|
||||
if (!uniqueMap.has(id)) {
|
||||
const projectName = name || id;
|
||||
uniqueMap.set(id, { id, name: projectName, parent_scope_id });
|
||||
}
|
||||
},
|
||||
);
|
||||
return Array.from(uniqueMap.values());
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Looks up org by ID and returns the org name
|
||||
* @param {string} orgID
|
||||
* @returns {string}
|
||||
*/
|
||||
@action
|
||||
orgName(orgID) {
|
||||
const org = this.store.peekRecord('scope', orgID);
|
||||
return org.displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all filter options for key for session recordings
|
||||
* @param {string} key
|
||||
* @returns {[object]}
|
||||
*/
|
||||
@action
|
||||
filterOptions(key) {
|
||||
const uniqueMap = new Map();
|
||||
this.model.allSessionRecordings.forEach(
|
||||
({
|
||||
create_time_values: {
|
||||
[key]: { id, name },
|
||||
},
|
||||
}) => {
|
||||
if (!uniqueMap.has(id)) {
|
||||
uniqueMap.set(id, { id, name });
|
||||
}
|
||||
},
|
||||
);
|
||||
return Array.from(uniqueMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input on each keystroke and the search queryParam
|
||||
* @param {object} event
|
||||
*/
|
||||
@action
|
||||
@debounce(250)
|
||||
handleSearchInput(event) {
|
||||
const { value } = event.target;
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected items for the given paramKey and sets the page to 1
|
||||
* @param {string} paramKey
|
||||
* @param {[string]} selectedItems
|
||||
*/
|
||||
@action
|
||||
applyFilter(paramKey, selectedItems) {
|
||||
this[paramKey] = [...selectedItems];
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time filter, sets the page to 1, and closes the filter dropdown
|
||||
* @param {object} timeId
|
||||
* @param {func} onClose
|
||||
*/
|
||||
@action
|
||||
changeTimeFilter(timeId, onClose) {
|
||||
this.time = timeId;
|
||||
this.page = 1;
|
||||
onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the all data for the current page
|
||||
*/
|
||||
@action
|
||||
refresh() {
|
||||
this.send('refreshAll');
|
||||
}
|
||||
}
|
||||
+51
-2
@@ -4,15 +4,26 @@
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import {
|
||||
STATE_SESSION_RECORDING_STARTED,
|
||||
STATE_SESSION_RECORDING_UNKNOWN,
|
||||
} from 'api/models/session-recording';
|
||||
|
||||
export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConnectionIndexController extends Controller {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Returns true if the session recording has started
|
||||
* but has not started any connections
|
||||
* but has not started any connections.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSessionInprogressWithNoConnections() {
|
||||
@@ -24,7 +35,7 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne
|
||||
|
||||
/**
|
||||
* Returns true if the session recording state is unknown
|
||||
* and has no connections
|
||||
* and has no connections.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isSessionUnknownWithNoConnections() {
|
||||
@@ -33,4 +44,42 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne
|
||||
this.model?.sessionRecording?.state === STATE_SESSION_RECORDING_UNKNOWN
|
||||
);
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Reapplies storage policy dates to session recording.
|
||||
* @param {SessionRecordingModel}
|
||||
*/
|
||||
@action
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess(({ end_time }) =>
|
||||
!end_time
|
||||
? 'resources.policy.messages.later'
|
||||
: 'resources.policy.messages.reapply',
|
||||
)
|
||||
async reapplyStoragePolicy(sessionRecording) {
|
||||
try {
|
||||
await sessionRecording.reapplyStoragePolicy();
|
||||
} catch (e) {
|
||||
this.router.replaceWith('scopes.scope.session-recordings');
|
||||
this.router.refresh();
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the session recording.
|
||||
* @param {SessionRecording} recording
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.session-recording.questions.delete')
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(sessionRecording) {
|
||||
await sessionRecording.destroyRecord();
|
||||
this.router.replaceWith('scopes.scope.session-recordings');
|
||||
this.router.refresh();
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import fetch from 'fetch';
|
||||
|
||||
async function autoInitializeFeatureEdition(owner) {
|
||||
const service = owner.lookup('service:feature-edition');
|
||||
const adapter = owner.lookup('adapter:application');
|
||||
|
||||
@@ -43,7 +43,8 @@ export default class ApplicationRoute extends Route {
|
||||
this.router.on('routeWillChange', async (transition) => {
|
||||
const fromName = transition?.from?.name;
|
||||
const toName = transition?.to?.name;
|
||||
const maybeModel = transition?.from?.attributes;
|
||||
const maybeModel =
|
||||
transition?.data?.model ?? transition?.from?.attributes;
|
||||
if (fromName !== toName && maybeModel?.hasDirtyAttributes) {
|
||||
transition.abort();
|
||||
try {
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
import { TYPE_TARGET_TCP } from 'api/models/target';
|
||||
|
||||
export default class OnboardingRoute extends Route {
|
||||
@@ -16,7 +13,6 @@ export default class OnboardingRoute extends Route {
|
||||
@service store;
|
||||
@service session;
|
||||
@service router;
|
||||
@service intl;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -34,7 +30,7 @@ export default class OnboardingRoute extends Route {
|
||||
|
||||
/**
|
||||
* List all scopes (orgs) under global.
|
||||
* Creare org, project, target and roles
|
||||
* Create org, project, target and roles
|
||||
*/
|
||||
async model() {
|
||||
return {
|
||||
@@ -59,95 +55,4 @@ export default class OnboardingRoute extends Route {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// =actions
|
||||
@action
|
||||
@loading
|
||||
@notifyError(function () {
|
||||
return this.intl.t('errors.onboarding-failed.description');
|
||||
})
|
||||
async createResources(targetAddress, targetPort) {
|
||||
const { org, project, target, role } = this.currentModel;
|
||||
|
||||
await this.createOrg(org);
|
||||
await this.createProject(org, project);
|
||||
await this.createTarget(project, target, targetAddress, targetPort);
|
||||
await this.createGrants(org, project, target, role);
|
||||
|
||||
// Redirect to success
|
||||
this.router.transitionTo('onboarding.success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Org, if fails, redirects to Orgs empty list
|
||||
* @param {Object} org
|
||||
*/
|
||||
async createOrg(org) {
|
||||
try {
|
||||
await org.save();
|
||||
} catch (error) {
|
||||
// Redirects to Orgs screen
|
||||
this.router.transitionTo('scopes.scope', 'global');
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Project, if fails redirects to Projects from Org empty list.
|
||||
* @param {Object} org
|
||||
* @param {Object} project
|
||||
*/
|
||||
async createProject(org, project) {
|
||||
project.scopeID = org.id;
|
||||
|
||||
try {
|
||||
await project.save();
|
||||
} catch (error) {
|
||||
// Redirects to Project screen
|
||||
this.router.transitionTo('scopes.scope.scopes', project.scopeID);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Target, if fails redirects to Targets empyt list.
|
||||
* @param {Object} project
|
||||
* @param {Object} target
|
||||
* @param {Object} targetAddress
|
||||
* @param {Object} targetPort
|
||||
*/
|
||||
async createTarget(project, target, targetAddress, targetPort) {
|
||||
target.scopeID = project.id;
|
||||
target.address = targetAddress;
|
||||
target.default_port = targetPort;
|
||||
|
||||
try {
|
||||
await target.save();
|
||||
} catch (error) {
|
||||
// Redirect to targets global
|
||||
this.router.replaceWith('scopes.scope.targets', project.scopeID);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async createGrants(org, project, target, role) {
|
||||
role.scopeID = org.id;
|
||||
role.grant_scope_ids = [project.id];
|
||||
try {
|
||||
await role.save();
|
||||
await role.saveGrantStrings([
|
||||
`type=target;actions=list`,
|
||||
`ids=${target.id};actions=authorize-session`,
|
||||
]);
|
||||
} catch (error) {
|
||||
// Redirect to targets that belong to scope
|
||||
this.router.replaceWith('scopes.scope.targets', project.scopeID);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async doLater() {
|
||||
this.router.transitionTo('index');
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,14 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class OnboardingSuccessRoute extends Route {
|
||||
// =services
|
||||
@service router;
|
||||
@service session;
|
||||
|
||||
@service('browser/window') window;
|
||||
|
||||
// =methods
|
||||
|
||||
model() {
|
||||
const model = this.modelFor('onboarding');
|
||||
return {
|
||||
@@ -21,13 +20,4 @@ export default class OnboardingSuccessRoute extends Route {
|
||||
clusterUrl: this.window.origin,
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
showTargetList(scope) {
|
||||
const {
|
||||
project: { id: scopeID },
|
||||
target: { id: targetID },
|
||||
} = scope;
|
||||
this.router.transitionTo('scopes.scope.targets.target', scopeID, targetID);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { A } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
|
||||
export default class ScopesRoute extends Route {
|
||||
// =services
|
||||
@@ -41,19 +37,4 @@ export default class ScopesRoute extends Route {
|
||||
})
|
||||
.catch(() => A([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the scope and redirects to index.
|
||||
* @param {Model} scope
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.policy.questions.detach')
|
||||
@notifyError(({ message }) => message)
|
||||
@notifySuccess('resources.policy.messages.detach')
|
||||
async detachStoragePolicy(scope) {
|
||||
const { storage_policy_id } = scope;
|
||||
scope.storage_policy_id = '';
|
||||
await scope.detachStoragePolicy(storage_policy_id);
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,13 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { A } from '@ember/array';
|
||||
import { action } from '@ember/object';
|
||||
import { next } from '@ember/runloop';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { TYPE_SCOPE_GLOBAL, TYPE_SCOPE_ORG } from 'api/models/scope';
|
||||
|
||||
export default class ScopesScopeRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service intl;
|
||||
@service session;
|
||||
@service scope;
|
||||
@service router;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -105,52 +97,4 @@ export default class ScopesScopeRoute extends Route {
|
||||
/* eslint-disable-next-line ember/no-controller-access-in-routes */
|
||||
this.controller.setProperties({ scopes });
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Rollback changes on scope.
|
||||
* @param {Model} scope
|
||||
*/
|
||||
@action
|
||||
cancel(scope) {
|
||||
const { isNew } = scope;
|
||||
next(() => {
|
||||
scope.rollbackAttributes();
|
||||
if (isNew) this.router.transitionTo('scopes.scope');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save scope.
|
||||
* @param {Model} scope
|
||||
* @param {Event} e
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message)
|
||||
@notifySuccess(({ isNew }) =>
|
||||
isNew ? 'notifications.create-success' : 'notifications.save-success',
|
||||
)
|
||||
async save(scope) {
|
||||
const { isNew } = scope;
|
||||
await scope.save();
|
||||
await this.router.transitionTo('scopes.scope.edit', scope);
|
||||
if (isNew) this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the scope and redirects to index.
|
||||
* @param {Model} scope
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('questions.delete-confirm')
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(scope) {
|
||||
const { scopeID } = scope;
|
||||
await scope.destroyRecord();
|
||||
this.router.replaceWith('scopes.scope.scopes', scopeID);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,11 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { all, hash } from 'rsvp';
|
||||
|
||||
export default class ScopesScopeAliasesRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service session;
|
||||
@service can;
|
||||
@service router;
|
||||
|
||||
// =methods
|
||||
@@ -23,34 +20,4 @@ export default class ScopesScopeAliasesRoute extends Route {
|
||||
beforeModel() {
|
||||
if (!this.session.isAuthenticated) this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all aliases in current scope
|
||||
* @return {Promise<[AliasModel]>}
|
||||
*/
|
||||
async model() {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const { id: scope_id } = scope;
|
||||
|
||||
if (
|
||||
this.can.can('list model', scope, {
|
||||
collection: 'aliases',
|
||||
})
|
||||
) {
|
||||
const aliases = await this.store.query('alias', { scope_id });
|
||||
// since we don't receive target info from aliases list API,
|
||||
// we query the store to fetch target information based on the destination id
|
||||
const aliasesWithResourceNames = await all(
|
||||
aliases.map((alias) =>
|
||||
hash({
|
||||
alias,
|
||||
target: alias.destination_id
|
||||
? this.store.findRecord('target', alias.destination_id)
|
||||
: null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
return aliasesWithResourceNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,100 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
export default class ScopesScopeAliasesIndexRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service can;
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = {
|
||||
search: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
page: {
|
||||
refreshModel: true,
|
||||
},
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Loads queried aliases and the number of aliases under current scope.
|
||||
* @returns {Promise<{totalItems: number, aliases: [object], aliasesExist: boolean}>}
|
||||
*/
|
||||
async model({ search, page, pageSize }) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const { id: scope_id } = scope;
|
||||
|
||||
let aliases;
|
||||
let doAliasesExist = false;
|
||||
let totalItems = 0;
|
||||
|
||||
if (
|
||||
this.can.can('list model', scope, {
|
||||
collection: 'aliases',
|
||||
})
|
||||
) {
|
||||
aliases = await this.store.query('alias', {
|
||||
scope_id,
|
||||
query: { search },
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
totalItems = aliases.meta?.totalItems;
|
||||
// since we don't receive target info from aliases list API,
|
||||
// we query the store to fetch target information based on the destination id
|
||||
aliases = await Promise.all(
|
||||
aliases.map((alias) =>
|
||||
hash({
|
||||
alias,
|
||||
target: alias.destination_id
|
||||
? this.store.findRecord('target', alias.destination_id, {
|
||||
backgroundReload: false,
|
||||
})
|
||||
: null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
doAliasesExist = await this.getDoAliasesExist(scope_id, totalItems);
|
||||
}
|
||||
|
||||
return { aliases, doAliasesExist, totalItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets doAliasesExist to true if there are any aliases.
|
||||
* @param {string} scope_id
|
||||
* @param {number} totalItems
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async getDoAliasesExist(scope_id, totalItems) {
|
||||
if (totalItems > 0) {
|
||||
return true;
|
||||
}
|
||||
const options = { pushToStore: false, peekIndexedDB: true };
|
||||
const aliases = await this.store.query(
|
||||
'alias',
|
||||
{
|
||||
scope_id,
|
||||
query: { filters: { scope_id: [{ equals: scope_id }] } },
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
},
|
||||
options,
|
||||
);
|
||||
return aliases.length > 0;
|
||||
}
|
||||
|
||||
setupController(controller) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
super.setupController(...arguments);
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
|
||||
|
||||
/**
|
||||
* Loads queried auth-methods and the number of auth-methods under current scope.
|
||||
* @returns {Promise<{totalItems: number, authMethods: [AuthMethodModel], authMethodsExist: boolean }> }
|
||||
* @returns {Promise<{totalItems: number, authMethods: [AuthMethodModel], doAuthMethodsExist: boolean }> }
|
||||
*/
|
||||
async model({ search, types, primary, page, pageSize }) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
@@ -62,7 +62,7 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
|
||||
|
||||
let authMethods;
|
||||
let totalItems = 0;
|
||||
let authMethodsExist = false;
|
||||
let doAuthMethodsExist = false;
|
||||
if (this.can.can('list model', scope, { collection: 'auth-methods' })) {
|
||||
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
|
||||
const options = { storeToken: false };
|
||||
@@ -77,19 +77,22 @@ export default class ScopesScopeAuthMethodsIndexRoute extends Route {
|
||||
options,
|
||||
);
|
||||
totalItems = authMethods.meta?.totalItems;
|
||||
authMethodsExist = await this.getAuthMethodsExist(scope_id, totalItems);
|
||||
doAuthMethodsExist = await this.getDoAuthMethodsExist(
|
||||
scope_id,
|
||||
totalItems,
|
||||
);
|
||||
}
|
||||
|
||||
return { authMethods, authMethodsExist, totalItems };
|
||||
return { authMethods, doAuthMethodsExist, totalItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets authMethodsExist to true if there exists any auth methods.
|
||||
* Sets doAuthMethodsExist to true if there exists any auth methods.
|
||||
* @param {string} scope_id
|
||||
* @param {number} totalItems
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async getAuthMethodsExist(scope_id, totalItems) {
|
||||
async getDoAuthMethodsExist(scope_id, totalItems) {
|
||||
if (totalItems > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,24 +5,11 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
import {
|
||||
TYPE_AUTH_METHOD_PASSWORD,
|
||||
TYPE_AUTH_METHOD_OIDC,
|
||||
TYPE_AUTH_METHOD_LDAP,
|
||||
} from 'api/models/auth-method';
|
||||
|
||||
export default class ScopesScopeAuthenticateMethodRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service session;
|
||||
@service intl;
|
||||
@service windowManager;
|
||||
@service router;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -35,55 +22,4 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
|
||||
const adapterOptions = { scopeID: this.modelFor('scopes.scope').id };
|
||||
return this.store.peekRecord('auth-method', id, adapterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off the OIDC auth flow by opening a new window to the provider.
|
||||
* @param {string} authenticatorName
|
||||
* @param {object} options
|
||||
*/
|
||||
async startOIDCAuthentication(authenticatorName, options) {
|
||||
const oidc = getOwner(this).lookup(authenticatorName);
|
||||
// TODO: delegate this call from the session service so that we don't have
|
||||
// to look up the authenticator directly
|
||||
const json = await oidc.startAuthentication(options);
|
||||
await this.windowManager.open(json.attributes.auth_url);
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Delegates authentication to an authenticator of type matching the current
|
||||
* auth method. If authentication fails, notifies with an alert.
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(() => 'errors.authentication-failed.title', {
|
||||
sticky: false,
|
||||
catch: true,
|
||||
})
|
||||
async authenticate(creds) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const authMethod = this.modelFor('scopes.scope.authenticate.method');
|
||||
const authenticatorName = `authenticator:${authMethod.type}`;
|
||||
const requestCookies = false;
|
||||
switch (authMethod.type) {
|
||||
case TYPE_AUTH_METHOD_PASSWORD:
|
||||
case TYPE_AUTH_METHOD_LDAP:
|
||||
await this.session.authenticate(
|
||||
authenticatorName,
|
||||
creds,
|
||||
requestCookies,
|
||||
{ scope, authMethod },
|
||||
);
|
||||
this.router.transitionTo('index');
|
||||
break;
|
||||
case TYPE_AUTH_METHOD_OIDC:
|
||||
await this.startOIDCAuthentication(authenticatorName, {
|
||||
scope,
|
||||
authMethod,
|
||||
});
|
||||
this.router.transitionTo('scopes.scope.authenticate.method.oidc');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
|
||||
|
||||
/**
|
||||
* Loads queried credential-stores and the number of credential-stores under current scope.
|
||||
* @returns {Promise<{totalItems: number, credentialStores: [CredentialStoreModel], credentialStoresExist: boolean }> }
|
||||
* @returns {Promise<{totalItems: number, credentialStores: [CredentialStoreModel], doCredentialStoresExist: boolean }> }
|
||||
*/
|
||||
async model({ search, types, page, pageSize }) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
@@ -51,7 +51,7 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
|
||||
|
||||
let credentialStores;
|
||||
let totalItems = 0;
|
||||
let credentialStoresExist = false;
|
||||
let doCredentialStoresExist = false;
|
||||
if (
|
||||
this.can.can('list model', scope, {
|
||||
collection: 'credential-stores',
|
||||
@@ -64,22 +64,21 @@ export default class ScopesScopeCredentialStoresIndexRoute extends Route {
|
||||
pageSize,
|
||||
});
|
||||
totalItems = credentialStores.meta?.totalItems;
|
||||
credentialStoresExist = await this.getCredentialStoresExist(
|
||||
doCredentialStoresExist = await this.getDoCredentialStoresExist(
|
||||
scope_id,
|
||||
totalItems,
|
||||
);
|
||||
}
|
||||
|
||||
return { credentialStores, credentialStoresExist, totalItems };
|
||||
return { credentialStores, doCredentialStoresExist, totalItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets credentialStoresExist to true if there exists any credential-stores.
|
||||
* Sets doCredentialStoresExist to true if there exists any credential-stores.
|
||||
* @param {string} scope_id
|
||||
* @param {number} totalItems
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async getCredentialStoresExist(scope_id, totalItems) {
|
||||
async getDoCredentialStoresExist(scope_id, totalItems) {
|
||||
if (totalItems > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Loaded 100 of 457 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user