Merge remote-tracking branch 'origin/generate-forge-config-on-build' into ICU-1356-enable-windows-platform-support
This commit is contained in:
commit
3d6d7c99c2
65 files changed
+1054
-223
No files matched your search
+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,
|
||||
};
|
||||
@@ -1,9 +1,40 @@
|
||||
import GeneratedAuthMethodModel from '../generated/models/auth-method';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
/**
|
||||
* Enum ptions per auth method type and field.
|
||||
*/
|
||||
export const options = {
|
||||
oidc: {
|
||||
signing_algorithm: [
|
||||
'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 {
|
||||
// =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 +53,29 @@ export default class AuthMethodModel extends GeneratedAuthMethodModel {
|
||||
get isOIDC() {
|
||||
return this.type === 'oidc';
|
||||
}
|
||||
|
||||
// =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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,23 @@
|
||||
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') client_secret_hmac;
|
||||
@attr('number') max_age;
|
||||
@attr('string') api_url_prefix;
|
||||
@attr('string') 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;
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
initialize,
|
||||
} from 'api/initializers/fragment-auth-method-attributes-account-claim-map-serializer';
|
||||
@@ -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';
|
||||
@@ -11,4 +11,22 @@ 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',
|
||||
state: 'active-public',
|
||||
};
|
||||
break;
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
});
|
||||
+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);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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,75 @@ 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' }],
|
||||
},
|
||||
});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.deepEqual(serializedRecord, {
|
||||
type: 'oidc',
|
||||
name: 'OIDC Auth Method',
|
||||
description: null,
|
||||
attributes: {
|
||||
account_claim_maps: ['foo=bar'],
|
||||
api_url_prefix: null,
|
||||
callback_url: null,
|
||||
client_id: null,
|
||||
client_secret: null,
|
||||
client_secret_hmac: null,
|
||||
disable_discovered_config_validation: false,
|
||||
dry_run: false,
|
||||
issuer: null,
|
||||
max_age: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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 +126,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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
@@ -10,9 +10,20 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
// This styles are add to stop the input to apply extra margin (default on forms)
|
||||
.rose-form-input-field,
|
||||
.rose-form-select,
|
||||
.rose-form-input {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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 +46,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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: 36 / 14;
|
||||
line-height: $lineHeight;
|
||||
padding: (sizing.rems(s) / 2) sizing.rems(s);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.rose-table-row-visually-hidden {
|
||||
position: absolute;
|
||||
top: -9999rem;
|
||||
left: -9999rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/table/row/cell';
|
||||
@@ -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>
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | application', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:application');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/auth-methods', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/auth-methods');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/auth-method'
|
||||
|
||||
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/auth-method/accounts'
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/auth-method/accounts/account'
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/auth-method/accounts/new'
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/auth-methods/new', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/new'
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/authenticate', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/authenticate');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/groups', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/groups');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/groups/group', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/groups/group');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/groups/group/add-members'
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/groups/new', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/groups/new');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/roles', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/roles');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/roles/new', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/roles/new');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/roles/role', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/roles/role');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/roles/role/add-principals'
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/users', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/users');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/users/new', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/users/new');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Controller | scopes/scope/users/user', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/users/user');
|
||||
assert.ok(controller);
|
||||
|
||||
@@ -6,7 +6,6 @@ module(
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/users/user/add-accounts'
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
Canonical reference for changes, improvements, and bugfixes for Boundary Desktop.
|
||||
|
||||
## Next
|
||||
## v1.0.1 [CLI 0.2.2] (2021.05.19)
|
||||
|
||||
### New
|
||||
|
||||
- Add prompt to close sessions when quitting client. ([PR](https://github.com/hashicorp/boundary-ui/pull/555))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
||||
@@ -88,6 +88,9 @@ typings/
|
||||
# Electron-Forge
|
||||
out/
|
||||
|
||||
# Electron-Forge config
|
||||
config/forge.config.js
|
||||
|
||||
# Ember build
|
||||
ember-dist/
|
||||
ember-test/
|
||||
|
||||
@@ -4,7 +4,7 @@ const https = require('https');
|
||||
const decompress = require('decompress');
|
||||
const os = require('os');
|
||||
|
||||
const artifactVersion = '0.2.0';
|
||||
const artifactVersion = '0.2.2';
|
||||
const artifactDestination = path.resolve(__dirname, '..', 'cli');
|
||||
|
||||
const platform = {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
const process = require('process');
|
||||
const { version } = require('../src/cli/index.js');
|
||||
|
||||
const formattedCLIVersion = version().formatted;
|
||||
|
||||
const appVersion = process.env.RELEASE_VERSION
|
||||
? `Version: ${process.env.RELEASE_VERSION}\n`
|
||||
: '';
|
||||
const appCommit = process.env.RELEASE_COMMIT
|
||||
? `Commit: ${process.env.RELEASE_COMMIT}\n`
|
||||
: '';
|
||||
|
||||
const formattedAppVersion = `${appVersion}${appCommit}`;
|
||||
|
||||
// Output version strings for debugging
|
||||
console.log('\n\nVersion Env Vars:');
|
||||
console.log('RELEASE_VERSION ', process.env.RELEASE_VERSION);
|
||||
console.log('RELEASE_COMMIT ', process.env.RELEASE_COMMIT);
|
||||
|
||||
module.exports = {
|
||||
releaseVersion: process.env.RELEASE_VERSION,
|
||||
hooks: {
|
||||
prePackage: () => {
|
||||
if (!process.env.BOUNDARY_DESKTOP_SIGNING_IDENTITY) {
|
||||
console.warn('\nWARNING: Could not find signing identity.');
|
||||
}
|
||||
},
|
||||
},
|
||||
packagerConfig: {
|
||||
ignore: ['/ember-test(/|$)', '/tests(/|$)'],
|
||||
name: 'Boundary',
|
||||
appBundleId: 'com.electron.boundary',
|
||||
// TODO: where should the client version number come from?
|
||||
// appVersion: `${formattedAppVersion}${formattedCLIVersion}\n`,
|
||||
// FIXME: Disable version temporarily to build windows archives
|
||||
appVersion: '1.0.0',
|
||||
appCopyright: 'Copyright © 2021 HashiCorp, Inc.',
|
||||
icon: './config/macos/icon.icns',
|
||||
osxSign: {
|
||||
identity: process.env.BOUNDARY_DESKTOP_SIGNING_IDENTITY,
|
||||
'hardened-runtime': true,
|
||||
entitlements: './config/macos/entitlements.plist',
|
||||
'entitlements-inherit': './config/macos/entitlements.plist',
|
||||
'signature-flags': 'library',
|
||||
},
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
title: 'Boundary',
|
||||
name: 'boundary',
|
||||
icon: './config/macos/disk.icns',
|
||||
background: './config/macos/background.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel'
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
packagerConfig: {
|
||||
ignore: ['/ember-test(/|$)', '/tests(/|$)'],
|
||||
name: 'Boundary',
|
||||
version: '0.0.0',
|
||||
appBundleId: 'com.electron.boundary',
|
||||
appCopyright: 'Copyright © 2021 HashiCorp, Inc.',
|
||||
},
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-zip',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Create forge config based on platform
|
||||
const createConfig = () => {
|
||||
const config = { ...DEFAULT_CONFIG };
|
||||
|
||||
// Store env vars in config
|
||||
['RELEASE_COMMIT', 'RELEASE_VERSION'].forEach((envVar) => {
|
||||
if (process.env[envVar]) {
|
||||
config[envVar] = process.env[envVar];
|
||||
console.log(`[forge-config] ${envVar} ${config[envVar]}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (os.platform().match(/(darwin)/i)) {
|
||||
config.packagerConfig.icon = './config/macos/icon.icns';
|
||||
config.packagerConfig.osxSign = {
|
||||
'hardened-runtime': true,
|
||||
entitlements: './config/macos/entitlements.plist',
|
||||
'entitlements-inherit': './config/macos/entitlements.plist',
|
||||
'signature-flags': 'library',
|
||||
};
|
||||
|
||||
// Version
|
||||
if (config.RELEASE_VERSION)
|
||||
config.packagerConfig.version = config.RELEASE_VERSION;
|
||||
|
||||
// Signing identity
|
||||
const signingIdentity = process.env.BOUNDARY_DESKTOP_SIGNING_IDENTITY;
|
||||
if (signingIdentity) {
|
||||
config.packagerConfig.osxSign.identity = signingIdentity;
|
||||
} else {
|
||||
console.warn(
|
||||
'[forge-config] WARNING: Could not find signing identity. Proceeding without signing.'
|
||||
);
|
||||
}
|
||||
|
||||
// Generate DMG file
|
||||
config.makers.push({
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
title: 'Boundary',
|
||||
name: 'boundary',
|
||||
icon: './config/macos/disk.icns',
|
||||
background: './config/macos/background.png',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// Save forge config file
|
||||
const saveConfig = (config, destination) => {
|
||||
const configPath = `${destination}/forge.config.js`;
|
||||
console.log(`[forge-config] ${configPath}`);
|
||||
const content = `module.exports = ${JSON.stringify(config, null, 2)}`;
|
||||
fs.writeFileSync(configPath, content);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setup: () => {
|
||||
const config = createConfig();
|
||||
const destination = path.resolve(__dirname);
|
||||
saveConfig(config, destination);
|
||||
},
|
||||
};
|
||||
@@ -9,9 +9,10 @@
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"build:cli": "node -e \"require('./config/cli.js').setup()\"",
|
||||
"build:config": "node -e 'require(\"./config/forge.js\").setup()'",
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"premake": "yarn build:cli",
|
||||
"premake": "yarn build:cli && yarn build:config",
|
||||
"make": "electron-forge make",
|
||||
"make:windows": "electron-forge make --targets @electron-forge/maker-squirrel",
|
||||
"publish": "electron-forge publish",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const { shell } = require('electron');
|
||||
const { app, shell, dialog } = require('electron');
|
||||
const appUpdater = require('../helpers/app-updater.js');
|
||||
const { isMac, isWindows } = require('../helpers/platform.js');
|
||||
const forgeConfig = require('../../config/forge.config.js');
|
||||
const { version } = require('../cli/index.js');
|
||||
|
||||
const generateMenuTemplate = () => {
|
||||
return [
|
||||
@@ -8,7 +10,23 @@ const generateMenuTemplate = () => {
|
||||
...(isMac() ? [{
|
||||
label: 'Boundary',
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
// { role: 'about' },
|
||||
{
|
||||
label: `About ${app.getName()}`,
|
||||
click: () => {
|
||||
const appVersion = `Version: ${forgeConfig.RELEASE_VERSION}`;
|
||||
const appCommit = `Commit: ${forgeConfig.RELEASE_COMMIT}`;
|
||||
const cliVersion = version().formatted;
|
||||
const copyright = forgeConfig.packagerConfig.appCopyright;
|
||||
|
||||
const dialogOpts = {
|
||||
type: 'none',
|
||||
message: app.getName(),
|
||||
detail: `${appVersion}\n${appCommit}\n\n${cliVersion}\n\n${copyright}`,
|
||||
};
|
||||
dialog.showMessageBox(dialogOpts);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'update',
|
||||
label: 'Check for Updates',
|
||||
|
||||
@@ -7,9 +7,9 @@ const {
|
||||
const {
|
||||
session,
|
||||
app,
|
||||
dialog,
|
||||
protocol,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} = require('electron');
|
||||
@@ -17,6 +17,7 @@ require('./ipc/handlers.js');
|
||||
|
||||
const runtimeSettings = require('./services/runtime-settings.js');
|
||||
const { generateCSPHeader } = require('./config/content-security-policy.js');
|
||||
const sessionManager = require('./services/session-manager.js');
|
||||
|
||||
const menu = require('./config/menu.js');
|
||||
const appUpdater = require('./helpers/app-updater.js');
|
||||
@@ -183,6 +184,30 @@ app.on('ready', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Prompt for closing spawned processes
|
||||
*/
|
||||
app.on('before-quit', (event) => {
|
||||
if (sessionManager.hasRunningSessions) {
|
||||
const dialogOpts = {
|
||||
type: 'question',
|
||||
buttons: ['Close', 'Cancel'],
|
||||
detail: 'Close sessions before quitting?',
|
||||
};
|
||||
|
||||
dialog.showMessageBox(dialogOpts).then((returnValue) => {
|
||||
switch (returnValue.response) {
|
||||
case 0:
|
||||
sessionManager.stopAll();
|
||||
break;
|
||||
default:
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle an unhandled error in the main thread
|
||||
//
|
||||
// Note that 'uncaughtException' is a crude mechanism for exception handling intended to
|
||||
|
||||
@@ -3,6 +3,14 @@ const Session = require('./session.js');
|
||||
class SessionManager {
|
||||
#sessions = [];
|
||||
|
||||
/**
|
||||
* Checks for running sessions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasRunningSessions() {
|
||||
return Boolean(this.#sessions.find((session) => session.isRunning));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a session and track it.
|
||||
* Returns session proxy details if successfully started.
|
||||
@@ -23,10 +31,11 @@ class SessionManager {
|
||||
*/
|
||||
stopById(session_id) {
|
||||
const session = this.#sessions.find((session) => session.id === session_id);
|
||||
return session.stop();
|
||||
return session?.stop?.();
|
||||
}
|
||||
|
||||
stopAll() {
|
||||
this.#sessions.forEach((session) => session.stop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Session {
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isActive() {
|
||||
get isRunning() {
|
||||
return !this.#process.killed;
|
||||
}
|
||||
|
||||
@@ -60,12 +60,12 @@ class Session {
|
||||
*/
|
||||
stop() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isActive) {
|
||||
if (this.isRunning) {
|
||||
this.#process.on('close', () => resolve());
|
||||
this.#process.on('error', (e) => reject(e));
|
||||
this.#process.kill();
|
||||
} else {
|
||||
// Do nothing when process isn't active
|
||||
// Do nothing when process isn't running
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"format:sass": "prettier --write app/**/*.scss",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"start": "ember serve",
|
||||
"prestart:desktop": "cd electron-app && yarn build:cli",
|
||||
"prestart:desktop": "cd electron-app && yarn premake",
|
||||
"start:desktop": "ember electron",
|
||||
"clean:coverage": "rm -rf coverage_*",
|
||||
"pretest": "yarn clean:coverage",
|
||||
|
||||
Reference in new issue
Block a user