Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23de0695d0 | ||
|
|
8d4cb1bfea | ||
|
|
d67f6dab2f | ||
|
|
8ee0e0b92d | ||
|
|
be9b68eeff | ||
|
|
1e20f7a149 | ||
|
|
b413e9d8dd | ||
|
|
fd3c9eb876 | ||
|
|
9979fe8464 | ||
|
|
9049bb2087 | ||
|
|
82135a4b20 | ||
|
|
dd37718c62 | ||
|
|
e6f0166ff0 | ||
|
|
606a48ca49 | ||
|
|
d1e46cb9b1 | ||
|
|
11ceb8b5ca | ||
|
|
3854d32e92 | ||
|
|
bbb703c671 | ||
|
|
f507edae03 | ||
|
|
9231963ac1 | ||
|
|
9bfb136247 | ||
|
|
4af4ee7cc0 | ||
|
|
12a9f72c68 | ||
|
|
bb88672aff | ||
|
|
25e004976e | ||
|
|
60993e455a | ||
|
|
40ee317f47 | ||
|
|
530cd0298a | ||
|
|
374a13cd07 | ||
|
|
6f5da333e0 | ||
|
|
d121bcf204 | ||
|
|
2acd555966 | ||
|
|
820843c51d | ||
|
|
243f165644 | ||
|
|
5036c62740 | ||
|
|
3ffe70cf5d | ||
|
|
c5b0061428 | ||
|
|
fefae2b73d | ||
|
|
196d01aae2 | ||
|
|
96707ebe15 | ||
|
|
79f5db0441 | ||
|
|
d4fee0af34 | ||
|
|
a8c65e73f5 | ||
|
|
13df776c18 | ||
|
|
12588a17ab | ||
|
|
7b2c8b7d58 | ||
|
|
873965d7b7 | ||
|
|
f02f488edd | ||
|
|
a2ee17e918 | ||
|
|
bf8620d75c | ||
|
|
d0eb41e03b | ||
|
|
6e932f8054 | ||
|
|
76291896c3 | ||
|
|
c803adccc1 | ||
|
|
f7854b642c | ||
|
|
a7a6bdedf9 | ||
|
|
4bcd0e7d06 | ||
|
|
895a25218e | ||
|
|
8356e7d47a | ||
|
|
680ae9980b | ||
|
|
e106454bb5 | ||
|
|
aa227a0db8 | ||
|
|
8aca872c66 | ||
|
|
269c56bf83 | ||
|
|
a89b27c813 | ||
|
|
2804b4322b | ||
|
|
66d16d84d0 | ||
|
|
f420d01fb8 | ||
|
|
4bd80a716e | ||
|
|
8098beb450 | ||
|
|
a6672ecef4 | ||
|
|
79b6737aaa | ||
|
|
31afafff4e | ||
|
|
d4a4828b67 | ||
|
|
44fa206619 | ||
|
|
bc10b00874 | ||
|
|
bbd73b36d1 | ||
|
|
3a52b40214 | ||
|
|
c41855ccc4 | ||
|
|
c013b178d7 | ||
|
|
683fffaa0e | ||
|
|
31f67e568c | ||
|
|
9c15d02080 | ||
|
|
c14be070f7 | ||
|
|
7b1410d832 | ||
|
|
06cf112b61 | ||
|
|
95e3410540 | ||
|
|
77d7187116 | ||
|
|
28d18e389c | ||
|
|
d95ae373ab | ||
|
|
a784eeecc2 | ||
|
|
84962c9ed3 | ||
|
|
cfd2e13832 | ||
|
|
880f8f6b39 | ||
|
|
c5b1cc1498 | ||
|
|
343f85f8e7 | ||
|
|
654bc18422 | ||
|
|
81b59af3b4 | ||
|
|
e50d0ac00b | ||
|
|
9fe89c9b18 | ||
|
|
8c20d29806 | ||
|
|
10d93992d4 | ||
|
|
0552d1db5a | ||
|
|
07919409d9 | ||
|
|
d691c1fa11 | ||
|
|
b632df901b | ||
|
|
ecb67ea344 | ||
|
|
4c2e8f7a76 | ||
|
|
22c4581d4d | ||
|
|
34c668b4d9 | ||
|
|
9437edbf8a | ||
|
|
5890b1b2e8 | ||
|
|
5ea26dc7ea | ||
|
|
6afde1fb18 | ||
|
|
708a0836cc | ||
|
|
3fc51ee54a | ||
|
|
e85a75305f | ||
|
|
9f469aec38 | ||
|
|
dd7907da8c | ||
|
|
634c522eaf | ||
|
|
57a924703e | ||
|
|
89285dcfba | ||
|
|
05c14d7bbb | ||
|
|
8c4228016c | ||
|
|
8d00730653 | ||
|
|
38ed272fc2 | ||
|
|
d0a11d6054 | ||
|
|
8022234e84 | ||
|
|
b58bde0461 | ||
|
|
48c7538172 | ||
|
|
cb938d0e33 | ||
|
|
f2544684ec | ||
|
|
131ce44ecc | ||
|
|
b91f8c1661 | ||
|
|
3943a740bc | ||
|
|
d88a4ac103 | ||
|
|
afeaf3a671 | ||
|
|
1a968a7d24 | ||
|
|
ac5f7fa1c9 | ||
|
|
c1cd762da4 | ||
|
|
5f9230c458 | ||
|
|
2381b3372c | ||
|
|
4a07478f1e | ||
|
|
5c4ceb7f0f | ||
|
|
e940ec98d4 | ||
|
|
b7e0f42d47 | ||
|
|
1b300c676b | ||
|
|
a7cc1b9ba2 | ||
|
|
c99449d8e5 | ||
|
|
2038a463db | ||
|
|
4f0e2794ec | ||
|
|
89891a401a | ||
|
|
d572d5157d | ||
|
|
6ffb4a2350 | ||
|
|
1e1ea1c49a | ||
|
|
fb4238b035 | ||
|
|
3d6d7c99c2 | ||
|
|
5bb7d41903 | ||
|
|
47c453e979 | ||
|
|
e16bfdfb03 | ||
|
|
3138aecc37 | ||
|
|
b19583c08d | ||
|
|
3d8a70d676 | ||
|
|
7e6158b847 | ||
|
|
23e82b352e | ||
|
|
2ccd08a8d4 | ||
|
|
6159378a52 | ||
|
|
65039ce820 | ||
|
|
fde633331c | ||
|
|
597b945aca | ||
|
|
d23684c42f | ||
|
|
cb3bcf1b9e | ||
|
|
c8c5c2e7ef | ||
|
|
8886f14996 | ||
|
|
e8c7e81828 | ||
|
|
1f47ca8ea9 | ||
|
|
f61cad986f | ||
|
|
6256bfc3cf | ||
|
|
3133997e78 | ||
|
|
ed9324a0de | ||
|
|
ab668d74eb | ||
|
|
7876de8e2d | ||
|
|
06f00ac8f6 | ||
|
|
97ee639d62 | ||
|
|
314aa62828 | ||
|
|
bcf27ae048 | ||
|
|
ce9503fc5b | ||
|
|
dead74c825 | ||
|
|
24e37d3faa | ||
|
|
385ea34c98 | ||
|
|
e91612b313 | ||
|
|
8feeaa47dc | ||
|
|
ec9d953bad | ||
|
|
84051e4b72 | ||
|
|
6b93032a78 | ||
|
|
a909ba67df | ||
|
|
cf40b53fd0 | ||
|
|
3425dc89be | ||
|
|
8e1e2b6b36 | ||
|
|
4ace04de7a |
No files matched your search
@@ -19,23 +19,6 @@ jobs:
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- run: yarn run compliance:licenses
|
||||
|
||||
audit:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- run: yarn install
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- run: yarn run compliance:audit
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -122,11 +122,20 @@ yarn install
|
||||
yarn build:ui:desktop
|
||||
```
|
||||
|
||||
In windows, UI is generated using docker to provide a stable UI across platforms.
|
||||
|
||||
```cmd
|
||||
yarn install
|
||||
docker-compose -f docker-compose-embedding.yml run build-desktop-production
|
||||
yarn build:ui:desktop:app
|
||||
```
|
||||
|
||||
To provide a signing identify for macOS build, use `BOUNDARY_DESKTOP_SIGNING_IDENTITY`
|
||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)).
|
||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `yarn build:ui:desktop:app`.
|
||||
|
||||
Assets are saved to `ui/desktop/electron-app/out/make/`.
|
||||
DMG packaged desktop UI is available at asset location as `boundary.dmg`.
|
||||
EXE archived desktop UI is available at asset location under `zip` folder.
|
||||
|
||||
### Connect Boundary UI to Boundary local instance
|
||||
|
||||
|
||||
@@ -248,7 +248,11 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
// Normalize field-specific errors, if any.
|
||||
const errors = fieldErrors.map((error) => ({
|
||||
detail: error.description,
|
||||
source: { pointer: `/data/attributes/${error.name}` },
|
||||
// Ember Data cannot handle errors on nested attributes, so these are
|
||||
// flattened (e.g. `attributes.foobar` becomes `attributes_foobar`).
|
||||
// To capture these errors, add read-only fields on the model
|
||||
// corresponding to the underscored field name.
|
||||
source: { pointer: `/data/attributes/${error.name.replace('.', '_')}` },
|
||||
}));
|
||||
// Return a list of JSON API errors rooted under the `errors` key.
|
||||
return {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// app/initializers/fragment-serializer.js
|
||||
import FragmentAuthMethodAttributesAccountClaimMapSerializer from '../serializers/fragment-auth-method-attributes-account-claim-map';
|
||||
|
||||
export function initialize(application) {
|
||||
application.register(
|
||||
'serializer:fragment-auth-method-attributes-account-claim-map',
|
||||
FragmentAuthMethodAttributesAccountClaimMapSerializer
|
||||
);
|
||||
}
|
||||
|
||||
// Fragment serializers must be manually registered
|
||||
export default {
|
||||
name: 'fragment-auth-method-attributes-account-claim-map',
|
||||
initialize: initialize,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// app/initializers/fragment-serializer.js
|
||||
import FragmentStringSerializer from '../serializers/fragment-string';
|
||||
|
||||
export function initialize(application) {
|
||||
application.register('serializer:fragment-string', FragmentStringSerializer);
|
||||
}
|
||||
|
||||
// Fragment serializers must be manually registered
|
||||
export default {
|
||||
name: 'fragment-string',
|
||||
initialize: initialize,
|
||||
};
|
||||
@@ -17,6 +17,11 @@ export default class AccountModel extends GeneratedAccountModel {
|
||||
*/
|
||||
@equal('type', 'password') isPassword;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@equal('type', 'oidc') isOIDC;
|
||||
|
||||
/**
|
||||
* Convenience for getting username in account.
|
||||
* @type {string}
|
||||
|
||||
@@ -1,9 +1,61 @@
|
||||
import GeneratedAuthMethodModel from '../generated/models/auth-method';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
/**
|
||||
* Enum options per auth method type and field.
|
||||
*/
|
||||
export const options = {
|
||||
oidc: {
|
||||
signing_algorithms: [
|
||||
'RS256',
|
||||
'RS384',
|
||||
'RS512',
|
||||
'ES256',
|
||||
'ES384',
|
||||
'ES512',
|
||||
'PS256',
|
||||
'PS384',
|
||||
'PS512',
|
||||
'EdDSA',
|
||||
],
|
||||
state: ['inactive', 'active-private', 'active-public'],
|
||||
account_claim_maps: {
|
||||
to: ['sub', 'name', 'email'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {
|
||||
// =error attributes
|
||||
// These attributes exist solely to capture errors on nested fields.
|
||||
// See the application adapter's error normalization method for
|
||||
// more information.
|
||||
|
||||
@attr('string', { readOnly: true }) attributes_state;
|
||||
@attr('string', { readOnly: true }) attributes_issuer;
|
||||
@attr('string', { readOnly: true }) attributes_client_id;
|
||||
@attr('string', { readOnly: true }) attributes_client_secret;
|
||||
@attr('string', { readOnly: true }) attributes_max_age;
|
||||
@attr('string', { readOnly: true }) attributes_api_url_prefix;
|
||||
@attr('string', { readOnly: true })
|
||||
attributes_disable_discovered_config_validation;
|
||||
@attr('string', { readOnly: true }) attributes_dry_run;
|
||||
@attr('string', { readOnly: true }) attributes_account_claim_maps;
|
||||
@attr('string', { readOnly: true }) attributes_claims_scopes;
|
||||
@attr('string', { readOnly: true }) attributes_signing_algorithms;
|
||||
@attr('string', { readOnly: true }) attributes_allowed_audiences;
|
||||
@attr('string', { readOnly: true }) attributes_idp_ca_certs;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Attributes of this resource, if any, represented as a JSON fragment.
|
||||
* @type {FragmentAuthMethodAttributesModel}
|
||||
*/
|
||||
@fragment('fragment-auth-method-attributes', { defaultValue: {} }) attributes;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@@ -22,4 +74,50 @@ export default class AuthMethodModel extends GeneratedAuthMethodModel {
|
||||
get isOIDC() {
|
||||
return this.type === 'oidc';
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isInactive() {
|
||||
return this.attributes.state === 'inactive';
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPrivate() {
|
||||
return this.attributes.state === 'active-private';
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPublic() {
|
||||
return this.attributes.state === 'active-public';
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Change the active and visibility state of an OIDC auth method
|
||||
* given its ID.
|
||||
* For OIDC auth methods only.
|
||||
* @param {[string]} state
|
||||
* @param {object} options
|
||||
* @param {object} options.adapterOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
changeState(state, options = { adapterOptions: {} }) {
|
||||
const defaultAdapterOptions = {
|
||||
method: 'change-state',
|
||||
state,
|
||||
};
|
||||
return this.save({
|
||||
...options,
|
||||
adapterOptions: {
|
||||
...defaultAdapterOptions,
|
||||
...options.adapterOptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,16 @@ import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
export default class FragmentAccountAttributesModel extends Fragment {
|
||||
// =attributes (password)
|
||||
@attr('string', {
|
||||
for: 'password',
|
||||
description: 'The account login name',
|
||||
})
|
||||
login_name;
|
||||
|
||||
// =attributes (OIDC)
|
||||
@attr('string', { for: 'oidc' }) subject;
|
||||
@attr('string', { for: 'oidc' }) issuer;
|
||||
@attr('string', { for: 'oidc', readOnly: true }) email;
|
||||
@attr('string', { for: 'oidc', readOnly: true }) full_name;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { attr } from '@ember-data/model';
|
||||
|
||||
export default class FragmentAuthMethodAttributesAccountClaimMapModel extends Fragment {
|
||||
// =attributes (OIDC)
|
||||
|
||||
@attr('string') from;
|
||||
@attr('string') to;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { fragmentArray } from 'ember-data-model-fragments/attributes';
|
||||
|
||||
export default class FragmentAuthMethodAttributesModel extends Fragment {
|
||||
// =attributes (OIDC)
|
||||
|
||||
@attr('string', { readOnly: true }) state;
|
||||
@attr('string') issuer;
|
||||
@attr('string') client_id;
|
||||
@attr('string') client_secret;
|
||||
@attr('string', { readOnly: true }) client_secret_hmac;
|
||||
@attr('number') max_age;
|
||||
@attr('string') api_url_prefix;
|
||||
@attr('string', { readOnly: true }) callback_url;
|
||||
@attr('boolean') disable_discovered_config_validation;
|
||||
@attr('boolean') dry_run;
|
||||
|
||||
@fragmentArray('fragment-auth-method-attributes-account-claim-map', {
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
account_claim_maps;
|
||||
|
||||
@fragmentArray('fragment-string', {
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
claims_scopes;
|
||||
|
||||
@fragmentArray('fragment-string', {
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
signing_algorithms;
|
||||
|
||||
@fragmentArray('fragment-string', {
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
allowed_audiences;
|
||||
|
||||
@fragmentArray('fragment-string', { emptyArrayIfMissing: true }) idp_ca_certs;
|
||||
}
|
||||
@@ -20,13 +20,36 @@ export default class AccountSerializer extends ApplicationSerializer {
|
||||
* @return {object}
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
switch (snapshot.record.type) {
|
||||
case 'password':
|
||||
return this.serializePassword(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Password serialization omits `attributes` and handles password-related
|
||||
* custom methods.
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {object}
|
||||
*/
|
||||
serializePassword(snapshot) {
|
||||
const password = snapshot?.adapterOptions?.password;
|
||||
let serialized = super.serialize(...arguments);
|
||||
// Only login_name is serialized into password attributes.
|
||||
const { login_name } = serialized.attributes;
|
||||
if (login_name) {
|
||||
serialized.attributes = { login_name };
|
||||
} else {
|
||||
serialized.attributes = {};
|
||||
}
|
||||
// New record case
|
||||
if (password && snapshot?.record?.isNew)
|
||||
serialized.attributes.password = password;
|
||||
// Set password custom method
|
||||
if (snapshot?.adapterOptions?.method === 'set-password') {
|
||||
serialized = this.serializeForSetPassword(snapshot, password);
|
||||
}
|
||||
// Change password custom method
|
||||
if (snapshot?.adapterOptions?.method === 'change-password') {
|
||||
const { currentPassword, newPassword } = snapshot?.adapterOptions;
|
||||
serialized = this.serializeForChangePassword(
|
||||
|
||||
@@ -1,6 +1,64 @@
|
||||
import ApplicationSerializer from './application';
|
||||
import { copy } from 'ember-copy';
|
||||
|
||||
export default class AuthMethodSerializer extends ApplicationSerializer {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Delegates to a type-specific serialization, or default.
|
||||
* @override
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {object}
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
switch (snapshot.record.type) {
|
||||
case 'oidc':
|
||||
return this.serializeOIDC(...arguments);
|
||||
default:
|
||||
return this.serializeDefault(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default serialization omits `attributes`.
|
||||
* @return {object}
|
||||
*/
|
||||
serializeDefault() {
|
||||
let serialized = super.serialize(...arguments);
|
||||
delete serialized.attributes;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `adapterOptions.state` is set, the serialization should
|
||||
* include **only state** and version. Normally, this is not serialized.
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {object}
|
||||
*/
|
||||
serializeOIDC(snapshot) {
|
||||
let serialized = super.serialize(...arguments);
|
||||
const state = snapshot?.adapterOptions?.state;
|
||||
if (state) {
|
||||
serialized = this.serializeOIDCWithState(snapshot, state);
|
||||
} else {
|
||||
delete serialized.attributes.state;
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a payload containing only state and version.
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {string} state
|
||||
* @return {object}
|
||||
*/
|
||||
serializeOIDCWithState(snapshot, state) {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
attributes: { state },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the array in order of "is_primary" such that the primary method
|
||||
* appears first. The "natural" order of auth methods places primary first.
|
||||
@@ -23,10 +81,19 @@ export default class AuthMethodSerializer extends ApplicationSerializer {
|
||||
*
|
||||
* TODO: generalize this so that all zero values are reified.
|
||||
* @override
|
||||
* @return {object}
|
||||
* @see FragmentScope
|
||||
* @param {Model} typeClass
|
||||
* @param {Object} hash
|
||||
* @return {Object}
|
||||
*/
|
||||
normalize() {
|
||||
const normalized = super.normalize(...arguments);
|
||||
normalize(typeClass, hash, ...rest) {
|
||||
let normalizedHash = copy(hash, true);
|
||||
// switch (normalizedHash.type) {
|
||||
// case 'oidc':
|
||||
// normalizedHash = this.normalizeOIDC(normalizedHash);
|
||||
// break;
|
||||
// }
|
||||
const normalized = super.normalize(typeClass, normalizedHash, ...rest);
|
||||
if (!normalized.data.attributes.is_primary) {
|
||||
normalized.data.attributes.is_primary = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import JSONSerializer from '@ember-data/serializer/json';
|
||||
|
||||
export default class FragmentAccountAttributesSerializer extends JSONSerializer {
|
||||
/**
|
||||
* If an attribute is annotated as readOnly in the model, don't serialize it.
|
||||
* Otherwise delegate to default attribute serializer.
|
||||
*
|
||||
* @override
|
||||
* @method serializeAttribute
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Object} json
|
||||
* @param {String} key
|
||||
* @param {Object} attribute
|
||||
*/
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const { type, options } = attribute;
|
||||
let value = super.serializeAttribute(...arguments);
|
||||
// Convert empty strings to null.
|
||||
if (type === 'string' && json[key] === '') json[key] = null;
|
||||
// Do not serialize read-only attributes.
|
||||
if (options.readOnly) delete json[key];
|
||||
// Client secret cannot be unset. If it's falsy, it must be removed.
|
||||
if (key === 'client_secret' && !json[key]) delete json[key];
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import JSONSerializer from '@ember-data/serializer/json';
|
||||
|
||||
export default class FragmentHostSetSerializer extends JSONSerializer {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Serializes an account claim to string.
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
const from = snapshot.attr('from');
|
||||
const to = snapshot.attr('to');
|
||||
return `${from}=${to}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes account claim strings into JSON API objects.
|
||||
* @param {Model} typeClass
|
||||
* @param {string} value
|
||||
* @return {object}
|
||||
*/
|
||||
normalize(typeClass, value) {
|
||||
const normalizedHash = {
|
||||
from: value.split('=')[0],
|
||||
to: value.split('=')[1],
|
||||
};
|
||||
return super.normalize(typeClass, normalizedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import JSONSerializer from '@ember-data/serializer/json';
|
||||
|
||||
export default class FragmentAuthMethodAttributesSerializer extends JSONSerializer {
|
||||
/**
|
||||
* If an attribute is annotated as readOnly in the model, don't serialize it.
|
||||
* Otherwise delegate to default attribute serializer.
|
||||
*
|
||||
* @override
|
||||
* @method serializeAttribute
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Object} json
|
||||
* @param {String} key
|
||||
* @param {Object} attribute
|
||||
*/
|
||||
serializeAttribute(snapshot, json, key, attribute) {
|
||||
const { type, options } = attribute;
|
||||
let value = super.serializeAttribute(...arguments);
|
||||
// Convert empty strings to null.
|
||||
if (type === 'string' && json[key] === '') json[key] = null;
|
||||
// Do not serialize read-only attributes.
|
||||
if (options.readOnly) delete json[key];
|
||||
// Client secret cannot be unset. If it's falsy, it must be removed.
|
||||
if (key === 'client_secret' && !json[key]) delete json[key];
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import JSONSerializer from '@ember-data/serializer/json';
|
||||
|
||||
export default class FragmentStringSerializer extends JSONSerializer {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Serializes a to string.
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
return snapshot.attr('value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes from strings.
|
||||
* @param {Model} typeClass
|
||||
* @param {string} value
|
||||
* @return {object}
|
||||
*/
|
||||
normalize(typeClass, value) {
|
||||
return super.normalize(typeClass, { value });
|
||||
}
|
||||
}
|
||||
@@ -37,21 +37,4 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
||||
host_ids: hostIDs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string arrays to object arrays for use with `fragment-string`.
|
||||
* E.g. `"foo"` -> `{value: "foo"}`
|
||||
* @override
|
||||
* @param {Model} modelClass
|
||||
* @param {Object} resourceHash
|
||||
* @return {Object}
|
||||
*/
|
||||
normalize(modelClass, resourceHash) {
|
||||
// TODO: can fragment string normalization be handled generically?
|
||||
// TODO: host_ids might actually be attributes.host_ids
|
||||
if (resourceHash.host_ids) {
|
||||
resourceHash.host_ids = resourceHash.host_ids.map((value) => ({ value }));
|
||||
}
|
||||
return super.normalize(modelClass, resourceHash);
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,9 @@ export default class StorageService extends Service {
|
||||
const inMemory = get(config, 'storage.memory');
|
||||
// Get the global context object (window) via service lookup,
|
||||
// because just reaching for a global is icky
|
||||
const globalContext = getOwner(this).lookup('service:-document')
|
||||
.documentElement.parentNode.defaultView;
|
||||
const globalContext =
|
||||
getOwner(this).lookup('service:-document').documentElement.parentNode
|
||||
.defaultView;
|
||||
// If memory storage enabled, return the memory storage instance,
|
||||
// otherwise return localStorage.
|
||||
return inMemory ? this.#memoryStorage : globalContext.localStorage;
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
initialize,
|
||||
} from 'api/initializers/fragment-auth-method-attributes-account-claim-map-serializer';
|
||||
@@ -0,0 +1 @@
|
||||
export { default, initialize } from 'api/initializers/fragment-string';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/fragment-auth-method-attributes-account-claim-map';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/models/fragment-auth-method-attributes';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/fragment-account-attributes';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/fragment-auth-method-attributes';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/fragment-string';
|
||||
@@ -106,8 +106,9 @@ export default function () {
|
||||
}
|
||||
);
|
||||
|
||||
// Authenticate/deauthenticate routes
|
||||
// Authenticate + auth methods custom routes
|
||||
this.post('/auth-methods/:id_method', authHandler);
|
||||
// Deauthenticate route
|
||||
this.post('/scopes/:id_method', deauthHandler);
|
||||
|
||||
// IAM : Users
|
||||
|
||||
@@ -11,4 +11,29 @@ export default Factory.extend({
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => datatype.boolean(),
|
||||
|
||||
attributes: function () {
|
||||
let attrs;
|
||||
switch (this.type) {
|
||||
case 'oidc':
|
||||
attrs = {
|
||||
api_url_prefix: 'http://127.0.0.1:9200',
|
||||
callback_url:
|
||||
'http://127.0.0.1:9200/v1/auth-methods/oidc:authenticate:callback',
|
||||
client_id: 'YTwdJBB55LpKnuXb9n65',
|
||||
client_secret_hmac: 'GY7blb4ynM5TEPNi_qnphJ9aDWhW8FpmN3D-eP_L7Zk',
|
||||
issuer: 'http://127.0.0.1:57070',
|
||||
max_age: 1800,
|
||||
disable_discovered_config_validation: false,
|
||||
state: 'active-public',
|
||||
account_claim_maps: ['oid=sub', 'full_name=name'],
|
||||
claims_scopes: ['profile', 'email'],
|
||||
signing_algorithms: ['RS256', 'RS384'],
|
||||
allowed_audiences: ['www.alice.com', 'www.alice.com/admin'],
|
||||
idp_ca_certs: ['certificate-1234', 'certificate-5678'],
|
||||
};
|
||||
break;
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
});
|
||||
@@ -107,14 +107,34 @@ const commandHandlers = {
|
||||
},
|
||||
};
|
||||
|
||||
// Handles all custom methods on /auth-methods/:id route
|
||||
export function authHandler({ scopes, authMethods }, request) {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
const [, id] = request.params.id_method.match(/(?<id>.[^:]*)/);
|
||||
const { command } = payload;
|
||||
const [, id, method] = request.params.id_method.match(
|
||||
/(?<id>.[^:]*):(?<method>(.*))/
|
||||
);
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = scopes.find(authMethod.scopeId);
|
||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
|
||||
|
||||
if (method === 'authenticate') {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
const { command } = payload;
|
||||
const scope = scopes.find(authMethod.scopeId);
|
||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
|
||||
}
|
||||
|
||||
// TODO: this handler doesn't really belong here, but we already route
|
||||
// POST requests on existing auth methods to this route handler file under the
|
||||
// assumption it would be for authentication. While authentication should
|
||||
// still occur here, we should handle other custom methods in the
|
||||
// mirage config and route here only for auth.
|
||||
if (method === 'change-state') {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
return authMethod.update({
|
||||
attributes: {
|
||||
state: attrs.attributes.state,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function deauthHandler() {
|
||||
|
||||
@@ -60,6 +60,28 @@ export default function initializeMockIPC(server) {
|
||||
session_id: newSession.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for browsers running on mac OS
|
||||
*/
|
||||
isMacOS() {
|
||||
return Boolean(window.navigator.userAgent.match(/(macintosh)/i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for browsers running on windows OS
|
||||
*/
|
||||
isWindowsOS() {
|
||||
return Boolean(window.navigator.userAgent.match(/(windows)/i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing when attempting to minimize, toggle fullscreen,
|
||||
* and close a browser window
|
||||
*/
|
||||
minimizeWindow() {}
|
||||
closeWindow() {}
|
||||
toggleFullScreenWindow() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"ember-cli-htmlbars": "^5.3.1",
|
||||
"ember-cli-mirage": "^1.1.8",
|
||||
"ember-data": "^3.20.0",
|
||||
"ember-data-model-fragments": "5.0.0-beta.0",
|
||||
"ember-data-model-fragments": "5.0.0-beta.2",
|
||||
"ember-fetch": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,7 +38,7 @@
|
||||
"@glimmer/tracking": "^1.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"doctoc": "^2.0.0",
|
||||
"ember-auto-import": "^1.10.1",
|
||||
"ember-cli": "~3.24.0",
|
||||
"ember-cli-dependency-checker": "^3.2.0",
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import Application from '@ember/application';
|
||||
|
||||
import { initialize } from 'dummy/initializers/fragment-auth-method-attributes-account-claim-map-serializer';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module(
|
||||
'Unit | Initializer | fragment-auth-method-attributes-account-claim-map-serializer',
|
||||
function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {};
|
||||
this.TestApplication.initializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
Resolver,
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import Application from '@ember/application';
|
||||
|
||||
import { initialize } from 'dummy/initializers/fragment-string';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Initializer | fragment-string', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.TestApplication = class TestApplication extends Application {};
|
||||
this.TestApplication.initializer({
|
||||
name: 'initializer under test',
|
||||
initialize,
|
||||
});
|
||||
|
||||
this.application = this.TestApplication.create({
|
||||
autoboot: false,
|
||||
Resolver,
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,49 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Model | auth method', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('auth-method', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
|
||||
test('it has a `changeState` method that targets a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post(
|
||||
'/v1/auth-methods/123abc:change-state',
|
||||
(schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
attributes: {
|
||||
state: 'foobar',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
return { id: '123abc' };
|
||||
}
|
||||
);
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
type: 'auth-method',
|
||||
attributes: {
|
||||
type: 'oidc',
|
||||
state: 'foobar',
|
||||
version: 1,
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('auth-method', '123abc');
|
||||
await model.changeState('foobar');
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Model | auth token', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('auth-token', {});
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module(
|
||||
'Unit | Model | fragment auth method attributes account claim map',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord(
|
||||
'fragment-auth-method-attributes-account-claim-map',
|
||||
{}
|
||||
);
|
||||
assert.ok(model);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Model | fragment auth method attributes', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('fragment-auth-method-attributes', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Model | fragment host set', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('fragment-host-set', {});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Model | fragment principal', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('fragment-principal', {});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Model | fragment string', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('fragment-string', {});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Model | host', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('host', {});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Serializer | auth method', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('auth-method');
|
||||
@@ -12,13 +13,94 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
test('it serializes password records', function (assert) {
|
||||
assert.expect(2);
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('auth-method', {});
|
||||
let record = store.createRecord('auth-method', { type: 'password' });
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
assert.notOk(
|
||||
serializedRecord.attributes,
|
||||
'Password should not have attributes'
|
||||
);
|
||||
});
|
||||
|
||||
test('it serializes OIDC records without state', function (assert) {
|
||||
assert.expect(1);
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('auth-method', {
|
||||
type: 'oidc',
|
||||
name: 'OIDC Auth Method',
|
||||
attributes: {
|
||||
state: 'foo',
|
||||
account_claim_maps: [{ from: 'foo', to: 'bar' }],
|
||||
claims_scopes: [{ value: 'profile' }, { value: 'email' }],
|
||||
signing_algorithms: [{ value: 'RS256' }, { value: 'RS384' }],
|
||||
allowed_audiences: [
|
||||
{ value: 'www.alice.com' },
|
||||
{ value: 'www.alice.com/admin' },
|
||||
],
|
||||
idp_ca_certs: [
|
||||
{ value: 'certificate-1234' },
|
||||
{ value: 'certificate-5678' },
|
||||
],
|
||||
api_url_prefix: 'protocol://host:port/foo',
|
||||
client_id: 'id123',
|
||||
client_secret: 'secret456',
|
||||
disable_discovered_config_validation: true,
|
||||
dry_run: true,
|
||||
issuer: 'http://www.example.net',
|
||||
max_age: 500,
|
||||
},
|
||||
});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.deepEqual(serializedRecord, {
|
||||
type: 'oidc',
|
||||
name: 'OIDC Auth Method',
|
||||
description: null,
|
||||
attributes: {
|
||||
account_claim_maps: ['foo=bar'],
|
||||
claims_scopes: ['profile', 'email'],
|
||||
signing_algorithms: ['RS256', 'RS384'],
|
||||
allowed_audiences: ['www.alice.com', 'www.alice.com/admin'],
|
||||
idp_ca_certs: ['certificate-1234', 'certificate-5678'],
|
||||
api_url_prefix: 'protocol://host:port/foo',
|
||||
client_id: 'id123',
|
||||
client_secret: 'secret456',
|
||||
disable_discovered_config_validation: true,
|
||||
dry_run: true,
|
||||
issuer: 'http://www.example.net',
|
||||
max_age: 500,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes OIDC records with only state and version when `adapterOptions.state` is passed', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('auth-method');
|
||||
const record = store.createRecord('auth-method', {
|
||||
type: 'oidc',
|
||||
attributes: {
|
||||
state: 'foo',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {
|
||||
state: 'bar',
|
||||
};
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
assert.deepEqual(serializedRecord, {
|
||||
attributes: {
|
||||
state: 'bar',
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('it sorts primary first in normalizeArrayResponse', function (assert) {
|
||||
@@ -63,4 +145,22 @@ module('Unit | Serializer | auth method', function (hooks) {
|
||||
'First normalized item is primary'
|
||||
);
|
||||
});
|
||||
|
||||
test('it normalizes OIDC records', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
this.server.get('/v1/auth-methods/oidc123', () => ({
|
||||
attributes: {
|
||||
account_claim_maps: ['from=to', 'foo=bar'],
|
||||
},
|
||||
version: 1,
|
||||
type: 'oidc',
|
||||
id: 'oidc123',
|
||||
}));
|
||||
const record = await store.findRecord('auth-method', 'oidc123');
|
||||
const { account_claim_maps } = record.attributes;
|
||||
assert.equal(account_claim_maps.firstObject.from, 'from');
|
||||
assert.equal(account_claim_maps.firstObject.to, 'to');
|
||||
assert.equal(account_claim_maps.lastObject.from, 'foo');
|
||||
assert.equal(account_claim_maps.lastObject.to, 'bar');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | fragment account attributes', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('fragment-account-attributes');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('fragment-account-attributes', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | fragment auth method attributes', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it serializes empty strings to null', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('user', {
|
||||
name: '',
|
||||
description: '',
|
||||
version: null,
|
||||
});
|
||||
let serializedRecord = record.serialize();
|
||||
assert.deepEqual(serializedRecord, {
|
||||
name: null,
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('it does not serialize client_secret if present but falsy', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('user', {
|
||||
name: '',
|
||||
description: '',
|
||||
version: null,
|
||||
client_secret: false,
|
||||
});
|
||||
let serializedRecord = record.serialize();
|
||||
assert.deepEqual(serializedRecord, {
|
||||
name: null,
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | fragment string', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('fragment-string');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('fragment-string', {
|
||||
value: 'string value',
|
||||
});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.equal(serializedRecord, 'string value');
|
||||
});
|
||||
|
||||
test('it normalizes records', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('fragment-string');
|
||||
const modelClass = store.createRecord('fragment-string').constructor;
|
||||
const payload = 'string value';
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
modelClass,
|
||||
payload
|
||||
);
|
||||
assert.deepEqual(normalized, {
|
||||
included: [],
|
||||
data: {
|
||||
id: null,
|
||||
type: 'fragment-string',
|
||||
attributes: {
|
||||
value: 'string value',
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Serializer | host', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('host');
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Serializer | session', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('session');
|
||||
|
||||
@@ -72,10 +72,9 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
*/
|
||||
async restore(data) {
|
||||
if (!data) return reject();
|
||||
return this.validateToken(
|
||||
data.attributes.token,
|
||||
data.attributes.id
|
||||
).then(() => this.normalizeData(data));
|
||||
return this.validateToken(data.attributes.token, data.attributes.id).then(
|
||||
() => this.normalizeData(data)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"babel-eslint": "^10.1.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"core": "*",
|
||||
"doctoc": "^1.4.0",
|
||||
"doctoc": "^2.0.0",
|
||||
"ember-auto-import": "^1.10.1",
|
||||
"ember-cli": "~3.24.0",
|
||||
"ember-cli-dependency-checker": "^3.2.0",
|
||||
|
||||
@@ -7,6 +7,25 @@ helpers.
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
- [Add core to an App](#add-core-to-an-app)
|
||||
- [Installation](#installation)
|
||||
- [I18n](#i18n)
|
||||
- [Notifications](#notifications)
|
||||
- [Confirmations](#confirmations)
|
||||
- [App and Company Names](#app-and-company-names)
|
||||
- [Documentation URLs](#documentation-urls)
|
||||
- [Loading Helper](#loading-helper)
|
||||
- [Scope Service](#scope-service)
|
||||
- [Yarn Commands](#yarn-commands)
|
||||
- [Linting](#linting)
|
||||
- [Formatting](#formatting)
|
||||
- [Running tests](#running-tests)
|
||||
- [Running the dummy application](#running-the-dummy-application)
|
||||
- [Contributing](#contributing)
|
||||
- [Building ToC](#building-toc)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
- [Add core to an App](#add-core-to-an-app)
|
||||
- [Installation](#installation)
|
||||
- [Yarn Commands](#yarn-commands)
|
||||
|
||||
@@ -76,8 +76,8 @@ export default class OIDCAuthenticator extends BaseOIDCAuthenticator {
|
||||
* @param {string} token
|
||||
*/
|
||||
addTokenToAuthorization(token) {
|
||||
const adapterPrototype = this.store.adapterFor('application').constructor
|
||||
.prototype;
|
||||
const adapterPrototype =
|
||||
this.store.adapterFor('application').constructor.prototype;
|
||||
const headers = adapterPrototype?.headers;
|
||||
if (!headers) adapterPrototype.headers = {};
|
||||
adapterPrototype.headers.Authorization = null;
|
||||
|
||||
@@ -91,8 +91,8 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* @param {string} token
|
||||
*/
|
||||
addTokenToAuthorization(token) {
|
||||
const adapterPrototype = this.store.adapterFor('application').constructor
|
||||
.prototype;
|
||||
const adapterPrototype =
|
||||
this.store.adapterFor('application').constructor.prototype;
|
||||
const headers = adapterPrototype?.headers;
|
||||
if (!headers) adapterPrototype.headers = {};
|
||||
adapterPrototype.headers.Authorization = null;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default, appName } from 'core/helpers/app-name';
|
||||
export { default } from 'core/helpers/app-name';
|
||||
@@ -1 +1 @@
|
||||
export { default, companyCopyright } from 'core/helpers/company-copyright';
|
||||
export { default } from 'core/helpers/company-copyright';
|
||||
@@ -1 +1 @@
|
||||
export { default, companyName } from 'core/helpers/company-name';
|
||||
export { default } from 'core/helpers/company-name';
|
||||
@@ -1 +1 @@
|
||||
export { default, docUrl } from 'core/helpers/doc-url';
|
||||
export { default } from 'core/helpers/doc-url';
|
||||
@@ -1,4 +1 @@
|
||||
export {
|
||||
default,
|
||||
formatDateIsoHuman,
|
||||
} from 'core/helpers/format-date-iso-human';
|
||||
export { default } from 'core/helpers/format-date-iso-human';
|
||||
@@ -1 +1 @@
|
||||
export { default, formatDateIso } from 'core/helpers/format-date-iso';
|
||||
export { default } from 'core/helpers/format-date-iso';
|
||||
@@ -1 +1 @@
|
||||
export { default, isLoading } from 'core/helpers/is-loading';
|
||||
export { default } from 'core/helpers/is-loading';
|
||||
@@ -48,7 +48,7 @@
|
||||
"@glimmer/tracking": "^1.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"doctoc": "^2.0.0",
|
||||
"ember-auto-import": "^1.10.1",
|
||||
"ember-cli": "~3.24.0",
|
||||
"ember-cli-dependency-checker": "^3.2.0",
|
||||
|
||||
@@ -31,3 +31,4 @@ set-password: Set Password
|
||||
change-password: Change Password
|
||||
edit-form: Edit Form
|
||||
change-origin: Change Origin URL
|
||||
change-state: Change State
|
||||
@@ -27,6 +27,8 @@ titles:
|
||||
abandon-confirm: Discard unsaved changes?
|
||||
toggle-color-theme: Toggle Color Theme
|
||||
welcome-to-boundary: Welcome to Boundary
|
||||
label: Label
|
||||
value: Value
|
||||
descriptions:
|
||||
empty-set: There are no items to display yet. You may be able to add items or try back later.
|
||||
origin-initialization: To get started, please enter your origin URL
|
||||
|
||||
@@ -51,3 +51,60 @@ origin:
|
||||
description: The Boundary URL to which to connect (e.g. protocol://host:port)
|
||||
proxy:
|
||||
label: Proxy
|
||||
issuer:
|
||||
label: Issuer
|
||||
help: The issuer URL. Boundary expects only the schema, host, and port and will strip off ".well-known/openid-configuration" if present. This will be used for configuration discovery as well as for validation of the "iss" claim.
|
||||
client_id:
|
||||
label: Client ID
|
||||
help: An OAuth 2.0 Client Identifier valid at the Authorization Server.
|
||||
client_secret:
|
||||
label: Client Secret
|
||||
help: The client secret is set-only and may not be unset.
|
||||
client_secret_hmac:
|
||||
label: Client Secret HMAC
|
||||
max_age:
|
||||
label: Maximum Age
|
||||
help: The OIDC "max_age" parameter sent to the Authorization Server indicating a maximum acceptable time in seconds since the user's last authentication before requiring the user to reauthenticate. 0 indicates an immediate need to reauthenticate.
|
||||
api_url_prefix:
|
||||
label: API URL Prefix
|
||||
help: The prefix that should be used for any URLs needed during the authentication flow. This includes the callback URL, the token retrieval URL, and the redirection URL used by the OIDC Authorization Server.
|
||||
disable_discovered_config_validation:
|
||||
label: Disable discovered config validation
|
||||
help: >
|
||||
If the Authorization Server's discovered configuration contains values that do not match the configuration set on this auth method, this can be set to force the local configuration to override the discovered values. This is useful in the case of slow-changing or incorrect values announced by the Authorization Server. NOTE: For safety, not providing this value will be treated the same as "false"; that is, every time the method is updated or the state is changed, this value must be set to "true" or it will be disabled.
|
||||
idp_ca_certs:
|
||||
label: Certificates
|
||||
help: Optional PEM-encoded X.509 CA certificates that can be used as trust anchors when connecting to an OIDC provider.
|
||||
certificate:
|
||||
label: Certificate
|
||||
allowed_audiences:
|
||||
label: Allowed Audiences
|
||||
help: Allowed audience claims for this auth method.
|
||||
claim:
|
||||
label: Claim
|
||||
claims_scopes:
|
||||
label: Claims Scopes
|
||||
help: Optional claims scopes that will be requested during authentication.
|
||||
claims_scope:
|
||||
label: Claims Scope
|
||||
account_claim_maps:
|
||||
label: Account Claim Maps
|
||||
help: >
|
||||
Optional claim maps from custom claims to the standard claims of sub, name and email. These maps are represented as key=value where the key equals the from_claim and the value equals the to_claim. For example "oid=sub".
|
||||
account_claim_map:
|
||||
label: Account Claim Map
|
||||
from_claim:
|
||||
label: From Claim
|
||||
to_claim:
|
||||
label: To Claim
|
||||
signing_algorithms:
|
||||
label: Signing Algorithms
|
||||
help: The signing algorithms allowed for the auth method.
|
||||
algorithm:
|
||||
label: Algorithm
|
||||
subject:
|
||||
label: Subject
|
||||
email:
|
||||
label: Email
|
||||
full_name:
|
||||
label: Full Name
|
||||
@@ -10,6 +10,7 @@ account:
|
||||
description: Set a password for the account.
|
||||
types:
|
||||
password: Password
|
||||
oidc: OIDC
|
||||
actions:
|
||||
create: Create Account
|
||||
delete: Delete Account
|
||||
@@ -30,6 +31,13 @@ auth-method:
|
||||
pending:
|
||||
title: Authentication Pending
|
||||
description: Complete authentication in the newly-opened window.
|
||||
titles:
|
||||
new-signing-algorithm: New Signing Algorithm
|
||||
new-allowed-audience: New Allowed Audience
|
||||
new-claims-scope: New Claims Scope
|
||||
new-from-claim: New From Claim
|
||||
new-to-claim: New To Claim
|
||||
new-certificate: New Certificate
|
||||
types:
|
||||
password: Password
|
||||
oidc: OIDC
|
||||
|
||||
@@ -8,3 +8,6 @@ connection:
|
||||
success:
|
||||
description: "Local proxy address ({protocol}): {address}:{port}"
|
||||
primary: Primary
|
||||
inactive: Inactive
|
||||
active-private: Private
|
||||
active-public: Public
|
||||
@@ -13,6 +13,7 @@ Boundary UIs.
|
||||
- [Linting](#linting)
|
||||
- [Formatting](#formatting)
|
||||
- [Building Components](#building-components)
|
||||
- [Using Components](#using-components)
|
||||
- [Running tests](#running-tests)
|
||||
- [Running the dummy application](#running-the-dummy-application)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
disabled={{@disabled}}
|
||||
@type="checkbox"
|
||||
@checked={{if @checked true false}}
|
||||
aria-describedby="{{if @error (concat 'errors-' this.id)}}" />
|
||||
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}" />
|
||||
|
||||
<label class="rose-form-checkbox-label" for={{this.id}}>
|
||||
<span class="rose-form-checkbox-label-text">{{@label}}</span>
|
||||
@@ -17,6 +17,12 @@
|
||||
{{/if}}
|
||||
</label>
|
||||
|
||||
{{#if @helperText}}
|
||||
<Rose::Form::HelperText @id="helper-text-{{this.id}}" @error={{@error}}>
|
||||
{{@helperText}}
|
||||
</Rose::Form::HelperText>
|
||||
{{/if}}
|
||||
|
||||
{{yield (hash
|
||||
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
|
||||
)}}
|
||||
|
||||
@@ -26,6 +26,7 @@ Renders a form input checkbox.
|
||||
@name={{name}}
|
||||
@label={{label}}
|
||||
@description={{description}}
|
||||
@helperText={{helperText}}
|
||||
@checked={{checked}}
|
||||
@error={{error}}
|
||||
@disabled={{disabled}}
|
||||
@@ -33,7 +34,8 @@ Renders a form input checkbox.
|
||||
`,
|
||||
context: {
|
||||
label: text('text', 'Checkbox 🛎'),
|
||||
description: text('description', 'This is an optional description paragraph.'),
|
||||
description: text('description', 'Short description in label'),
|
||||
helperText: text('helperText', 'This is an optional help text.'),
|
||||
name: text('name', 'checkbox-1'),
|
||||
checked: boolean('checked', false),
|
||||
error: boolean('error', false),
|
||||
|
||||
@@ -30,5 +30,6 @@
|
||||
cancel=this.handleCancel
|
||||
enableEdit=this.enableEdit
|
||||
)
|
||||
isEditable=this.isEditable
|
||||
)}}
|
||||
</form>
|
||||
@@ -1,16 +1,13 @@
|
||||
<div class="rose-form-select">
|
||||
|
||||
<Rose::Form::Label @for={{this.id}} @error={{@error}}>
|
||||
{{@label}}
|
||||
</Rose::Form::Label>
|
||||
|
||||
{{#if @helperText}}
|
||||
<Rose::Form::HelperText @id="helper-text-{{this.id}}" @error={{@error}}>
|
||||
{{@helperText}}
|
||||
</Rose::Form::HelperText>
|
||||
{{/if}}
|
||||
|
||||
{{#if @contextual}}
|
||||
{{yield (hash
|
||||
label=(component 'rose/form/label'
|
||||
for=this.id
|
||||
error=@error
|
||||
)
|
||||
helperText=(component 'rose/form/helper-text'
|
||||
id=(concat 'helper-text-' this.id)
|
||||
error=@error
|
||||
)
|
||||
field=(component 'rose/form/select/select'
|
||||
id=this.id
|
||||
name=@name
|
||||
@@ -20,7 +17,35 @@
|
||||
error=@error
|
||||
aria-describedby=(concat (if @helperText (concat 'helper-text-' this.id)) (if @error (concat ' errors-' this.id)))
|
||||
)
|
||||
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
|
||||
errors=(component 'rose/form/errors'
|
||||
id=(concat 'errors-' this.id)
|
||||
)
|
||||
)}}
|
||||
{{else}}
|
||||
<div class="rose-form-select">
|
||||
|
||||
</div>
|
||||
<Rose::Form::Label @for={{this.id}} @error={{@error}}>
|
||||
{{@label}}
|
||||
</Rose::Form::Label>
|
||||
|
||||
{{#if @helperText}}
|
||||
<Rose::Form::HelperText @id="helper-text-{{this.id}}" @error={{@error}}>
|
||||
{{@helperText}}
|
||||
</Rose::Form::HelperText>
|
||||
{{/if}}
|
||||
|
||||
{{yield (hash
|
||||
field=(component 'rose/form/select/select'
|
||||
id=this.id
|
||||
name=@name
|
||||
value=@value
|
||||
onChange=@onChange
|
||||
disabled=@disabled
|
||||
error=@error
|
||||
aria-describedby=(concat (if @helperText (concat 'helper-text-' this.id)) (if @error (concat ' errors-' this.id)))
|
||||
)
|
||||
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
|
||||
)}}
|
||||
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -112,3 +112,28 @@ Renders a form select element.
|
||||
}
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
<Preview>
|
||||
<Story name="Fully Contextual Select">{{
|
||||
template: hbs`
|
||||
<Rose::Form::Select @value="value-3" @error={{true}} @contextual={{true}} as |field|>
|
||||
<hr />
|
||||
<field.label>Label</field.label>
|
||||
<hr />
|
||||
<field.helperText>Help</field.helperText>
|
||||
<hr />
|
||||
<field.field as |select|>
|
||||
<select.option @value="value-1">Value 1</select.option>
|
||||
<select.option @value="value-2">Value 2</select.option>
|
||||
<select.option @value="value-3">Value 3</select.option>
|
||||
</field.field>
|
||||
<hr />
|
||||
<field.errors as |errors|>
|
||||
<errors.message>An error occurred.</errors.message>
|
||||
</field.errors>
|
||||
<hr />
|
||||
</Rose::Form::Select>
|
||||
`,
|
||||
context: {},
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -1,26 +1,50 @@
|
||||
<div class="rose-form-input">
|
||||
|
||||
<Rose::Form::Label @for={{this.id}} @error={{@error}}>
|
||||
{{@label}}
|
||||
</Rose::Form::Label>
|
||||
|
||||
{{#if @helperText}}
|
||||
<Rose::Form::HelperText @id="helper-text-{{this.id}}" @error={{@error}}>
|
||||
{{@helperText}}
|
||||
</Rose::Form::HelperText>
|
||||
{{/if}}
|
||||
|
||||
<Rose::Form::Textarea::Textarea
|
||||
...attributes
|
||||
@id={{this.id}}
|
||||
@name={{@name}}
|
||||
@value={{@value}}
|
||||
@disabled={{@disabled}}
|
||||
@error={{@error}}
|
||||
@aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}" />
|
||||
|
||||
{{#if @contextual}}
|
||||
{{yield (hash
|
||||
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
|
||||
label=(component 'rose/form/label'
|
||||
for=this.id
|
||||
error=@error
|
||||
)
|
||||
helperText=(component 'rose/form/helper-text'
|
||||
id=(concat 'helper-text-' this.id)
|
||||
error=@error
|
||||
)
|
||||
field=(component 'rose/form/textarea/textarea'
|
||||
id=this.id
|
||||
name=@name
|
||||
value=@value
|
||||
disabled=@disabled
|
||||
error=@error
|
||||
aria-describedby=(concat (if @helperText (concat 'helper-text-' this.id)) (if @error (concat ' errors-' this.id)))
|
||||
)
|
||||
errors=(component 'rose/form/errors'
|
||||
id=(concat 'errors-' this.id)
|
||||
)
|
||||
)}}
|
||||
{{else}}
|
||||
<div class="rose-form-input">
|
||||
|
||||
</div>
|
||||
<Rose::Form::Label @for={{this.id}} @error={{@error}}>
|
||||
{{@label}}
|
||||
</Rose::Form::Label>
|
||||
|
||||
{{#if @helperText}}
|
||||
<Rose::Form::HelperText @id="helper-text-{{this.id}}" @error={{@error}}>
|
||||
{{@helperText}}
|
||||
</Rose::Form::HelperText>
|
||||
{{/if}}
|
||||
|
||||
<Rose::Form::Textarea::Textarea
|
||||
...attributes
|
||||
@id={{this.id}}
|
||||
@name={{@name}}
|
||||
@value={{@value}}
|
||||
@disabled={{@disabled}}
|
||||
@error={{@error}}
|
||||
@aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}" />
|
||||
|
||||
{{yield (hash
|
||||
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
|
||||
)}}
|
||||
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -70,3 +70,24 @@ Renders a simple form textarea.
|
||||
}
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
<Preview>
|
||||
<Story name="Fully Contextual Textarea">{{
|
||||
template: hbs`
|
||||
<Rose::Form::Textarea @value="Text" @error={{true}} @contextual={{true}} as |field|>
|
||||
<hr />
|
||||
<field.label>Label</field.label>
|
||||
<hr />
|
||||
<field.helperText>Help</field.helperText>
|
||||
<hr />
|
||||
<field.field />
|
||||
<hr />
|
||||
<field.errors as |errors|>
|
||||
<errors.message>An error occurred.</errors.message>
|
||||
</field.errors>
|
||||
<hr />
|
||||
</Rose::Form::Textarea>
|
||||
`,
|
||||
context: {},
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -1,4 +1,4 @@
|
||||
<table ...attributes class="rose-table">
|
||||
<table ...attributes class="rose-table {{if @style (concat "rose-table-" @style)}}">
|
||||
{{#if @caption}}
|
||||
<caption>{{@caption}}</caption>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
|
||||
<Meta title="Rose|Table" component="Table" />
|
||||
|
||||
# Table
|
||||
|
||||
A table component to display data in structure of rows and columns. It supports `header`, `body` and `footer` optional definitions. Each definition allows for a table `row` to be defined with either a `cell` or `headerCell` table element.
|
||||
A table component to display data in structure of rows and columns. It supports `header`, `body` and `footer` optional definitions. Each definition allows for a table `row` to be defined with either a `cell` or `headerCell` table element. Rows can be visually hidden, and cells can be shrinked to its content with, and center/right cell alignment is supported.
|
||||
|
||||
```htmlbars
|
||||
<Rose::Table as |table|>
|
||||
Cell shrink should be applied to all the cells in a row. Otherwise the bigger cell with shrink applied will determinate the width of the row.
|
||||
|
||||
```html
|
||||
<Rose::Table @style={{style}} as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<header.row @hidden={{hideHeaderRow}} as |row|>
|
||||
<row.headerCell>Address</row.headerCell>
|
||||
<row.headerCell>Host Set</row.headerCell>
|
||||
<row.headerCell>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
<row.headerCell @alignCenter={{true}}>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-1, host-set-2</row.cell>
|
||||
<row.cell>optional name</row.cell>
|
||||
<row.cell @shrink={{true}}>optional name</row.cell>
|
||||
<row.cell @alignCenter={{true}}>description</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-2</row.cell>
|
||||
<row.cell @shrink={{true}} @alignCenter={{true}}>optional name</row.cell>
|
||||
<row.cell @alignRight={{true}}>description</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-1</row.cell>
|
||||
<row.cell @shrink={{true}}>optional name</row.cell>
|
||||
<row.cell>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
@@ -36,22 +51,34 @@ A table component to display data in structure of rows and columns. It supports
|
||||
<Preview>
|
||||
<Story name="Basic">{{
|
||||
template: hbs`
|
||||
<Rose::Table as |table|>
|
||||
<Rose::Table @style={{style}} as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>Address</row.headerCell>
|
||||
<row.headerCell>Host Set</row.headerCell>
|
||||
<row.headerCell>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
<header.row @hidden={{hideHeaderRow}} as |row|>
|
||||
<row.headerCell @shrink={{cellShrink}} @alignCenter={{cellAlignmentCenter}} @alignRight={{cellAlignmentRight}}>Address</row.headerCell>
|
||||
<row.headerCell @alignCenter={{cellAlignmentCenter}} @alignRight={{cellAlignmentRight}}>Host Set</row.headerCell>
|
||||
<row.headerCell @shrink={{cellShrink}} @alignCenter={{cellAlignmentCenter}} @alignRight={{cellAlignmentRight}}>Name</row.headerCell>
|
||||
<row.headerCell @alignCenter={{cellAlignmentCenter}} @alignRight={{cellAlignmentRight}}>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell @shrink={{cellShrink}}>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-1, host-set-2</row.cell>
|
||||
<row.cell>optional name</row.cell>
|
||||
<row.cell @shrink={{cellShrink}}>optional name</row.cell>
|
||||
<row.cell>description</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell @shrink={{cellShrink}} @alignCenter={{cellAlignmentCenter}}>1.1.1.1</row.cell>
|
||||
<row.cell @alignCenter={{cellAlignmentCenter}}>host-set-2</row.cell>
|
||||
<row.cell @shrink={{cellShrink}} @alignCenter={{cellAlignmentCenter}}>optional name</row.cell>
|
||||
<row.cell @alignCenter={{cellAlignmentCenter}}>description</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell @shrink={{cellShrink}} @alignRight={{cellAlignmentRight}}>1.1.1.1</row.cell>
|
||||
<row.cell @alignRight={{cellAlignmentRight}}>host-set-1</row.cell>
|
||||
<row.cell @shrink={{cellShrink}} @alignRight={{cellAlignmentRight}}>optional name</row.cell>
|
||||
<row.cell @alignRight={{cellAlignmentRight}}>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer as |footer|>
|
||||
<footer.row as |row|>
|
||||
@@ -60,6 +87,102 @@ A table component to display data in structure of rows and columns. It supports
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
`,
|
||||
context: {}
|
||||
context: {
|
||||
hideHeaderRow: boolean('hideHeaderRow', false),
|
||||
cellShrink: boolean('cellShrink', false),
|
||||
style: select(
|
||||
'style',
|
||||
{
|
||||
'default': '',
|
||||
'condensed': 'condensed'
|
||||
}
|
||||
),
|
||||
cellAlignmentCenter: boolean('cellAlignmentCenter', false),
|
||||
cellAlignmentRight: boolean('cellAlignmentRight', false),
|
||||
},
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
<Preview>
|
||||
<Story name="Basic Key/Value Table">{{
|
||||
template: hbs`
|
||||
<Rose::Table @style={{style}} as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell @shrink={{true}}>Key</row.headerCell>
|
||||
<row.headerCell colspan="2">Value</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Input @contextual={{true}} as |input|>
|
||||
<input.field />
|
||||
</Rose::Form::Input>
|
||||
</row.cell>
|
||||
<row.cell @shrink={{true}} @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary" @iconOnly="trash" title="Remove">Remove</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Select
|
||||
@name="Test select"
|
||||
as |field|
|
||||
>
|
||||
<field.field as |select|>
|
||||
<select.option>Choose an option</select.option>
|
||||
<select.option @value="value-1">Value 1</select.option>
|
||||
<select.option @value="value-2">Value 2</select.option>
|
||||
<select.option @value="value-3">Value 3</select.option>
|
||||
</field.field>
|
||||
</Rose::Form::Select>
|
||||
</row.cell>
|
||||
<row.cell @shrink={{true}} @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary">Add</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Input @contextual={{true}} as |input|>
|
||||
<input.field />
|
||||
</Rose::Form::Input>
|
||||
</row.cell>
|
||||
<row.cell @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary">Add</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Textarea
|
||||
@name="field"
|
||||
readonly={{false}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary">Add</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer @hidden={{true}} as |footer|>
|
||||
<footer.row as |row|>
|
||||
<row.cell colspan="3">Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
`,
|
||||
context: {
|
||||
style: select(
|
||||
'style',
|
||||
{
|
||||
'condensed': 'condensed',
|
||||
'default': ''
|
||||
}
|
||||
)
|
||||
}
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -0,0 +1 @@
|
||||
{{yield}}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Component from '@ember/component';
|
||||
import layout from './index';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
tagName: 'td',
|
||||
classNameBindings: ['cellTypeStyle', 'cellShrink', 'cellAlignmentStyle'],
|
||||
cellTypeStyle: computed(function () {
|
||||
return this.tagName.match('th')
|
||||
? `rose-table-header-cell`
|
||||
: 'rose-table-cell';
|
||||
}),
|
||||
cellShrink: computed(function () {
|
||||
if (this.shrink) {
|
||||
return 'rose-table-cell-shrink';
|
||||
}
|
||||
}),
|
||||
cellAlignmentStyle: computed(function () {
|
||||
if (this.alignRight) {
|
||||
return 'align-right';
|
||||
}
|
||||
|
||||
if (this.alignCenter) {
|
||||
return 'align-center';
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
<tr ...attributes class='rose-table-row'>
|
||||
<tr ...attributes class='rose-table-row {{if @hidden (concat "rose-table-row-visually-hidden")}} {{if @style (concat "rose-table-row-" @style)}}'>
|
||||
{{yield (hash
|
||||
cell=(component 'rose/anonymous'
|
||||
tagName='td'
|
||||
class='rose-table-cell'
|
||||
)
|
||||
headerCell=(component 'rose/anonymous'
|
||||
tagName='th'
|
||||
class='rose-table-header-cell'
|
||||
)
|
||||
cell=(component 'rose/table/row/cell')
|
||||
headerCell=(component 'rose/table/row/cell' tagName='th')
|
||||
)}}
|
||||
</tr>
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-dropdown-button {
|
||||
@include type.type(s);
|
||||
@@ -11,7 +12,7 @@
|
||||
padding: 0 sizing.rems(s);
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
$line-height: 36 / 14;
|
||||
$line-height: math.div(36, 14);
|
||||
line-height: $line-height;
|
||||
|
||||
--background-color: transparent;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
$checkmark: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231563ff' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'></path></svg>");
|
||||
|
||||
.rose-dropdown-item {
|
||||
@include type.type(s);
|
||||
min-width: (sizing.rems(m)) * 20;
|
||||
$line-height: 36 / 14;
|
||||
$line-height: math.div(36, 14);
|
||||
line-height: $line-height;
|
||||
|
||||
--color: var(--stark);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-dropdown-link {
|
||||
@include type.type(s);
|
||||
@@ -11,7 +12,7 @@
|
||||
padding: 0 sizing.rems(s);
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
$line-height: 36 / 14;
|
||||
$line-height: math.div(36, 14);
|
||||
line-height: $line-height;
|
||||
|
||||
--background-color: transparent;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-dropdown-section {
|
||||
.rose-dropdown-section-title {
|
||||
@@ -7,7 +8,7 @@
|
||||
text-transform: uppercase;
|
||||
padding: 0 sizing.rems(s);
|
||||
margin: 0;
|
||||
$line-height: 36 / 14;
|
||||
$line-height: math.div(36, 14);
|
||||
line-height: $line-height;
|
||||
color: var(--ui-gray);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
.rose-form-checkbox {
|
||||
@include type.type(m);
|
||||
position: relative;
|
||||
margin-bottom: sizing.rems(xs);
|
||||
color: var(--ui-gray);
|
||||
margin-bottom: sizing.rems(m);
|
||||
|
||||
.rose-form-errors {
|
||||
margin-top: sizing.rems(xs);
|
||||
margin-bottom: sizing.rems(xs);
|
||||
}
|
||||
|
||||
.rose-form-helper-text {
|
||||
padding-left: sizing.rems(l);
|
||||
}
|
||||
|
||||
.rose-form-checkbox-label {
|
||||
// label styles
|
||||
@include type.type(s, semibold);
|
||||
@@ -23,6 +26,7 @@
|
||||
}
|
||||
|
||||
.rose-form-checkbox-label-description {
|
||||
margin-bottom: 0;
|
||||
margin-top: sizing.rems(xs);
|
||||
}
|
||||
|
||||
@@ -54,6 +58,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rose-form-helper-text {
|
||||
margin-top: sizing.rems(xs);
|
||||
}
|
||||
|
||||
.rose-form-checkbox-field {
|
||||
// mostly this just visually hides the native checkbox
|
||||
position: absolute;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
@use '../../utilities/type';
|
||||
|
||||
.rose-form-fieldset {
|
||||
margin-bottom: sizing.rems(m);
|
||||
|
||||
.rose-form-fieldset-title {
|
||||
@include type.type(l, bold);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-header-nav-link {
|
||||
@include type.type(s, semibold);
|
||||
|
||||
// target line height is 16, which is not a standard line height
|
||||
$lineHeight: 16 / 14;
|
||||
$lineHeight: math.div(16, 14);
|
||||
line-height: $lineHeight;
|
||||
text-decoration: none;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../variables/typography';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-list-key-value {
|
||||
margin-bottom: sizing.rems(m);
|
||||
@@ -10,9 +11,9 @@
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--ui-gray-subtler-3);
|
||||
@include type.type(s);
|
||||
$lineHeight: 36 / 14;
|
||||
$lineHeight: math.div(36, 14);
|
||||
line-height: $lineHeight;
|
||||
padding: (sizing.rems(s) / 2) 0;
|
||||
padding: math.div(sizing.rems(s), 2) 0;
|
||||
|
||||
.rose-list-key-value-item-cell {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-nav-sidebar {
|
||||
$threePX: sizing.rems(xxxs) + sizing.rems(xxxxs);
|
||||
@@ -19,7 +20,7 @@
|
||||
display: none;
|
||||
color: var(--ui-gray);
|
||||
text-transform: uppercase;
|
||||
$lineHeight: 20 / 12;
|
||||
$lineHeight: math.div(20, 12);
|
||||
line-height: $lineHeight;
|
||||
padding: 0 sizing.rems(m);
|
||||
margin-bottom: $threePX;
|
||||
@@ -30,7 +31,7 @@
|
||||
@include type.type(s);
|
||||
display: block;
|
||||
color: var(--stark);
|
||||
$lineHeight: 36 / 14;
|
||||
$lineHeight: math.div(36, 14);
|
||||
line-height: $lineHeight;
|
||||
text-decoration: none;
|
||||
padding: 0 sizing.rems(m);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use 'sass:math';
|
||||
|
||||
.rose-table {
|
||||
@include type.type(s, normal);
|
||||
@@ -10,9 +11,30 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
// This styles are add to stop the input to apply extra margin (default on forms)
|
||||
.rose-form-input,
|
||||
.rose-form-input-field,
|
||||
.rose-form-select,
|
||||
.rose-form-select-field {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Fix margin issue with textarea elements inside tables
|
||||
textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rose-table-cell,
|
||||
.rose-table-header-cell {
|
||||
vertical-align: middle;
|
||||
|
||||
&.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
&.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-header-cell {
|
||||
@@ -35,7 +57,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-cell-shrink {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[class*='rose-button'] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.rose-table-condensed {
|
||||
.rose-table-cell {
|
||||
@include type.type(s);
|
||||
$lineHeight: math.div(36, 14);
|
||||
line-height: $lineHeight;
|
||||
padding: math.div(sizing.rems(s), 2) sizing.rems(s);
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table-row-visually-hidden {
|
||||
position: absolute;
|
||||
top: -9999rem;
|
||||
left: -9999rem;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'sass:map';
|
||||
@use 'sizing';
|
||||
@use 'sass:math';
|
||||
|
||||
/**
|
||||
* SASS variables defining typography.
|
||||
@@ -46,15 +47,15 @@ $font-sizes: (
|
||||
|
||||
// Line height expressed as ratio.
|
||||
$line-heights: (
|
||||
xl: 36 / 28,
|
||||
l: 24 / 20,
|
||||
m: 20 / 16,
|
||||
s: 18 / 14,
|
||||
xs: 16 / 12,
|
||||
h1: 48 / 28,
|
||||
h2: 36 / 20,
|
||||
h3: 24 / 16,
|
||||
button: 24 / 13,
|
||||
xl: math.div(36, 28),
|
||||
l: math.div(24, 20),
|
||||
m: math.div(20, 16),
|
||||
s: math.div(18, 14),
|
||||
xs: math.div(16, 12),
|
||||
h1: math.div(48, 28),
|
||||
h2: math.div(36, 20),
|
||||
h3: math.div(24, 16),
|
||||
button: math.div(24, 13),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'rose/components/rose/layout/tabs';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'rose/components/rose/tab';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/table/row/cell';
|
||||
@@ -43,15 +43,15 @@
|
||||
"@ember/test-helpers": "^2.1.4",
|
||||
"@glimmer/component": "^1.0.3",
|
||||
"@glimmer/tracking": "^1.0.3",
|
||||
"@hashicorp/structure-icons": "^1.8.1",
|
||||
"@hashicorp/structure-icons": "^1.9.0-0",
|
||||
"@storybook/addon-actions": "^6.0.26",
|
||||
"@storybook/addon-docs": "^6.0.26",
|
||||
"@storybook/addon-knobs": "^6.0.26",
|
||||
"@storybook/ember": "^6.0.26",
|
||||
"@storybook/ember-cli-storybook": "^0.2.1",
|
||||
"@storybook/ember-cli-storybook": "^0.4.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"doctoc": "^2.0.0",
|
||||
"ember-auto-import": "^1.10.1",
|
||||
"ember-cli": "~3.24.0",
|
||||
"ember-cli-dependency-checker": "^3.2.0",
|
||||
|
||||
@@ -1,93 +1,196 @@
|
||||
<Rose::Header as |header|>
|
||||
<header.brand @logo="logo" @text="Product Name" />
|
||||
<h2>Condensed Table</h2>
|
||||
<Rose::Table @style="condensed" as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell >Address</row.headerCell>
|
||||
<row.headerCell>Host Set</row.headerCell>
|
||||
<row.headerCell>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-1, host-set-2</row.cell>
|
||||
<row.cell>optional name</row.cell>
|
||||
<row.cell>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer as |footer|>
|
||||
<footer.row as |row|>
|
||||
<row.cell>Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
|
||||
<header.nav as |nav|>
|
||||
<nav.dropdown @text="org-name" @icon="user-organization" as |dropdown|>
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
<dropdown.link @route="about" @style="danger">Menu item - danger</dropdown.link>
|
||||
<dropdown.separator />
|
||||
<dropdown.button>Button menu item</dropdown.button>
|
||||
</nav.dropdown>
|
||||
<h2>Condensed Table with Shrink Cells</h2>
|
||||
<Rose::Table @style="condensed" as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>Address</row.headerCell>
|
||||
<row.headerCell @shrink={{true}}>Host Set</row.headerCell>
|
||||
<row.headerCell @shrink={{true}}>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell @shrink={{true}}>host-set-1, host-set-2</row.cell>
|
||||
<row.cell @shrink={{true}}>
|
||||
<Rose::Button @style="secondary" @iconOnly="trash" title="Remove">Remove</Rose::Button>
|
||||
</row.cell>
|
||||
<row.cell>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer @hidden={{true}} as |footer|>
|
||||
<footer.row as |row|>
|
||||
<row.cell>Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
|
||||
<nav.link @route="index">Section</nav.link>
|
||||
<nav.link @route="about">Section</nav.link>
|
||||
</header.nav>
|
||||
<h2>Condensed Table with Right and Center aligned cells</h2>
|
||||
<Rose::Table @style="condensed" as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>Address</row.headerCell>
|
||||
<row.headerCell @alignCenter={{true}}>Host Set</row.headerCell>
|
||||
<row.headerCell>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell @alignCenter={{true}}>host-set-1, host-set-2</row.cell>
|
||||
<row.cell @alignRight={{true}}>optional name</row.cell>
|
||||
<row.cell @alignCenter={{true}}>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer as |footer|>
|
||||
<footer.row as |row|>
|
||||
<row.cell>Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
|
||||
<header.utilities as |utilities|>
|
||||
<utilities.dropdown
|
||||
@text="user@example.net"
|
||||
@iconOnly={{true}}
|
||||
@icon="user-square-fill" as |dropdown|
|
||||
>
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
<dropdown.link @route="about" @style="danger">Menu item - danger</dropdown.link>
|
||||
<dropdown.button>Button - default</dropdown.button>
|
||||
<dropdown.separator />
|
||||
<dropdown.button @style="danger">Button - danger</dropdown.button>
|
||||
</utilities.dropdown>
|
||||
</header.utilities>
|
||||
</Rose::Header>
|
||||
<h2>Condensed Table Key/Value Lookalike (edit mode)</h2>
|
||||
<Rose::Table @style="condensed" as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell @shrink={{true}}>Key</row.headerCell>
|
||||
<row.headerCell colspan="2">Value</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Input @contextual={{true}} as |input|>
|
||||
<input.field />
|
||||
</Rose::Form::Input>
|
||||
</row.cell>
|
||||
<row.cell @shrink={{true}} @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary" @iconOnly="trash" title="Remove">Remove</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Select
|
||||
@name="Test select"
|
||||
as |field|
|
||||
>
|
||||
<field.field as |select|>
|
||||
<select.option>Choose an option</select.option>
|
||||
<select.option @value="value-1">Value 1</select.option>
|
||||
<select.option @value="value-2">Value 2</select.option>
|
||||
<select.option @value="value-3">Value 3</select.option>
|
||||
</field.field>
|
||||
</Rose::Form::Select>
|
||||
</row.cell>
|
||||
<row.cell @shrink={{true}} @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary">Add</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Input @contextual={{true}} as |input|>
|
||||
<input.field />
|
||||
</Rose::Form::Input>
|
||||
</row.cell>
|
||||
<row.cell @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary">Add</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
<body.row as |row|>
|
||||
<row.cell>Key</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Form::Textarea
|
||||
@name="field"
|
||||
readonly={{false}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell @alignRight={{true}}>
|
||||
<Rose::Button @style="secondary">Add</Rose::Button>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer @hidden={{true}} as |footer|>
|
||||
<footer.row as |row|>
|
||||
<row.cell colspan="3">Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
|
||||
<Rose::Dropdown @text="Dropdown" as |dropdown|>
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
<dropdown.link @route="about" @style="danger">Menu item - danger</dropdown.link>
|
||||
<dropdown.separator></dropdown.separator>
|
||||
<h2>Condensed Table With Visually Hidden Header & Footer</h2>
|
||||
<Rose::Table @style="condensed" as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row @hidden={{true}} as |row|>
|
||||
<row.headerCell >Address</row.headerCell>
|
||||
<row.headerCell>Host Set</row.headerCell>
|
||||
<row.headerCell>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-1, host-set-2</row.cell>
|
||||
<row.cell>optional name</row.cell>
|
||||
<row.cell>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer as |footer|>
|
||||
<footer.row @hidden={{true}} as |row|>
|
||||
<row.cell>Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
|
||||
<dropdown.button>Button menu item</dropdown.button>
|
||||
<dropdown.button @style="danger">Button - danger</dropdown.button>
|
||||
<dropdown.separator />
|
||||
|
||||
<dropdown.section @title="Dropdown Section Title">
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
</dropdown.section>
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Dropdown @showCaret={{false}} @icon="more-horizontal" as |dropdown|>
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
<dropdown.button>Button menu item</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Form as |form|>
|
||||
<form.radioGroup tagName="ul" @name="color" @selectedValue="red" as |radioGroup|>
|
||||
<Rose::Dropdown @text="Color" as |dropdown|>
|
||||
<ul>
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label="Red"
|
||||
@value="red"
|
||||
@icon="user-organization" />
|
||||
</dropdown.item>
|
||||
</li>
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label="Green"
|
||||
@value="green"
|
||||
/>
|
||||
</dropdown.item>
|
||||
</li>
|
||||
<li>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label="Blue"
|
||||
@value="blue"
|
||||
/>
|
||||
</dropdown.item>
|
||||
</li>
|
||||
</ul>
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
|
||||
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Authentication Pending</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Complete authentication in the newly-opened window.</p>
|
||||
<p>Don't see the new window? <a href="#">Try again.</a></p>
|
||||
<p><a href="#">Cancel</a></p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
<h2>Table</h2>
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>Address</row.headerCell>
|
||||
<row.headerCell>Host Set</row.headerCell>
|
||||
<row.headerCell>Name</row.headerCell>
|
||||
<row.headerCell>Description</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
<body.row as |row|>
|
||||
<row.cell>1.1.1.1</row.cell>
|
||||
<row.cell>host-set-1, host-set-2</row.cell>
|
||||
<row.cell>optional name</row.cell>
|
||||
<row.cell>description</row.cell>
|
||||
</body.row>
|
||||
</table.body>
|
||||
<table.footer as |footer|>
|
||||
<footer.row as |row|>
|
||||
<row.cell>Footer</row.cell>
|
||||
</footer.row>
|
||||
</table.footer>
|
||||
</Rose::Table>
|
||||
@@ -9,9 +9,9 @@ module('Integration | Component | rose/form/checkbox', function (hooks) {
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(3);
|
||||
await render(hbs`<Rose::Form::Checkbox @label="Label" />`);
|
||||
assert.equal(await find('label').textContent.trim(), 'Label');
|
||||
assert.ok(await find('input'));
|
||||
assert.notOk(await find('.rose-form-checkbox-label-description'));
|
||||
assert.equal(find('label').textContent.trim(), 'Label');
|
||||
assert.ok(find('input'));
|
||||
assert.notOk(find('.rose-form-checkbox-label-description'));
|
||||
});
|
||||
|
||||
test('it renders optional description paragraph', async function (assert) {
|
||||
@@ -20,14 +20,25 @@ module('Integration | Component | rose/form/checkbox', function (hooks) {
|
||||
hbs`<Rose::Form::Checkbox @label="Label" @description="Hello world" />`
|
||||
);
|
||||
assert.equal(
|
||||
await find('.rose-form-checkbox-label-text').textContent.trim(),
|
||||
find('.rose-form-checkbox-label-text').textContent.trim(),
|
||||
'Label'
|
||||
);
|
||||
assert.equal(
|
||||
await find('.rose-form-checkbox-label-description').textContent.trim(),
|
||||
find('.rose-form-checkbox-label-description').textContent.trim(),
|
||||
'Hello world'
|
||||
);
|
||||
assert.ok(find('input'));
|
||||
});
|
||||
|
||||
test('it renders optional helper text', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(
|
||||
hbs`<Rose::Form::Checkbox @label="Label" @helperText="Hello world" />`
|
||||
);
|
||||
assert.equal(
|
||||
find('.rose-form-helper-text').textContent.trim(),
|
||||
'Hello world'
|
||||
);
|
||||
assert.ok(await find('input'));
|
||||
});
|
||||
|
||||
test('it displays optional errors', async function (assert) {
|
||||
@@ -49,26 +60,26 @@ module('Integration | Component | rose/form/checkbox', function (hooks) {
|
||||
|
||||
test('it is not checked by default', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Checkbox />`);
|
||||
assert.equal(await find('input').checked, false);
|
||||
assert.equal(find('input').checked, false);
|
||||
});
|
||||
|
||||
test('it is not disabled by default', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Checkbox />`);
|
||||
assert.equal(await find('input').disabled, false);
|
||||
assert.equal(find('input').disabled, false);
|
||||
});
|
||||
|
||||
test('it is checked when @checked={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Checkbox @checked={{true}} />`);
|
||||
assert.equal(await find('input').checked, true);
|
||||
assert.equal(find('input').checked, true);
|
||||
});
|
||||
|
||||
test('it marks error when @error={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Checkbox @error={{true}} />`);
|
||||
assert.ok(await find('.error'));
|
||||
assert.ok(find('.error'));
|
||||
});
|
||||
|
||||
test('it is disabled when @disabled={{true}}', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Checkbox @disabled={{true}} />`);
|
||||
assert.equal(await find('input').disabled, true);
|
||||
assert.equal(find('input').disabled, true);
|
||||
});
|
||||
});
|
||||
@@ -144,4 +144,28 @@ module('Integration | Component | rose/form/select', function (hooks) {
|
||||
`);
|
||||
await fillIn('[name="my-select"]', 'value-1');
|
||||
});
|
||||
|
||||
test('it supports a fully contextual usage', async function (assert) {
|
||||
assert.expect(5);
|
||||
await render(hbs`
|
||||
<Rose::Form::Select @value="value-1" @error={{true}} @contextual={{true}} as |field|>
|
||||
<field.label>Label</field.label>
|
||||
<field.helperText>Help</field.helperText>
|
||||
<field.field as |select|>
|
||||
<select.option @value="value-1">value-1</select.option>
|
||||
</field.field>
|
||||
<field.errors as |errors|>
|
||||
<errors.message>An error occurred.</errors.message>
|
||||
</field.errors>
|
||||
</Rose::Form::Select>
|
||||
`);
|
||||
assert.notOk(
|
||||
find('.rose-form-input'),
|
||||
'The form field wrapper element is not present in contextual mode'
|
||||
);
|
||||
assert.ok(find('.rose-form-label.error'));
|
||||
assert.ok(find('.rose-form-helper-text.error'));
|
||||
assert.ok(find('.rose-form-select-field.error'));
|
||||
assert.ok(find('.rose-form-error-message'));
|
||||
});
|
||||
});
|
||||
@@ -57,4 +57,26 @@ module('Integration | Component | rose/form/textarea', function (hooks) {
|
||||
await render(hbs`<Rose::Form::Textarea @error={{true}} />`);
|
||||
assert.ok(await find('.error'));
|
||||
});
|
||||
|
||||
test('it supports a fully contextual usage', async function (assert) {
|
||||
assert.expect(5);
|
||||
await render(hbs`
|
||||
<Rose::Form::Textarea @value="Text" @error={{true}} @contextual={{true}} as |field|>
|
||||
<field.label>Label</field.label>
|
||||
<field.helperText>Help</field.helperText>
|
||||
<field.field />
|
||||
<field.errors as |errors|>
|
||||
<errors.message>An error occurred.</errors.message>
|
||||
</field.errors>
|
||||
</Rose::Form::Textarea>
|
||||
`);
|
||||
assert.notOk(
|
||||
find('.rose-form-input'),
|
||||
'The form field wrapper element is not present in contextual mode'
|
||||
);
|
||||
assert.ok(find('.rose-form-label.error'));
|
||||
assert.ok(find('.rose-form-helper-text.error'));
|
||||
assert.ok(find('.rose-form-input-field.error'));
|
||||
assert.ok(find('.rose-form-error-message'));
|
||||
});
|
||||
});
|
||||
@@ -43,4 +43,9 @@ module('Integration | Component | rose/table', function (hooks) {
|
||||
</Rose::Table>`);
|
||||
assert.ok(find('tfoot'));
|
||||
});
|
||||
|
||||
test('it supports a style class', async function (assert) {
|
||||
await render(hbs`<Rose::Table @style="condensed" />`);
|
||||
assert.ok(find('.rose-table-condensed'));
|
||||
});
|
||||
});
|
||||
@@ -7,17 +7,20 @@ module('Integration | Component | rose/table/row', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Table::Row />`);
|
||||
assert.ok(find('tr'));
|
||||
assert.ok(find('.rose-table-row'));
|
||||
});
|
||||
|
||||
test('it renders with attributes', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Table::Row id="row"/>`);
|
||||
assert.ok(find('#row'));
|
||||
});
|
||||
|
||||
test('it renders a cell', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Table::Row as |row|>
|
||||
<row.cell />
|
||||
</Rose::Table::Row>`);
|
||||
@@ -26,10 +29,17 @@ module('Integration | Component | rose/table/row', function (hooks) {
|
||||
});
|
||||
|
||||
test('it renders a header cell', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Table::Row as |row|>
|
||||
<row.headerCell />
|
||||
</Rose::Table::Row>`);
|
||||
assert.ok(find('th'));
|
||||
assert.ok(find('.rose-table-header-cell'));
|
||||
});
|
||||
|
||||
test('it can be hidden', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Table::Row @hidden=true />`);
|
||||
assert.ok(find('.rose-table-row-visually-hidden'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | rose/table/row/cell', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Table::Row::Cell>
|
||||
content
|
||||
</Rose::Table::Row::Cell>`);
|
||||
assert.ok(find('td'));
|
||||
assert.equal(find('.rose-table-cell').textContent.trim(), 'content');
|
||||
});
|
||||
|
||||
test('it renders with attributes', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Table::Row::Cell id="cell"/>`);
|
||||
assert.ok(find('#cell'));
|
||||
});
|
||||
|
||||
test('it renders as header cell', async function (assert) {
|
||||
assert.expect(4);
|
||||
await render(hbs`<Rose::Table::Row::Cell @tagName='th'/>`);
|
||||
assert.ok(find('th'));
|
||||
assert.ok(find('.rose-table-header-cell'));
|
||||
assert.notOk(find('td'));
|
||||
assert.notOk(find('.rose-table-cell'));
|
||||
});
|
||||
|
||||
test('it adds shrink style class', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Table::Row::Cell @shrink={{true}} />`);
|
||||
assert.ok(find('.rose-table-cell-shrink'));
|
||||
});
|
||||
|
||||
test('it does not add expand style class in default cells', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Table::Row::Cell />`);
|
||||
assert.notOk(find('.rose-table-cell-shrink'));
|
||||
});
|
||||
|
||||
test('it renders with align center in a header cell', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(
|
||||
hbs`<Rose::Table::Row::Cell @tagName='th' @alignCenter=true />`
|
||||
);
|
||||
assert.ok(find('.rose-table-header-cell'));
|
||||
assert.ok(find('.align-center'));
|
||||
});
|
||||
|
||||
test('it renders with align center in a body', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Table::Row::Cell @alignCenter=true />`);
|
||||
assert.ok(find('.rose-table-cell'));
|
||||
assert.ok(find('.align-center'));
|
||||
});
|
||||
|
||||
test('it renders with align right in a header cell', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(
|
||||
hbs`<Rose::Table::Row::Cell @tagName='th' @alignRight=true />`
|
||||
);
|
||||
assert.ok(find('.rose-table-header-cell'));
|
||||
assert.ok(find('.align-right'));
|
||||
});
|
||||
|
||||
test('it renders with align right', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Table::Row::Cell @alignRight=true />`);
|
||||
assert.ok(find('.rose-table-cell'));
|
||||
assert.ok(find('.align-right'));
|
||||
});
|
||||
});
|
||||
Regular → Executable
+11
-2
@@ -6,24 +6,33 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
node: &node
|
||||
image: docker.mirror.hashicorp.services/node:12
|
||||
image: docker.mirror.hashicorp.services/node:14
|
||||
volumes:
|
||||
- './:/src'
|
||||
- 'icu_node_modules:/src/node_modules'
|
||||
- 'icu_addons_api_node_modules:/src/addons/api/node_modules'
|
||||
- 'icu_addons_auth_node_modules:/src/addons/auth/node_modules'
|
||||
- 'icu_addons_core_node_modules:/src/addons/core/node_modules'
|
||||
- 'icu_addons_rose_node_modules:/src/addons/rose/node_modules'
|
||||
- 'icu_ui_admin_node_modules:/src/ui/admin/node_modules'
|
||||
- 'icu_ui_desktop_node_modules:/src/ui/desktop/node_modules'
|
||||
working_dir: /src
|
||||
|
||||
build:
|
||||
build-admin:
|
||||
<<: *node
|
||||
entrypoint: bash
|
||||
command: '-c "yarn install && yarn build:ui:admin && chmod -R 777 ."'
|
||||
|
||||
build-desktop-production:
|
||||
<<: *node
|
||||
entrypoint: bash
|
||||
command: '-c "yarn install --frozen-lockfile --non-interactive && yarn build:ui:desktop:production && chmod -R 777 ."'
|
||||
|
||||
volumes:
|
||||
icu_node_modules:
|
||||
icu_addons_api_node_modules:
|
||||
icu_addons_auth_node_modules:
|
||||
icu_addons_core_node_modules:
|
||||
icu_addons_rose_node_modules:
|
||||
icu_ui_admin_node_modules:
|
||||
icu_ui_desktop_node_modules:
|
||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user