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}} {{/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",
{{@caption}}