Merge remote-tracking branch 'origin/generate-forge-config-on-build' into ICU-1356-enable-windows-platform-support

This commit is contained in:
Susmitha Girumala committed 2021-05-22 15:59:21 -04:00
commit 3d6d7c99c2
65 files changed
+1054 -223

No files matched your search

@@ -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,
};
+56
View File
@@ -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;
}
+70 -3
View File
@@ -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);
}
}
@@ -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;
},
});
@@ -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', {});
@@ -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';
+191 -88
View File
@@ -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'
@@ -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'
@@ -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'
+5 -1
View File
@@ -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
+3
View File
@@ -88,6 +88,9 @@ typings/
# Electron-Forge
out/
# Electron-Forge config
config/forge.config.js
# Ember build
ember-dist/
ember-test/
+1 -1
View File
@@ -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',
},
],
};
+84
View File
@@ -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);
},
};
+2 -1
View File
@@ -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",
+20 -2
View File
@@ -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',
+26 -1
View File
@@ -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();
}
});
+1 -1
View File
@@ -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",