diff --git a/addons/api/addon/initializers/fragment-auth-method-attributes-account-claim-map-serializer.js b/addons/api/addon/initializers/fragment-auth-method-attributes-account-claim-map-serializer.js
new file mode 100644
index 000000000..59387536f
--- /dev/null
+++ b/addons/api/addon/initializers/fragment-auth-method-attributes-account-claim-map-serializer.js
@@ -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,
+};
diff --git a/addons/api/addon/models/auth-method.js b/addons/api/addon/models/auth-method.js
index 7686c2783..699f5137e 100644
--- a/addons/api/addon/models/auth-method.js
+++ b/addons/api/addon/models/auth-method.js
@@ -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,
+ },
+ });
+ }
}
diff --git a/addons/api/addon/models/fragment-auth-method-attributes-account-claim-map.js b/addons/api/addon/models/fragment-auth-method-attributes-account-claim-map.js
new file mode 100644
index 000000000..c3e2aec21
--- /dev/null
+++ b/addons/api/addon/models/fragment-auth-method-attributes-account-claim-map.js
@@ -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;
+}
diff --git a/addons/api/addon/models/fragment-auth-method-attributes.js b/addons/api/addon/models/fragment-auth-method-attributes.js
new file mode 100644
index 000000000..b023401f2
--- /dev/null
+++ b/addons/api/addon/models/fragment-auth-method-attributes.js
@@ -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;
+}
diff --git a/addons/api/addon/serializers/auth-method.js b/addons/api/addon/serializers/auth-method.js
index a8569c14d..6895cf512 100644
--- a/addons/api/addon/serializers/auth-method.js
+++ b/addons/api/addon/serializers/auth-method.js
@@ -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;
}
diff --git a/addons/api/addon/serializers/fragment-auth-method-attributes-account-claim-map.js b/addons/api/addon/serializers/fragment-auth-method-attributes-account-claim-map.js
new file mode 100644
index 000000000..5c5b4789e
--- /dev/null
+++ b/addons/api/addon/serializers/fragment-auth-method-attributes-account-claim-map.js
@@ -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);
+ }
+}
diff --git a/addons/api/app/initializers/fragment-auth-method-attributes-account-claim-map-serializer.js b/addons/api/app/initializers/fragment-auth-method-attributes-account-claim-map-serializer.js
new file mode 100644
index 000000000..844980fbc
--- /dev/null
+++ b/addons/api/app/initializers/fragment-auth-method-attributes-account-claim-map-serializer.js
@@ -0,0 +1,4 @@
+export {
+ default,
+ initialize,
+} from 'api/initializers/fragment-auth-method-attributes-account-claim-map-serializer';
diff --git a/addons/api/app/models/fragment-auth-method-attributes-account-claim-map.js b/addons/api/app/models/fragment-auth-method-attributes-account-claim-map.js
new file mode 100644
index 000000000..8d0f7b0c7
--- /dev/null
+++ b/addons/api/app/models/fragment-auth-method-attributes-account-claim-map.js
@@ -0,0 +1 @@
+export { default } from 'api/models/fragment-auth-method-attributes-account-claim-map';
diff --git a/addons/api/app/models/fragment-auth-method-attributes.js b/addons/api/app/models/fragment-auth-method-attributes.js
new file mode 100644
index 000000000..46bd1e0f2
--- /dev/null
+++ b/addons/api/app/models/fragment-auth-method-attributes.js
@@ -0,0 +1 @@
+export { default } from 'api/models/fragment-auth-method-attributes';
diff --git a/addons/api/mirage/generated/factories/auth-method.js b/addons/api/mirage/generated/factories/auth-method.js
index 6ea181f58..8fd338a53 100644
--- a/addons/api/mirage/generated/factories/auth-method.js
+++ b/addons/api/mirage/generated/factories/auth-method.js
@@ -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;
+ },
});
diff --git a/addons/api/tests/unit/initializers/fragment-auth-method-attributes-account-claim-map-serializer-test.js b/addons/api/tests/unit/initializers/fragment-auth-method-attributes-account-claim-map-serializer-test.js
new file mode 100644
index 000000000..caac46ee2
--- /dev/null
+++ b/addons/api/tests/unit/initializers/fragment-auth-method-attributes-account-claim-map-serializer-test.js
@@ -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);
+ });
+ }
+);
diff --git a/addons/api/tests/unit/models/auth-method-test.js b/addons/api/tests/unit/models/auth-method-test.js
index 31ff3fca1..5e96526e3 100644
--- a/addons/api/tests/unit/models/auth-method-test.js
+++ b/addons/api/tests/unit/models/auth-method-test.js
@@ -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');
+ });
});
diff --git a/addons/api/tests/unit/models/auth-token-test.js b/addons/api/tests/unit/models/auth-token-test.js
index bf9809ee0..0900707df 100644
--- a/addons/api/tests/unit/models/auth-token-test.js
+++ b/addons/api/tests/unit/models/auth-token-test.js
@@ -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', {});
diff --git a/addons/api/tests/unit/models/fragment-auth-method-attributes-account-claim-map-test.js b/addons/api/tests/unit/models/fragment-auth-method-attributes-account-claim-map-test.js
new file mode 100644
index 000000000..7f87ebe7e
--- /dev/null
+++ b/addons/api/tests/unit/models/fragment-auth-method-attributes-account-claim-map-test.js
@@ -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);
+ });
+ }
+);
diff --git a/addons/api/tests/unit/models/fragment-auth-method-attributes-test.js b/addons/api/tests/unit/models/fragment-auth-method-attributes-test.js
new file mode 100644
index 000000000..433c2045d
--- /dev/null
+++ b/addons/api/tests/unit/models/fragment-auth-method-attributes-test.js
@@ -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);
+ });
+});
diff --git a/addons/api/tests/unit/models/fragment-host-set-test.js b/addons/api/tests/unit/models/fragment-host-set-test.js
index 9d9248f8b..3031c94f5 100644
--- a/addons/api/tests/unit/models/fragment-host-set-test.js
+++ b/addons/api/tests/unit/models/fragment-host-set-test.js
@@ -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', {});
diff --git a/addons/api/tests/unit/models/fragment-principal-test.js b/addons/api/tests/unit/models/fragment-principal-test.js
index f6c243ede..c55fd56de 100644
--- a/addons/api/tests/unit/models/fragment-principal-test.js
+++ b/addons/api/tests/unit/models/fragment-principal-test.js
@@ -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', {});
diff --git a/addons/api/tests/unit/models/fragment-string-test.js b/addons/api/tests/unit/models/fragment-string-test.js
index 72f2c3eee..7d3c7dcb9 100644
--- a/addons/api/tests/unit/models/fragment-string-test.js
+++ b/addons/api/tests/unit/models/fragment-string-test.js
@@ -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', {});
diff --git a/addons/api/tests/unit/models/host-test.js b/addons/api/tests/unit/models/host-test.js
index 73e66b086..871551523 100644
--- a/addons/api/tests/unit/models/host-test.js
+++ b/addons/api/tests/unit/models/host-test.js
@@ -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', {});
diff --git a/addons/api/tests/unit/serializers/auth-method-test.js b/addons/api/tests/unit/serializers/auth-method-test.js
index cd7fcd396..95706c401 100644
--- a/addons/api/tests/unit/serializers/auth-method-test.js
+++ b/addons/api/tests/unit/serializers/auth-method-test.js
@@ -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');
+ });
});
diff --git a/addons/api/tests/unit/serializers/host-test.js b/addons/api/tests/unit/serializers/host-test.js
index 86c17fdd0..4c531ca82 100644
--- a/addons/api/tests/unit/serializers/host-test.js
+++ b/addons/api/tests/unit/serializers/host-test.js
@@ -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');
diff --git a/addons/api/tests/unit/serializers/session-test.js b/addons/api/tests/unit/serializers/session-test.js
index a5ae53586..8c674b1c9 100644
--- a/addons/api/tests/unit/serializers/session-test.js
+++ b/addons/api/tests/unit/serializers/session-test.js
@@ -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');
diff --git a/addons/rose/addon/components/rose/table/index.hbs b/addons/rose/addon/components/rose/table/index.hbs
index 655e7dbd4..643a14cfa 100644
--- a/addons/rose/addon/components/rose/table/index.hbs
+++ b/addons/rose/addon/components/rose/table/index.hbs
@@ -1,4 +1,4 @@
-
+
{{#if @caption}}
{{@caption}}
{{/if}}
diff --git a/addons/rose/addon/components/rose/table/index.stories.mdx b/addons/rose/addon/components/rose/table/index.stories.mdx
index a720bd877..444419be4 100644
--- a/addons/rose/addon/components/rose/table/index.stories.mdx
+++ b/addons/rose/addon/components/rose/table/index.stories.mdx
@@ -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';
# 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
-
+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
+
-
+
Address
Host Set
Name
- Description
+ Description
1.1.1.1
host-set-1, host-set-2
- optional name
+ optional name
+ description
+
+
+ 1.1.1.1
+ host-set-2
+ optional name
+ description
+
+
+ 1.1.1.1
+ host-set-1
+ optional name
description
@@ -36,22 +51,34 @@ A table component to display data in structure of rows and columns. It supports
{{
template: hbs`
-
+
-
- Address
- Host Set
- Name
- Description
+
+ Address
+ Host Set
+ Name
+ Description
- 1.1.1.1
+ 1.1.1.1
host-set-1, host-set-2
- optional name
+ optional name
description
+
+ 1.1.1.1
+ host-set-2
+ optional name
+ description
+
+
+ 1.1.1.1
+ host-set-1
+ optional name
+ description
+
@@ -60,6 +87,102 @@ A table component to display data in structure of rows and columns. It supports
`,
- context: {}
+ context: {
+ hideHeaderRow: boolean('hideHeaderRow', false),
+ cellShrink: boolean('cellShrink', false),
+ style: select(
+ 'style',
+ {
+ 'default': '',
+ 'condensed': 'condensed'
+ }
+ ),
+ cellAlignmentCenter: boolean('cellAlignmentCenter', false),
+ cellAlignmentRight: boolean('cellAlignmentRight', false),
+ },
+ }}
+
+
+
+ {{
+ template: hbs`
+
+
+
+ Key
+ Value
+
+
+
+
+ Key
+
+
+
+
+
+
+ Remove
+
+
+
+ Key
+
+
+
+ Choose an option
+ Value 1
+ Value 2
+ Value 3
+
+
+
+
+ Add
+
+
+
+ Key
+
+
+
+
+
+
+ Add
+
+
+
+ Key
+
+
+
+
+ Add
+
+
+
+
+
+ Footer
+
+
+
+ `,
+ context: {
+ style: select(
+ 'style',
+ {
+ 'condensed': 'condensed',
+ 'default': ''
+ }
+ )
+ }
}}
diff --git a/addons/rose/addon/components/rose/table/row/cell/index.hbs b/addons/rose/addon/components/rose/table/row/cell/index.hbs
new file mode 100644
index 000000000..fb5c4b157
--- /dev/null
+++ b/addons/rose/addon/components/rose/table/row/cell/index.hbs
@@ -0,0 +1 @@
+{{yield}}
\ No newline at end of file
diff --git a/addons/rose/addon/components/rose/table/row/cell/index.js b/addons/rose/addon/components/rose/table/row/cell/index.js
new file mode 100644
index 000000000..588c072cf
--- /dev/null
+++ b/addons/rose/addon/components/rose/table/row/cell/index.js
@@ -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';
+ }
+ })
+});
diff --git a/addons/rose/addon/components/rose/table/row/index.hbs b/addons/rose/addon/components/rose/table/row/index.hbs
index e5bad550e..65fb90e81 100644
--- a/addons/rose/addon/components/rose/table/row/index.hbs
+++ b/addons/rose/addon/components/rose/table/row/index.hbs
@@ -1,12 +1,6 @@
-
+
{{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')
)}}
diff --git a/addons/rose/addon/styles/rose/components/table/_table.scss b/addons/rose/addon/styles/rose/components/table/_table.scss
index 0c581393e..f9b7b9daf 100644
--- a/addons/rose/addon/styles/rose/components/table/_table.scss
+++ b/addons/rose/addon/styles/rose/components/table/_table.scss
@@ -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;
+ }
}
diff --git a/addons/rose/app/components/rose/table/row/cell.js b/addons/rose/app/components/rose/table/row/cell.js
new file mode 100644
index 000000000..50e7502b4
--- /dev/null
+++ b/addons/rose/app/components/rose/table/row/cell.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/table/row/cell';
diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs
index 8bb8b116d..e82d02286 100644
--- a/addons/rose/tests/dummy/app/templates/index.hbs
+++ b/addons/rose/tests/dummy/app/templates/index.hbs
@@ -1,93 +1,196 @@
-
-
+Condensed Table
+
+
+
+ Address
+ Host Set
+ Name
+ Description
+
+
+
+
+ 1.1.1.1
+ host-set-1, host-set-2
+ optional name
+ description
+
+
+
+
+ Footer
+
+
+
-
-
- Menu item
- Menu item - danger
-
- Button menu item
-
+Condensed Table with Shrink Cells
+
+
+
+ Address
+ Host Set
+ Name
+ Description
+
+
+
+
+ 1.1.1.1
+ host-set-1, host-set-2
+
+ Remove
+
+ description
+
+
+
+
+ Footer
+
+
+
- Section
- Section
-
+Condensed Table with Right and Center aligned cells
+
+
+
+ Address
+ Host Set
+ Name
+ Description
+
+
+
+
+ 1.1.1.1
+ host-set-1, host-set-2
+ optional name
+ description
+
+
+
+
+ Footer
+
+
+
-
-
- Menu item
- Menu item - danger
- Button - default
-
- Button - danger
-
-
-
+Condensed Table Key/Value Lookalike (edit mode)
+
+
+
+ Key
+ Value
+
+
+
+
+ Key
+
+
+
+
+
+
+ Remove
+
+
+
+ Key
+
+
+
+ Choose an option
+ Value 1
+ Value 2
+ Value 3
+
+
+
+
+ Add
+
+
+
+ Key
+
+
+
+
+
+
+ Add
+
+
+
+ Key
+
+
+
+
+ Add
+
+
+
+
+
+ Footer
+
+
+
-
- Menu item
- Menu item - danger
-
+Condensed Table With Visually Hidden Header & Footer
+
+
+
+ Address
+ Host Set
+ Name
+ Description
+
+
+
+
+ 1.1.1.1
+ host-set-1, host-set-2
+ optional name
+ description
+
+
+
+
+ Footer
+
+
+
- Button menu item
- Button - danger
-
-
-
- Menu item
-
-
-
-
- Menu item
- Button menu item
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- <:header>
- Authentication Pending
-
- <:body>
- Complete authentication in the newly-opened window.
- Don't see the new window? Try again.
- Cancel
-
-
+Table
+
+
+
+ Address
+ Host Set
+ Name
+ Description
+
+
+
+
+ 1.1.1.1
+ host-set-1, host-set-2
+ optional name
+ description
+
+
+
+
+ Footer
+
+
+
\ No newline at end of file
diff --git a/addons/rose/tests/integration/components/rose/table-test.js b/addons/rose/tests/integration/components/rose/table-test.js
index 06f2a5b8c..aa78b58eb 100644
--- a/addons/rose/tests/integration/components/rose/table-test.js
+++ b/addons/rose/tests/integration/components/rose/table-test.js
@@ -43,4 +43,9 @@ module('Integration | Component | rose/table', function (hooks) {
`);
assert.ok(find('tfoot'));
});
+
+ test('it supports a style class', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.rose-table-condensed'));
+ });
});
diff --git a/addons/rose/tests/integration/components/rose/table/row-test.js b/addons/rose/tests/integration/components/rose/table/row-test.js
index 94c57cd08..2a0c1f758 100644
--- a/addons/rose/tests/integration/components/rose/table/row-test.js
+++ b/addons/rose/tests/integration/components/rose/table/row-test.js
@@ -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``);
assert.ok(find('tr'));
assert.ok(find('.rose-table-row'));
});
test('it renders with attributes', async function (assert) {
+ assert.expect(1);
await render(hbs``);
assert.ok(find('#row'));
});
test('it renders a cell', async function (assert) {
+ assert.expect(2);
await render(hbs`
`);
@@ -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`
`);
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``);
+ assert.ok(find('.rose-table-row-visually-hidden'));
+ });
});
diff --git a/addons/rose/tests/integration/components/rose/table/row/cell-test.js b/addons/rose/tests/integration/components/rose/table/row/cell-test.js
new file mode 100644
index 000000000..4b8c3332d
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/table/row/cell-test.js
@@ -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`
+ content
+ `);
+ 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``);
+ assert.ok(find('#cell'));
+ });
+
+ test('it renders as header cell', async function (assert) {
+ assert.expect(4);
+ await render(hbs``);
+ 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``);
+ 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``);
+ 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``
+ );
+ 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``);
+ 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``
+ );
+ 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``);
+ assert.ok(find('.rose-table-cell'));
+ assert.ok(find('.align-right'));
+ });
+});
diff --git a/ui/admin/tests/unit/controllers/application-test.js b/ui/admin/tests/unit/controllers/application-test.js
index eb120e471..ba85a8331 100644
--- a/ui/admin/tests/unit/controllers/application-test.js
+++ b/ui/admin/tests/unit/controllers/application-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope-test.js b/ui/admin/tests/unit/controllers/scopes/scope-test.js
index 1ac27e62b..491aad466 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods-test.js
index af2caa0e3..8b1297655 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method-test.js
index f1396eba0..7523b46d0 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts-test.js
index 7475cb779..f0706cc2e 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/account-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/account-test.js
index d906d24de..dcd65f4bb 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/account-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/account-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/new-test.js
index 1c8baec09..990775639 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/new-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/accounts/new-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/new-test.js
index a3d44a02a..048e103b5 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/new-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/new-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/authenticate-test.js b/ui/admin/tests/unit/controllers/scopes/scope/authenticate-test.js
index cd9b09503..04ab78eb5 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/authenticate-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/authenticate-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/groups-test.js b/ui/admin/tests/unit/controllers/scopes/scope/groups-test.js
index 4f1a10f19..4cfb2bb56 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/groups-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/groups-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/groups/group-test.js b/ui/admin/tests/unit/controllers/scopes/scope/groups/group-test.js
index bb3e8b7cc..ddae53937 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/groups/group-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/groups/group-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js b/ui/admin/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js
index 263e690aa..b1dd91e25 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/groups/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/groups/new-test.js
index 56eaa6c76..ef0ff13af 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/groups/new-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/groups/new-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles-test.js
index 1567c0d37..4bd0614e6 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/roles-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/roles-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/new-test.js
index ee2595d36..f42c4638e 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/roles/new-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/new-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role-test.js
index 4edaa508f..32e0058c3 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/roles/role-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js
index 7eb851443..587884b96 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js
@@ -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'
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/users-test.js b/ui/admin/tests/unit/controllers/scopes/scope/users-test.js
index 25ced1669..a6ea8b7cb 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/users-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/users-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/users/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/users/new-test.js
index 440b4523c..de3dce931 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/users/new-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/users/new-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/users/user-test.js b/ui/admin/tests/unit/controllers/scopes/scope/users/user-test.js
index 17d292de0..6d985406e 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/users/user-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/users/user-test.js
@@ -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);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/users/user/add-accounts-test.js b/ui/admin/tests/unit/controllers/scopes/scope/users/user/add-accounts-test.js
index 7971e808f..5cbf98f77 100644
--- a/ui/admin/tests/unit/controllers/scopes/scope/users/user/add-accounts-test.js
+++ b/ui/admin/tests/unit/controllers/scopes/scope/users/user/add-accounts-test.js
@@ -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'
diff --git a/ui/desktop/CHANGELOG.md b/ui/desktop/CHANGELOG.md
index 614ad5f8a..42bd5a07d 100644
--- a/ui/desktop/CHANGELOG.md
+++ b/ui/desktop/CHANGELOG.md
@@ -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
diff --git a/ui/desktop/electron-app/.gitignore b/ui/desktop/electron-app/.gitignore
index 75a95bf75..bb7b930fa 100644
--- a/ui/desktop/electron-app/.gitignore
+++ b/ui/desktop/electron-app/.gitignore
@@ -88,6 +88,9 @@ typings/
# Electron-Forge
out/
+# Electron-Forge config
+config/forge.config.js
+
# Ember build
ember-dist/
ember-test/
diff --git a/ui/desktop/electron-app/config/cli.js b/ui/desktop/electron-app/config/cli.js
index 3b106ce7b..bf3f62fdb 100644
--- a/ui/desktop/electron-app/config/cli.js
+++ b/ui/desktop/electron-app/config/cli.js
@@ -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 = {
diff --git a/ui/desktop/electron-app/config/forge.config.js b/ui/desktop/electron-app/config/forge.config.js
deleted file mode 100644
index d41d14a4e..000000000
--- a/ui/desktop/electron-app/config/forge.config.js
+++ /dev/null
@@ -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',
- },
- ],
-};
diff --git a/ui/desktop/electron-app/config/forge.js b/ui/desktop/electron-app/config/forge.js
new file mode 100644
index 000000000..1b6aab69f
--- /dev/null
+++ b/ui/desktop/electron-app/config/forge.js
@@ -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);
+ },
+};
diff --git a/ui/desktop/electron-app/package.json b/ui/desktop/electron-app/package.json
index 104066699..c6dc8f01f 100644
--- a/ui/desktop/electron-app/package.json
+++ b/ui/desktop/electron-app/package.json
@@ -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",
diff --git a/ui/desktop/electron-app/src/config/menu.js b/ui/desktop/electron-app/src/config/menu.js
index addea3d1a..c1540e3f7 100644
--- a/ui/desktop/electron-app/src/config/menu.js
+++ b/ui/desktop/electron-app/src/config/menu.js
@@ -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',
diff --git a/ui/desktop/electron-app/src/index.js b/ui/desktop/electron-app/src/index.js
index 93c9c3024..1f220eb85 100644
--- a/ui/desktop/electron-app/src/index.js
+++ b/ui/desktop/electron-app/src/index.js
@@ -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
diff --git a/ui/desktop/electron-app/src/models/session-manager.js b/ui/desktop/electron-app/src/models/session-manager.js
index dfcfd923a..311d86899 100644
--- a/ui/desktop/electron-app/src/models/session-manager.js
+++ b/ui/desktop/electron-app/src/models/session-manager.js
@@ -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());
}
}
diff --git a/ui/desktop/electron-app/src/models/session.js b/ui/desktop/electron-app/src/models/session.js
index 6ec9d3ce8..a59316f7f 100644
--- a/ui/desktop/electron-app/src/models/session.js
+++ b/ui/desktop/electron-app/src/models/session.js
@@ -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();
}
});
diff --git a/ui/desktop/package.json b/ui/desktop/package.json
index fa2531e1f..c88ebd846 100644
--- a/ui/desktop/package.json
+++ b/ui/desktop/package.json
@@ -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",