diff --git a/addons/api/addon/generated/models/session.js b/addons/api/addon/generated/models/session.js new file mode 100644 index 000000000..a33426e4c --- /dev/null +++ b/addons/api/addon/generated/models/session.js @@ -0,0 +1,36 @@ +import BaseModel from '../../models/base'; +import { attr } from '@ember-data/model'; + +export default class GeneratedSessionModel extends BaseModel { + + // =attributes + + @attr('string') user_id; + @attr('string') target_id; + @attr('string') host_id; + @attr('string') host_set_id; + + @attr('string', { + description: 'The name of the current status of this session.', + readOnly: true, + }) + status; + + @attr('date', { + description: 'The time this resource was created\nOutput only.', + readOnly: true, + }) + created_time; + + @attr('date', { + description: 'The time this resource was last updated\nOutput only.', + readOnly: true, + }) + updated_time; + + @attr('number', { + description: 'Current version number of this resource.', + }) + version; + +} diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js index b28558732..71b337f44 100644 --- a/addons/api/addon/models/account.js +++ b/addons/api/addon/models/account.js @@ -1,13 +1,32 @@ import GeneratedAccountModel from '../generated/models/account'; import { fragment } from 'ember-data-model-fragments/attributes'; +import { computed } from '@ember/object'; export default class AccountModel extends GeneratedAccountModel { + + // =attributes + /** * Attributes of this resource, if any, represented as a JSON fragment. * @type {FragmentAccountAttributesModel} */ @fragment('fragment-account-attributes', { defaultValue: {} }) attributes; + /** + * Convenience for getting username in account. + * @type {string} + */ + @computed('type', 'attributes.{login_name}') + get username() { + let username = ''; + switch (this.type) { + case 'password': + username = this.attributes.login_name; + break; + } + return username; + } + // =methods /** @@ -41,7 +60,11 @@ export default class AccountModel extends GeneratedAccountModel { * @param {object} options.adapterOptions * @return {Promise} */ - changePassword(currentPassword, newPassword, options = { adapterOptions: {} }) { + changePassword( + currentPassword, + newPassword, + options = { adapterOptions: {} } + ) { const defaultAdapterOptions = { method: 'change-password', currentPassword, diff --git a/addons/api/addon/models/base.js b/addons/api/addon/models/base.js index 2a01e39f5..d883c8846 100644 --- a/addons/api/addon/models/base.js +++ b/addons/api/addon/models/base.js @@ -26,6 +26,7 @@ import { get } from '@ember/object'; * 2) Or as an ID: `record.scopeID`. */ export default class BaseModel extends Model { + // =relationships /** diff --git a/addons/api/addon/models/host-set.js b/addons/api/addon/models/host-set.js index 61033dd79..583971735 100644 --- a/addons/api/addon/models/host-set.js +++ b/addons/api/addon/models/host-set.js @@ -2,6 +2,7 @@ import GeneratedHostSetModel from '../generated/models/host-set'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; export default class HostSetModel extends GeneratedHostSetModel { + // =attributes /** diff --git a/addons/api/addon/models/host.js b/addons/api/addon/models/host.js index 7748e023b..a2e855084 100644 --- a/addons/api/addon/models/host.js +++ b/addons/api/addon/models/host.js @@ -2,9 +2,11 @@ import GeneratedHostModel from '../generated/models/host'; import { fragment } from 'ember-data-model-fragments/attributes'; export default class HostModel extends GeneratedHostModel { + /** * Attributes of this resource, if any, represented as a JSON fragment. * @type {FragmentHostAttributesModel} */ @fragment('fragment-host-attributes', { defaultValue: {} }) attributes; + } diff --git a/addons/api/addon/models/session.js b/addons/api/addon/models/session.js new file mode 100644 index 000000000..b37f08fc2 --- /dev/null +++ b/addons/api/addon/models/session.js @@ -0,0 +1,27 @@ +import GeneratedSessionModel from '../generated/models/session'; + +export default class SessionModel extends GeneratedSessionModel { + + // =methods + + /** + * Cancels the session via the `cancel` method. + * See serializer and adapter for more information. + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + cancelSession(options = { adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'cancel', + }; + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions, + }, + }); + } + +} diff --git a/addons/api/addon/serializers/account.js b/addons/api/addon/serializers/account.js index 416bc488e..5f5291dcd 100644 --- a/addons/api/addon/serializers/account.js +++ b/addons/api/addon/serializers/account.js @@ -1,6 +1,14 @@ import ApplicationSerializer from './application'; export default class AccountSerializer extends ApplicationSerializer { + + // =properties + + /** + * @type {boolean} + */ + serializeScopeID = false; + // =methods /** @@ -22,8 +30,11 @@ export default class AccountSerializer extends ApplicationSerializer { } if (snapshot?.adapterOptions?.method === 'change-password') { const { currentPassword, newPassword } = snapshot?.adapterOptions; - serialized = - this.serializeForChangePassword(snapshot, currentPassword, newPassword); + serialized = this.serializeForChangePassword( + snapshot, + currentPassword, + newPassword + ); } return serialized; } diff --git a/addons/api/addon/serializers/application.js b/addons/api/addon/serializers/application.js index a41c97730..2819231fa 100644 --- a/addons/api/addon/serializers/application.js +++ b/addons/api/addon/serializers/application.js @@ -7,6 +7,14 @@ import { copy } from 'ember-copy'; * Manages serialization/normalization of data to/from the API. */ export default class ApplicationSerializer extends RESTSerializer { + + // =properties + + /** + * @type {boolean} + */ + serializeScopeID = true; + // =methods /** @@ -71,8 +79,10 @@ export default class ApplicationSerializer extends RESTSerializer { // Delete `scope` field delete serialized.scope; // And serialize `scope_id` - const scope_id = snapshot?.attr('scope')?.attr('scope_id'); - if (scope_id) serialized.scope_id = scope_id; + if(this.serializeScopeID) { + const scope_id = snapshot?.attr('scope')?.attr('scope_id'); + if (scope_id) serialized.scope_id = scope_id; + } return serialized; } diff --git a/addons/api/addon/serializers/host-set.js b/addons/api/addon/serializers/host-set.js index 591da7c76..ee38bd5eb 100644 --- a/addons/api/addon/serializers/host-set.js +++ b/addons/api/addon/serializers/host-set.js @@ -1,6 +1,14 @@ import ApplicationSerializer from './application'; export default class HostSetSerializer extends ApplicationSerializer { + + // =properties + + /** + * @type {boolean} + */ + serializeScopeID = false; + // =methods /** diff --git a/addons/api/addon/serializers/host.js b/addons/api/addon/serializers/host.js new file mode 100644 index 000000000..fad6f2ca7 --- /dev/null +++ b/addons/api/addon/serializers/host.js @@ -0,0 +1,12 @@ +import ApplicationSerializer from './application'; + +export default class HostSerializer extends ApplicationSerializer { + + // =properties + + /** + * @type {boolean} + */ + serializeScopeID = false; + +} diff --git a/addons/api/addon/serializers/session.js b/addons/api/addon/serializers/session.js new file mode 100644 index 000000000..d8d5ea7f8 --- /dev/null +++ b/addons/api/addon/serializers/session.js @@ -0,0 +1,31 @@ +import ApplicationSerializer from './application'; + +export default class SessionSerializer extends ApplicationSerializer { + // =methods + + /** + * If `adapterOptions.method` is `cancel`, the serialization should + * include the version field only. + * @override + * @param {Snapshot} snapshot + * @return {object} + */ + serialize(snapshot) { + let serialized = super.serialize(...arguments); + if (snapshot?.adapterOptions?.method === 'cancel') { + serialized = this.serializeForCancel(snapshot); + } + return serialized; + } + + /** + * Returns a payload containing only version + * @param {Snapshot} snapshot + * @return {object} + */ + serializeForCancel(snapshot) { + return { + version: snapshot.attr('version'), + }; + } +} diff --git a/addons/api/app/models/session.js b/addons/api/app/models/session.js new file mode 100644 index 000000000..3106229d6 --- /dev/null +++ b/addons/api/app/models/session.js @@ -0,0 +1 @@ +export { default } from 'api/models/session'; diff --git a/addons/api/app/serializers/host.js b/addons/api/app/serializers/host.js new file mode 100644 index 000000000..33844fbaf --- /dev/null +++ b/addons/api/app/serializers/host.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/host'; diff --git a/addons/api/app/serializers/session.js b/addons/api/app/serializers/session.js new file mode 100644 index 000000000..407fe5c23 --- /dev/null +++ b/addons/api/app/serializers/session.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/session'; diff --git a/addons/api/tests/unit/models/account-test.js b/addons/api/tests/unit/models/account-test.js index 554a72be5..f937b3c26 100644 --- a/addons/api/tests/unit/models/account-test.js +++ b/addons/api/tests/unit/models/account-test.js @@ -9,4 +9,17 @@ module('Unit | Model | account', function (hooks) { let model = store.createRecord('account', {}); assert.ok(model); }); + + test('it has an username attribute for password account', function (assert) { + assert.expect(2); + const store = this.owner.lookup('service:store'); + let model = store.createRecord('account', { + name: 'name', + type: 'password', + }); + assert.notOk(model.username); + model.attributes.login_name = 'username'; + assert.equal(model.username, 'username'); + }); + }); diff --git a/addons/api/tests/unit/models/session-test.js b/addons/api/tests/unit/models/session-test.js new file mode 100644 index 000000000..2d373a2f6 --- /dev/null +++ b/addons/api/tests/unit/models/session-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | session', 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('session', {}); + assert.ok(model); + }); +}); diff --git a/addons/api/tests/unit/serializers/application-test.js b/addons/api/tests/unit/serializers/application-test.js index 3ab733e52..5c2c07a20 100644 --- a/addons/api/tests/unit/serializers/application-test.js +++ b/addons/api/tests/unit/serializers/application-test.js @@ -30,7 +30,7 @@ module('Unit | Serializer | application', function (hooks) { }); }); - test('it serializes scope_id when scope is set', function (assert) { + test('it serializes scope_id', function (assert) { assert.expect(1); const store = this.owner.lookup('service:store'); const record = store.createRecord('user', { @@ -49,6 +49,27 @@ module('Unit | Serializer | application', function (hooks) { }); }); + test('it does not serialize scope_id when serializeScopeID is false', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('user'); + const record = store.createRecord('user', { + name: 'User', + description: 'Description', + scope: { + scope_id: 'global', + type: 'global', + }, + }); + const snapshot = record._createSnapshot(); + serializer.serializeScopeID = false; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + name: 'User', + description: 'Description' + }); + }); + test('it serializes non-nullish version fields', function (assert) { assert.expect(2); const store = this.owner.lookup('service:store'); diff --git a/addons/api/tests/unit/serializers/host-test.js b/addons/api/tests/unit/serializers/host-test.js new file mode 100644 index 000000000..c31a5ce4e --- /dev/null +++ b/addons/api/tests/unit/serializers/host-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'qunit'; +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'); + + assert.ok(serializer); + }); + + test('it serializes records', function(assert) { + let store = this.owner.lookup('service:store'); + let record = store.createRecord('host', {}); + + let serializedRecord = record.serialize(); + + assert.ok(serializedRecord); + }); +}); diff --git a/addons/api/tests/unit/serializers/session-test.js b/addons/api/tests/unit/serializers/session-test.js new file mode 100644 index 000000000..fde7c215f --- /dev/null +++ b/addons/api/tests/unit/serializers/session-test.js @@ -0,0 +1,31 @@ +import { module, test } from 'qunit'; +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'); + + assert.ok(serializer); + }); + + test('it serializes only version when `adapterOptions.method` is `cancel`', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('session'); + const record = store.createRecord('session', { + version: 1, + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + method: 'cancel', + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + version: 1, + }); + }); +}); diff --git a/addons/rose/addon/components/rose/card/link.js b/addons/rose/addon/components/rose/card/link.js deleted file mode 100644 index 162a08a0e..000000000 --- a/addons/rose/addon/components/rose/card/link.js +++ /dev/null @@ -1,7 +0,0 @@ -import Component from '@ember/component'; -import layout from '../../../templates/components/rose/card/link'; - -export default Component.extend({ - layout, - tagName: '', -}); diff --git a/addons/rose/addon/templates/components/rose/card/link.hbs b/addons/rose/addon/components/rose/card/link/index.hbs similarity index 100% rename from addons/rose/addon/templates/components/rose/card/link.hbs rename to addons/rose/addon/components/rose/card/link/index.hbs diff --git a/addons/rose/addon/components/rose/card/link.stories.mdx b/addons/rose/addon/components/rose/card/link/index.stories.mdx similarity index 100% rename from addons/rose/addon/components/rose/card/link.stories.mdx rename to addons/rose/addon/components/rose/card/link/index.stories.mdx diff --git a/addons/rose/addon/components/rose/cards.js b/addons/rose/addon/components/rose/cards.js deleted file mode 100644 index c54fb638d..000000000 --- a/addons/rose/addon/components/rose/cards.js +++ /dev/null @@ -1,7 +0,0 @@ -import Component from '@ember/component'; -import layout from '../../templates/components/rose/cards'; - -export default Component.extend({ - layout, - tagName: '', -}); diff --git a/addons/rose/addon/templates/components/rose/cards.hbs b/addons/rose/addon/components/rose/cards/index.hbs similarity index 100% rename from addons/rose/addon/templates/components/rose/cards.hbs rename to addons/rose/addon/components/rose/cards/index.hbs diff --git a/addons/rose/addon/components/rose/cards.stories.mdx b/addons/rose/addon/components/rose/cards/index.stories.mdx similarity index 100% rename from addons/rose/addon/components/rose/cards.stories.mdx rename to addons/rose/addon/components/rose/cards/index.stories.mdx diff --git a/addons/rose/addon/styles/rose/_typography.scss b/addons/rose/addon/styles/rose/_typography.scss index b44914ce8..c5fda86e4 100644 --- a/addons/rose/addon/styles/rose/_typography.scss +++ b/addons/rose/addon/styles/rose/_typography.scss @@ -54,3 +54,7 @@ p { a { color: var(--action); } + +time { + font-variant-numeric: tabular-nums; +} diff --git a/addons/rose/addon/styles/rose/components/table/_table.scss b/addons/rose/addon/styles/rose/components/table/_table.scss index ba63d37aa..956ab5faa 100644 --- a/addons/rose/addon/styles/rose/components/table/_table.scss +++ b/addons/rose/addon/styles/rose/components/table/_table.scss @@ -34,4 +34,8 @@ padding: sizing.rems(l) sizing.rems(s); } } + + [class*="rose-button"] { + margin-bottom: 0; + } } diff --git a/addons/rose/index.js b/addons/rose/index.js index 73cccf517..6de73c3ca 100644 --- a/addons/rose/index.js +++ b/addons/rose/index.js @@ -7,7 +7,7 @@ var mergeTrees = require('broccoli-merge-trees'); module.exports = { name: require('./package').name, included(app) { - this._super.included(app); + this._super.included.apply(this, arguments); this.includeStyles(app); this.includeIcons(app); diff --git a/addons/rose/package.json b/addons/rose/package.json index 5a8cccc67..fcf5cd228 100644 --- a/addons/rose/package.json +++ b/addons/rose/package.json @@ -61,7 +61,7 @@ "ember-qunit": "^4.6.0", "ember-radio-button": "^2.0.1", "ember-resolver": "^7.0.0", - "ember-source": "~3.12.0", + "ember-source": "~3.16.0", "ember-source-channel-url": "^2.0.1", "ember-try": "^1.4.0", "ember-uuid": "^2.1.0", diff --git a/ui/core/app/formats.js b/ui/core/app/formats.js index f5d1f1502..9ea07ca26 100644 --- a/ui/core/app/formats.js +++ b/ui/core/app/formats.js @@ -7,6 +7,15 @@ export default { }, }, date: { + default: { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false + }, hhmmss: { hour: 'numeric', minute: 'numeric', diff --git a/ui/core/app/helpers/format-date-iso-human.js b/ui/core/app/helpers/format-date-iso-human.js new file mode 100644 index 000000000..2744a4eeb --- /dev/null +++ b/ui/core/app/helpers/format-date-iso-human.js @@ -0,0 +1,7 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function formatDateIsoHuman(params/*, hash*/) { + return params[0].toISOString() + .replace('T', ' ') + .replace(/\.\d*Z/, ''); +}); diff --git a/ui/core/app/helpers/format-date-iso.js b/ui/core/app/helpers/format-date-iso.js new file mode 100644 index 000000000..52cd9687d --- /dev/null +++ b/ui/core/app/helpers/format-date-iso.js @@ -0,0 +1,5 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function formatDateIso(params/*, hash*/) { + return params[0].toISOString(); +}); diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 204ced768..0894289f9 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -52,6 +52,7 @@ Router.map(function () { }); this.route('new'); }); + this.route('sessions', function() {}); }); this.route('new'); }); diff --git a/ui/core/app/routes/scopes/scope/projects/project/sessions.js b/ui/core/app/routes/scopes/scope/projects/project/sessions.js new file mode 100644 index 000000000..b41d76d7a --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/sessions.js @@ -0,0 +1,85 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { all, hash } from 'rsvp'; +import { task, timeout } from 'ember-concurrency'; + +const POLL_TIMEOUT_SECONDS = 2.5; + +export default class ScopesScopeProjectsProjectSessionsRoute extends Route { + + // =services + + @service intl; + @service notify; + + // =attributes + + /** + * A simple Ember Concurrency-based polling task that refreshes the route + * every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes + * to session `status` that may occur. + * + * NOTE: tasks are sort of attributes and sort of methods, but they are not + * language-level constructs. Thus we annotate this task as if it + * is an attribute. + * @type {Task} + */ + @task(function * () { + while(true) { + yield timeout(POLL_TIMEOUT_SECONDS * 1000); + yield this.refresh(); + } + }).drop() poller; + + // =methods + + /** + * Loads all sessions under the current scope and encapsulates them into + * an array of objects containing their associated users and targets. + * @return {Promise{[{session: SessionModel, user: UserModel, target: TargetModel}]}} + */ + async model() { + const { id: scope_id } = this.modelFor('scopes.scope.projects.project'); + const sessions = await this.store.query('session', { scope_id }); + return all( + sessions.map(session => hash({ + session, + user: this.store.findRecord('user', session.user_id), + target: this.store.findRecord('target', session.target_id), + })) + ); + } + + /** + * When this route is activated (entered), begin polling for changes. + */ + activate() { + this.poller.perform(); + } + + /** + * When this route is deactivated (exited), stop polling for changes. + */ + deactivate() { + this.poller.cancelAll(); + } + + // =actions + + /** + * Cancels the specified session and notifies user of success or error. + * @param {SessionModel} + */ + @action + async cancelSession(session) { + try { + await session.cancelSession(); + this.notify.success(this.intl.t('notify.session-canceled')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } + +} diff --git a/ui/core/app/routes/scopes/scope/projects/project/sessions/index.js b/ui/core/app/routes/scopes/scope/projects/project/sessions/index.js new file mode 100644 index 000000000..a8ae56692 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/sessions/index.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeProjectsProjectSessionsIndexRoute extends Route { +} diff --git a/ui/core/app/routes/scopes/scope/roles/role/grants.js b/ui/core/app/routes/scopes/scope/roles/role/grants.js index 60368286b..d53bf4919 100644 --- a/ui/core/app/routes/scopes/scope/roles/role/grants.js +++ b/ui/core/app/routes/scopes/scope/roles/role/grants.js @@ -10,6 +10,22 @@ export default class ScopesScopeRolesRoleGrantsRoute extends Route { // =methods + /** + * Renders the grants-specific header template. + * Empties the actions and navigation outlets and renders a custom empty header. + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/roles/role/grants/-header', { + into: 'scopes/scope/roles/role', + outlet: 'header', + }); + } + + // =actions + /** * Adds a new grant to the role at the beginning of the grants list. * Grant creation is not immediately permanent; users may rollback the change diff --git a/ui/core/app/routes/scopes/scope/roles/role/principals.js b/ui/core/app/routes/scopes/scope/roles/role/principals.js index ca364acfd..3d5e55055 100644 --- a/ui/core/app/routes/scopes/scope/roles/role/principals.js +++ b/ui/core/app/routes/scopes/scope/roles/role/principals.js @@ -39,6 +39,20 @@ export default class ScopesScopeRolesRolePrincipalsRoute extends Route { return all(users.concat(groups)); } + /** + * Renders the principals-specific header template. + * Empties the actions and navigation outlets and renders a custom empty header. + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/roles/role/principals/-header', { + into: 'scopes/scope/roles/role', + outlet: 'header', + }); + } + // =actions /** @@ -57,4 +71,5 @@ export default class ScopesScopeRolesRolePrincipalsRoute extends Route { this.notify.error(error.message, { closeAfter: null }); } } + } diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs index 06a6a4291..2f295e9b4 100644 --- a/ui/core/app/templates/application.hbs +++ b/ui/core/app/templates/application.hbs @@ -5,7 +5,7 @@ - + {{#if this.session.isAuthenticated}} diff --git a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/-header.hbs b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/-header.hbs index 8eab49f00..f83a85430 100644 --- a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/-header.hbs +++ b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/-header.hbs @@ -1 +1,2 @@

{{t "resources.auth-method"}}

+

{{t "resources.descriptions.auth-method"}}

diff --git a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs index 8586ac7c0..83ed4fa1a 100644 --- a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs +++ b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs @@ -4,6 +4,7 @@ {{t 'form.label.name'}} + {{t 'form.label.identification'}} {{t 'form.label.id'}} @@ -19,6 +20,9 @@

{{account.description}}

+ + {{account.username}} + {{account.id}} diff --git a/ui/core/app/templates/scopes/scope/auth-methods/index.hbs b/ui/core/app/templates/scopes/scope/auth-methods/index.hbs index f06afc224..78e39a496 100644 --- a/ui/core/app/templates/scopes/scope/auth-methods/index.hbs +++ b/ui/core/app/templates/scopes/scope/auth-methods/index.hbs @@ -1,6 +1,7 @@

{{t "resources.auth-methods"}}

+

{{t "resources.descriptions.auth-methods"}}

diff --git a/ui/core/app/templates/scopes/scope/groups/group/-header.hbs b/ui/core/app/templates/scopes/scope/groups/group/-header.hbs index 6631940a3..1c7ff249d 100644 --- a/ui/core/app/templates/scopes/scope/groups/group/-header.hbs +++ b/ui/core/app/templates/scopes/scope/groups/group/-header.hbs @@ -1 +1,2 @@

{{t "resources.group"}}

+

{{t "resources.descriptions.group"}}

diff --git a/ui/core/app/templates/scopes/scope/groups/group/members.hbs b/ui/core/app/templates/scopes/scope/groups/group/members.hbs index fb562164b..24a6e13d9 100644 --- a/ui/core/app/templates/scopes/scope/groups/group/members.hbs +++ b/ui/core/app/templates/scopes/scope/groups/group/members.hbs @@ -7,7 +7,7 @@ {{t 'form.label.name'}} {{t 'form.label.id'}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/groups/index.hbs b/ui/core/app/templates/scopes/scope/groups/index.hbs index 0a973d184..21fd64095 100644 --- a/ui/core/app/templates/scopes/scope/groups/index.hbs +++ b/ui/core/app/templates/scopes/scope/groups/index.hbs @@ -6,6 +6,7 @@

{{t "resources.groups"}}

+

{{t "resources.descriptions.groups"}}

diff --git a/ui/core/app/templates/scopes/scope/orgs/index.hbs b/ui/core/app/templates/scopes/scope/orgs/index.hbs index fac9c044e..10b4f04a9 100644 --- a/ui/core/app/templates/scopes/scope/orgs/index.hbs +++ b/ui/core/app/templates/scopes/scope/orgs/index.hbs @@ -6,6 +6,7 @@

{{t "resources.orgs"}}

+

{{t "resources.descriptions.orgs"}}

diff --git a/ui/core/app/templates/scopes/scope/projects/index.hbs b/ui/core/app/templates/scopes/scope/projects/index.hbs index f892904cd..9c5ae805a 100644 --- a/ui/core/app/templates/scopes/scope/projects/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/index.hbs @@ -6,6 +6,7 @@

{{t "resources.projects"}}

+

{{t "resources.descriptions.projects"}}

diff --git a/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs b/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs index ae4f79e0b..e29af0451 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs @@ -1,4 +1,7 @@ + + {{t "resources.sessions"}} + {{t "resources.targets"}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-header.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-header.hbs index 82af78ffe..655ecb273 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-header.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-header.hbs @@ -1 +1,2 @@

{{t "resources.host-catalog"}}

+

{{t "resources.descriptions.host-catalog"}}

\ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-header.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-header.hbs index ae86a8707..05fa412e3 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-header.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-header.hbs @@ -1 +1,2 @@ -

{{t "resources.host-set"}}

\ No newline at end of file +

{{t "resources.host-set"}}

+

{{t "resources.descriptions.host-set"}}

\ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs index 437140df4..b0ef4a625 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs @@ -6,7 +6,7 @@ {{t "form.label.name"}} {{t "form.label.type"}} {{t "form.label.id"}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-header.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-header.hbs index 3187158d1..d9eea7635 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-header.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-header.hbs @@ -1 +1,2 @@ -

{{t "resources.host"}}

\ No newline at end of file +

{{t "resources.host"}}

+

{{t "resources.descriptions.host"}}

\ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/index.hbs index 630275b18..1a4717479 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/index.hbs @@ -6,7 +6,7 @@

{{t "resources.host-catalogs"}}

-

{{t "descriptions.host-catalogs"}}

+

{{t "resources.descriptions.host-catalogs"}}

diff --git a/ui/core/app/templates/scopes/scope/projects/project/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/index.hbs index c050b9570..243ef1276 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/index.hbs @@ -6,6 +6,7 @@

{{t "resources.project"}}

+

{{t "resources.descriptions.project"}}

diff --git a/ui/core/app/templates/scopes/scope/projects/project/sessions.hbs b/ui/core/app/templates/scopes/scope/projects/project/sessions.hbs new file mode 100644 index 000000000..37fea125e --- /dev/null +++ b/ui/core/app/templates/scopes/scope/projects/project/sessions.hbs @@ -0,0 +1,3 @@ +{{title (t "resources.sessions")}} + +{{outlet}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs new file mode 100644 index 000000000..6fe82d12b --- /dev/null +++ b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs @@ -0,0 +1,66 @@ + + + + + + + +

{{t "resources.sessions"}}

+
+ + + + {{#if @model}} + + + + + {{t "form.label.id"}} + {{t "resources.user"}} + {{t "resources.target"}} + {{t "form.label.started"}} + {{t "form.label.status"}} + {{t "titles.actions"}} + + + + {{#each @model as |sessionAggregate|}} + + + {{sessionAggregate.session.id}} + + {{sessionAggregate.user.displayName}} + {{sessionAggregate.target.displayName}} + + + + + {{sessionAggregate.session.status}} + + + + {{t "actions.cancel"}} + + + + {{/each}} + + + + {{else}} + + + + + {{t "descriptions.no-sessions"}} + + + + + {{/if}} + + + +
diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/index.hbs index cb6254a49..af99edaec 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/targets/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/index.hbs @@ -6,6 +6,7 @@

{{t "resources.targets"}}

+

{{t "resources.descriptions.targets"}}

diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/-header.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/-header.hbs index 721607f47..0dc39d88d 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/targets/target/-header.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/-header.hbs @@ -1 +1,2 @@

{{t "resources.target"}}

+

{{t "resources.descriptions.target"}}

diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs index 40f12df7f..583a8df55 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs @@ -8,7 +8,7 @@ {{t "form.label.name"}} {{t "form.label.type"}} {{t "form.label.id"}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/roles/index.hbs b/ui/core/app/templates/scopes/scope/roles/index.hbs index 835881d77..dcb462b4d 100644 --- a/ui/core/app/templates/scopes/scope/roles/index.hbs +++ b/ui/core/app/templates/scopes/scope/roles/index.hbs @@ -6,6 +6,7 @@

{{t "resources.roles"}}

+

{{t "resources.descriptions.roles"}}

diff --git a/ui/core/app/templates/scopes/scope/roles/role/-header.hbs b/ui/core/app/templates/scopes/scope/roles/role/-header.hbs index d7985c901..96ed6300a 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/-header.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/-header.hbs @@ -1 +1,2 @@

{{t "resources.role"}}

+

{{t "resources.descriptions.role"}}

diff --git a/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs b/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs index 8696fed6d..5ce0ab603 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs @@ -1 +1 @@ -

{{t "actions.add-principals"}}

+

{{t "actions.add-principals"}}

\ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/roles/role/grants/-header.hbs b/ui/core/app/templates/scopes/scope/roles/role/grants/-header.hbs new file mode 100644 index 000000000..6c9ebd67b --- /dev/null +++ b/ui/core/app/templates/scopes/scope/roles/role/grants/-header.hbs @@ -0,0 +1,2 @@ +

{{t "resources.role"}}

+

{{t "resources.descriptions.grants"}}

diff --git a/ui/core/app/templates/scopes/scope/roles/role/principals.hbs b/ui/core/app/templates/scopes/scope/roles/role/principals.hbs index b364b136f..160c91c7b 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/principals.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/principals.hbs @@ -8,7 +8,7 @@ {{t 'form.label.name'}} {{t 'form.label.type'}} {{t 'form.label.id'}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/roles/role/principals/-header.hbs b/ui/core/app/templates/scopes/scope/roles/role/principals/-header.hbs new file mode 100644 index 000000000..a549d9a8b --- /dev/null +++ b/ui/core/app/templates/scopes/scope/roles/role/principals/-header.hbs @@ -0,0 +1,2 @@ +

{{t "resources.role"}}

+

{{t "resources.descriptions.principals"}}

diff --git a/ui/core/app/templates/scopes/scope/users/index.hbs b/ui/core/app/templates/scopes/scope/users/index.hbs index ad58e6ca8..506abc01f 100644 --- a/ui/core/app/templates/scopes/scope/users/index.hbs +++ b/ui/core/app/templates/scopes/scope/users/index.hbs @@ -6,6 +6,7 @@

{{t "resources.users"}}

+

{{t "resources.descriptions.users"}}

diff --git a/ui/core/app/templates/scopes/scope/users/user/index.hbs b/ui/core/app/templates/scopes/scope/users/user/index.hbs index 1f8c67630..a57e6f8eb 100644 --- a/ui/core/app/templates/scopes/scope/users/user/index.hbs +++ b/ui/core/app/templates/scopes/scope/users/user/index.hbs @@ -6,6 +6,7 @@

{{t "resources.user"}}

+

{{t "resources.descriptions.user"}}

diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 399d7f03e..59cbf0180 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,7 +1,7 @@ import config from '../config/environment'; import { authHandler, deauthHandler } from './route-handlers/auth'; -// Uncomment to use in simulating error responses -// import { Response } from 'miragejs'; +import { pickRandomStatusString } from './factories/session'; +//import { Response } from 'miragejs'; export default function() { @@ -294,6 +294,38 @@ export default function() { return target.update(updatedAttrs); }); + // session + + // To simulate changes to `session.status` that may occur in the backend, + // we quietly randomize the value of the field on GET. + this.get('/sessions', function ({ sessions }, { queryParams: { scope_id } }) { + sessions.where(session => session.scopeId === scope_id) + .models + .forEach(session => session.update({ + status: pickRandomStatusString() + })); + return sessions.where(session => session.scopeId === scope_id) + }); + this.get('/sessions/:id', function ({ sessions }, { params: { id } }) { + const session = sessions.find(id); + return session.update({ + status: pickRandomStatusString() + }); + }); + this.post('/sessions/:idMethod', function ({ sessions }, { params: { idMethod } }) { + const attrs = this.normalizedRequestAttrs(); + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + const session = sessions.find(id); + const updatedAttrs = { + version: attrs.version + }; + if (method === 'cancel') { + updatedAttrs.status = 'canceling'; + } + return session.update(updatedAttrs); + }); + /* Uncomment the following line and the Response import above * Then change the response code to simulate error responses. * this.get('/scopes', () => new Response(505)); diff --git a/ui/core/mirage/factories/session.js b/ui/core/mirage/factories/session.js new file mode 100644 index 000000000..0100a4b75 --- /dev/null +++ b/ui/core/mirage/factories/session.js @@ -0,0 +1,46 @@ +import factory from '../generated/factories/target'; +import { trait } from 'ember-cli-mirage'; +import { random } from 'faker'; + +const statusStrings = [ + 'pending', + 'active', + 'canceling', + 'terminated' +]; + +export const pickRandomStatusString = () => random.arrayElement(statusStrings); + +export default factory.extend({ + + id: (i) => `session-id-${i}`, + status: pickRandomStatusString, + + /** + * Sessions generated with this trait will automatically form + * associations to user, target, host set, and host (if available) within + * the current scope. + */ + withAssociations: trait({ + afterCreate(record, server) { + const scopeId = record.scopeId; + if (record.scope.type === 'project') { + const orgScopeId = record.scope?.scope.id; + const user = + server.schema.users.where({ scopeId: orgScopeId }).models[0]; + if (user) record.update({ user }); + } + const target = server.schema.targets.where({ scopeId }).models[0]; + if (target) { + record.update({ target }); + const hostSet = target.hostSets.models[0]; + if (hostSet) { + record.update({ hostSet }); + const host = hostSet.hosts.models[0]; + if (host) record.update({ host }); + } + } + } + }) + +}); diff --git a/ui/core/mirage/generated/factories/session.js b/ui/core/mirage/generated/factories/session.js new file mode 100644 index 000000000..25aca3759 --- /dev/null +++ b/ui/core/mirage/generated/factories/session.js @@ -0,0 +1,14 @@ +import { Factory } from 'ember-cli-mirage'; +import { random, date } from 'faker'; + +/** + * GeneratedSessionModelFactory + */ +export default Factory.extend({ + + type: 'tcp', + created_time: () => date.recent(), + updated_time: () => date.recent(), + version: () => random.number(), + +}); diff --git a/ui/core/mirage/models/host-catalog.js b/ui/core/mirage/models/host-catalog.js index 08b1f5ad3..8c2757e71 100644 --- a/ui/core/mirage/models/host-catalog.js +++ b/ui/core/mirage/models/host-catalog.js @@ -1,7 +1,8 @@ -import { Model, belongsTo } from 'ember-cli-mirage'; +import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; export default Model.extend({ - scope: belongsTo() + scope: belongsTo(), + hostSets: hasMany() }); diff --git a/ui/core/mirage/models/session.js b/ui/core/mirage/models/session.js new file mode 100644 index 000000000..91a4da594 --- /dev/null +++ b/ui/core/mirage/models/session.js @@ -0,0 +1,11 @@ +import { Model, belongsTo } from 'ember-cli-mirage'; + +export default Model.extend({ + + scope: belongsTo(), + user: belongsTo(), + target: belongsTo(), + host: belongsTo(), + hostSet: belongsTo(), + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 9492f92e3..5d1a85fe6 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -26,6 +26,7 @@ export default function(server) { server.schema.scopes.where({type: 'project'}).models.forEach(scope => { server.createList('host-catalog', 2, { scope }, 'withChildren'); server.createList('target', 2, { scope }, 'withRandomHostSets'); + server.createList('session', 4, { scope }, 'withAssociations'); }); } diff --git a/ui/core/mirage/serializers/session.js b/ui/core/mirage/serializers/session.js new file mode 100644 index 000000000..3e14a23e6 --- /dev/null +++ b/ui/core/mirage/serializers/session.js @@ -0,0 +1,22 @@ +import ApplicationSerializer from './application'; +import { underscore } from '@ember/string'; + +export default ApplicationSerializer.extend({ + modelName: 'session', + + keyForRelationshipId(relationshipName) { + return `${this._container.inflector.singularize( + underscore(relationshipName) + )}_id`; + }, + + _hashForModel(model) { + const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments); + json.user_id = model.userId; + json.target_id = model.targetId; + json.host_set_id = model.hostSetId; + json.host_id = model.hostId; + return json; + } + +}); diff --git a/ui/core/package.json b/ui/core/package.json index 258f13a98..212ce3416 100644 --- a/ui/core/package.json +++ b/ui/core/package.json @@ -54,6 +54,7 @@ "ember-cli-sri": "^2.1.1", "ember-cli-template-lint": "^1.0.0-beta.3", "ember-cli-uglify": "^3.0.0", + "ember-concurrency": "^1.3.0", "ember-data": "~3.16.0", "ember-data-model-fragments": "5.0.0-beta.0", "ember-exam": "^5.0.1", diff --git a/ui/core/public/logo-boundary.svg b/ui/core/public/logo-boundary.svg deleted file mode 100644 index ea71154fb..000000000 --- a/ui/core/public/logo-boundary.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ui/core/tests/acceptance/accounts-test.js b/ui/core/tests/acceptance/accounts-test.js index 9412f115a..a7e6a544c 100644 --- a/ui/core/tests/acceptance/accounts-test.js +++ b/ui/core/tests/acceptance/accounts-test.js @@ -246,23 +246,49 @@ module('Acceptance | accounts', function (hooks) { ); }); - test('can set a new password for account', async function (assert) { - assert.expect(1); - this.server.post('/accounts/:id', (_, { requestBody }) => { - const attrs = JSON.parse(requestBody); - assert.equal(attrs.password, 'update password', "attributes don't match the expected ones"); + module('password', function() { + test('can set a new password for account', async function (assert) { + assert.expect(1); + this.server.post('/accounts/:id', (_, { requestBody }) => { + const attrs = JSON.parse(requestBody); + assert.equal(attrs.password, 'update password', 'new password is set'); + }); + await visit(urls.setAccountPassword); + await fillIn('[name="password"]', 'update password'); + await click('form [type="submit"]:not(:disabled)'); + await a11yAudit(); }); - await visit(urls.setAccountPassword); - await fillIn('[name="password"]', 'update password'); - await click('form [type="submit"]:not(:disabled)'); - await a11yAudit(); - }); - test('can cancel setting new password', async function (assert) { - assert.expect(1); - await visit(urls.setAccountPassword); - await fillIn('[name="password"]', 'update password'); - await click('form button:not([type="submit"])'); - assert.notOk(find('[name="password"]').textContent.trim()); + test('can cancel setting new password', async function (assert) { + assert.expect(1); + await visit(urls.setAccountPassword); + await fillIn('[name="password"]', 'update password'); + await click('form button:not([type="submit"])'); + assert.notOk(find('[name="password"]').textContent.trim()); + }); + + test('errors are displayed when setting password fails', async function (assert) { + assert.expect(1); + this.server.post('/accounts/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit(urls.setAccountPassword); + await fillIn('[name="password"]', 'update password'); + await click('form [type="submit"]'); + await a11yAudit(); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); + }); }); }); diff --git a/ui/core/tests/acceptance/auth-methods-test.js b/ui/core/tests/acceptance/auth-methods-test.js index d7f025c63..fa46de1ed 100644 --- a/ui/core/tests/acceptance/auth-methods-test.js +++ b/ui/core/tests/acceptance/auth-methods-test.js @@ -1,69 +1,268 @@ import { module, test } from 'qunit'; -//import { visit, currentURL } from '@ember/test-helpers'; +import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; -//import a11yAudit from 'ember-a11y-testing/test-support/audit'; -// import { -// authenticateSession, +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { Response } from 'miragejs'; +import { + authenticateSession, // // These are left here intentionally for future reference. // //currentSession, // //invalidateSession, -// } from 'ember-simple-auth/test-support'; +} from 'ember-simple-auth/test-support'; module('Acceptance | auth methods', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + const instances = { + scopes: { + global: null, + org: null, + }, + authMethod: null, + }; + const urls = { + orgScope: null, + authMethods: null, + newAuthMethod: null, + authMethod: null, + }; + hooks.beforeEach(function () { // Setup Mirage mock resources for this test + authenticateSession({}); + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.authMethod = this.server.create('auth-method', { + scope: instances.scopes.org, + }); + // Generate route URLs for resources + urls.orgScope = `/scopes/${instances.scopes.org.id}`; + urls.authMethods = `${urls.orgScope}/auth-methods`; + urls.newAuthMethod = `${urls.authMethods}/new`; + urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`; }); test('visiting auth methods', async function (assert) { - assert.expect(0); + assert.expect(1); + await visit(urls.authMethods); + await a11yAudit(); + assert.equal(currentURL(), urls.authMethods); }); test('can navigate to an auth method form', async function (assert) { - assert.expect(0); + assert.expect(1); + await visit(urls.authMethods); + await click('main tbody .rose-table-header-cell:nth-child(1) a'); + await a11yAudit(); + assert.equal(currentURL(), urls.authMethod); }); test('can update an auth method and save changes', async function (assert) { - assert.expect(0); + assert.expect(1); + await visit(urls.authMethod); + await fillIn('[name="name"]', 'update name'); + await click('form [type="submit"]:not(:disabled)'); + assert.equal(this.server.db.authMethods[0].name, 'update name'); }); test('can update an auth method and cancel changes', async function (assert) { - assert.expect(0); + assert.expect(1); + await visit(urls.authMethod) + await fillIn('[name="name"]', 'update name'); + await click('form button:not([type="submit"])'); + assert.notEqual(this.server.db.authMethods[0].name, 'update name'); }); test('can create new auth method', async function (assert) { - assert.expect(0); + assert.expect(1); + const authMethodsCount = this.server.db.authMethods.length; + await visit(urls.newAuthMethod); + await fillIn('[name="name"]', 'AuthMethod name'); + await fillIn('[name="description"]', 'description'); + await click('form [type="submit"]:not(:disabled)'); + assert.equal(this.server.db.authMethods.length, authMethodsCount + 1); }); test('can cancel new auth method creation', async function (assert) { - assert.expect(0); + assert.expect(2); + const authMethodsCount = this.server.db.authMethods.length; + await visit(urls.newAuthMethod); + await fillIn('[name="name"]', 'AuthMethod name'); + await fillIn('[name="description"]', 'description'); + await click('form button:not([type="submit"])'); + assert.equal(this.server.db.authMethods.length, authMethodsCount); + assert.equal(currentURL(), urls.authMethods); }); test('can delete an auth method', async function (assert) { - assert.expect(0); + assert.expect(1); + const authMethodsCount = this.server.db.authMethods.length; + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); + assert.equal(this.server.db.authMethods.length, authMethodsCount - 1); }); test('saving a new auth method with invalid fields displays error messages', async function (assert) { - assert.expect(0); + assert.expect(2); + this.server.post('/auth-methods', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + request_fields: [ + { + name: 'name', + description: 'Name is required.', + }, + ], + }, + } + ); + }); + await visit(urls.newAuthMethod); + await fillIn('[name="name"]', 'new auth method'); + await click('form [type="submit"]'); + await a11yAudit(); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'The request was invalid.', + 'Displays primary error message.' + ); + assert.ok( + find('.rose-form-error-message').textContent.trim(), + 'Name is required.', + 'Displays field-level errors.' + ); }); test('errors are displayed when save on auth method fails', async function (assert) { - assert.expect(0); + assert.expect(1); + this.server.patch('/auth-methods/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit(urls.authMethod); + await fillIn('[name="name"]', 'save auth method'); + await click('form [type="submit"]'); + await a11yAudit(); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); }); test('errors are displayed when delete on an auth method fails', async function (assert) { - assert.expect(0); + assert.expect(1); + this.server.del('/auth-methods/:id', () => { + return new Response( + 490, + {}, + { + status: 490, + code: 'error', + message: 'Oops.', + } + ); + }); + await visit(urls.authMethod); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); + await a11yAudit(); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); }); test('saving a new auth method with invalid fields displays error messages', async function (assert) { - assert.expect(0); + assert.expect(2); + this.server.patch('/auth-methods/:id', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + request_fields: [ + { + name: 'name', + description: 'Name is required.', + }, + ], + }, + } + ); + }); + await visit(urls.authMethod); + await fillIn('[name="name"]', 'existing auth method'); + await click('form [type="submit"]'); + await a11yAudit(); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'The request was invalid.', + 'Displays primary error message.' + ); + assert.ok( + find('.rose-form-error-message').textContent.trim(), + 'Name is required.', + 'Displays field-level errors.' + ); }); test('saving an existing auth method with invalid fields displays error messages', async function (assert) { - assert.expect(0); + assert.expect(2); + this.server.patch('/auth-methods/:id', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: { + request_fields: [ + { + name: 'name', + description: 'Name is required.', + }, + ], + }, + } + ); + }); + await visit(urls.authMethod); + await fillIn('[name="name"]', 'existing auth method'); + await click('form [type="submit"]'); + await a11yAudit(); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'The request was invalid.', + 'Displays primary error message.' + ); + assert.ok( + find('.rose-form-error-message').textContent.trim(), + 'Name is required.', + 'Displays field-level errors.' + ); }); }); diff --git a/ui/core/tests/acceptance/targets-test.js b/ui/core/tests/acceptance/targets-test.js index 1f3ca2e13..822986f12 100644 --- a/ui/core/tests/acceptance/targets-test.js +++ b/ui/core/tests/acceptance/targets-test.js @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers'; +import { visit, currentURL, click, find, findAll, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; @@ -47,11 +47,13 @@ module('Acceptance | targets', function (hooks) { type: 'project', scope: { id: instances.scopes.org.id, type: 'org' }, }); - instances.hostSets = this.server.createList('host-set', 4, { - scope: instances.scopes.project, - }); + + instances.hostCatalog = this.server.create('host-catalog', { + scope: instances.scopes.project + }, 'withChildren'); instances.target = this.server.create('target', { scope: instances.scopes.project, + hostSets: instances.hostCatalog.hostSets }); // Generate route URLs for resources @@ -161,16 +163,8 @@ module('Acceptance | targets', function (hooks) { await fillIn('[name="name"]', 'new target'); await click('form [type="submit"]'); await a11yAudit(); - assert.ok( - find('[role="alert"]').textContent.trim(), - 'The request was invalid.', - 'Displays primary error message.' - ); - assert.ok( - find('.rose-form-error-message').textContent.trim(), - 'Name is required.', - 'Displays field-level errors.' - ); + assert.ok(find('[role="alert"]')); + assert.ok(find('.rose-form-error-message')); }); test('errors are displayed when save on target fails', async function (assert) { @@ -190,11 +184,7 @@ module('Acceptance | targets', function (hooks) { await fillIn('[name="name"]', 'save target'); await click('form [type="submit"]'); await a11yAudit(); - assert.ok( - find('[role="alert"]').textContent.trim(), - 'Oops.', - 'Displays primary error message.' - ); + assert.ok(find('[role="alert"]')); }); test('errors are displayed when delete on a target fails', async function (assert) { @@ -213,11 +203,7 @@ module('Acceptance | targets', function (hooks) { await visit(urls.target); await click('.rose-layout-page-actions .rose-dropdown-button-danger'); await a11yAudit(); - assert.ok( - find('[role="alert"]').textContent.trim(), - 'Oops.', - 'Displays primary error message.' - ); + assert.ok(find('[role="alert"]')); }); test('saving an existing target with invalid fields displays error messages', async function (assert) { @@ -245,35 +231,100 @@ module('Acceptance | targets', function (hooks) { await fillIn('[name="name"]', 'existing target'); await click('form [type="submit"]'); await a11yAudit(); - assert.ok( - find('[role="alert"]').textContent.trim(), - 'The request was invalid.', - 'Displays primary error message.' - ); - assert.ok( - find('.rose-form-error-message').textContent.trim(), - 'Name is required.', - 'Displays field-level errors.' - ); + assert.ok(find('[role="alert"]')); + assert.ok(find('.rose-form-error-message')); }); test('visiting target host sets', async function (assert) { - assert.expect(1); + assert.expect(3); + const targetHostSetCount = instances.target.hostSets.length; await visit(urls.targetHostSets); await a11yAudit(); assert.equal(currentURL(), urls.targetHostSets); + assert.ok(targetHostSetCount); + assert.equal(findAll('tbody tr').length, targetHostSetCount); }); test('can delete a host sets', async function (assert) { - assert.expect(0); + assert.expect(2); + const targetHostSetCount = instances.target.hostSets.length; + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, targetHostSetCount); + await click('tbody tr .rose-dropdown-button-danger'); + assert.equal(findAll('tbody tr').length, targetHostSetCount - 1); + }); + + test('shows error message on host set remove', async function (assert) { + assert.expect(2); + this.server.post('/targets/:idMethod', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: {}, + } + ); + }); + const targetHostSetCount = instances.target.hostSets.length; + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, targetHostSetCount); + await click('tbody tr .rose-dropdown-button-danger'); + assert.ok(find('[role="alert"]')); }); test('select and save host sets to add', async function (assert) { - assert.expect(0); - await visit(urls.targetAddHostSets); + assert.expect(3); + instances.target.update({ hostSetIds: [] }); + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, 0); + await click('.rose-layout-page-actions a') + assert.equal(currentURL(), urls.targetAddHostSets); + await click('tbody label'); + await click('form [type="submit"]'); + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, 1); }); test('select and cancel host sets to add', async function (assert) { - assert.expect(0); + assert.expect(4); + const targetHostSetCount = instances.target.hostSets.length; + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, targetHostSetCount); + // first, remove a target host set (otherwise none would be available to add) + await click('tbody tr .rose-dropdown-button-danger'); + assert.equal(findAll('tbody tr').length, targetHostSetCount - 1); + await click('.rose-layout-page-actions a') + assert.equal(currentURL(), urls.targetAddHostSets); + await click('tbody label'); + await click('form [type="button"]'); + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, targetHostSetCount - 1); + }); + + test('shows error message on host set add', async function (assert) { + assert.expect(4); + this.server.post('/targets/:idMethod', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: {}, + } + ); + }); + instances.target.update({ hostSetIds: [] }); + await visit(urls.targetAddHostSets); + assert.equal(instances.target.hostSets.length, 0); + assert.equal(currentURL(), urls.targetAddHostSets); + await click('tbody label'); + await click('form [type="submit"]'); + assert.equal(currentURL(), urls.targetAddHostSets); + assert.equal(instances.target.hostSets.length, 0); }); }); diff --git a/ui/core/tests/integration/helpers/format-date-iso-human-test.js b/ui/core/tests/integration/helpers/format-date-iso-human-test.js new file mode 100644 index 000000000..94eec92fa --- /dev/null +++ b/ui/core/tests/integration/helpers/format-date-iso-human-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Helper | format-date-iso-human', function(hooks) { + setupRenderingTest(hooks); + + // Replace this with your real tests. + test('it renders', async function(assert) { + this.set('inputValue', new Date('2020-01-01T00:00:00.999Z')); + + await render(hbs`{{format-date-iso-human inputValue}}`); + + assert.equal(this.element.textContent.trim(), '2020-01-01 00:00:00'); + }); +}); diff --git a/ui/core/tests/integration/helpers/format-date-iso-test.js b/ui/core/tests/integration/helpers/format-date-iso-test.js new file mode 100644 index 000000000..2954d3890 --- /dev/null +++ b/ui/core/tests/integration/helpers/format-date-iso-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Helper | format-date-iso', function(hooks) { + setupRenderingTest(hooks); + + // Replace this with your real tests. + test('it renders', async function(assert) { + this.set('inputValue', new Date('2020-01-01T00:00:00.999Z')); + + await render(hbs`{{format-date-iso inputValue}}`); + + assert.equal(this.element.textContent.trim(), '2020-01-01T00:00:00.999Z'); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js new file mode 100644 index 000000000..70769438e --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/project/sessions', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/project/sessions'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js new file mode 100644 index 000000000..a774bf223 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/projects/project/sessions/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/projects/project/sessions/index'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index fdfb779f9..4c044bb2f 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -77,6 +77,35 @@ resources: targets: Targets accounts: Accounts account: Account + sessions: Sessions + session: Session + descriptions: + user: A User inherits from Principal ("is-a" Principal) allowing it to be associated with zero or more Roles. + users: A user is any entity authorized to access Boundary using authentication credentials specific to one of the configured Authentication Methods. A User is owned by one and only one Organization. A User is associated with zero or one Authentication Methods. A User not associated with an Authentication Method can not log in to the system. A User can belong to zero or more Groups. A User is deleted when the Organization it belongs to is deleted. The lifecycle of a User is not tied to the lifecycle of any Authentication Method, Group or Role, but rather to its Organization only. + role: A Role can be assigned to zero or more Principals. A Principal assigned a Role receives all capabilities granted by any Direct Grant owned by the Role. A Role is deleted when the Scope it belongs to is deleted. All Direct Grants owned by a Role are deleted when the Role is deleted. + roles: A role is a collection of capabilities granted to any Principal the Role is assigned to. A Role belongs to one and only one Scope. A Role owns zero or more Direct Grants. The lifecycle of a Role is not tied to the lifecycle of any Principal. + grants: A grant represents a set of capabilities granted to a role. It couples a set of Actions to either a set of Resource Types or an individual Resource. A Direct Grant belongs exclusively to one and only one Role. However, equivalent Direct Grants may exist across different Roles. A Direct Grant references one or more Actions and either one or more Resource types or one specific Resource. A Direct Grant is deleted when the Role it belongs to is deleted. A Direct Grant is also deleted if it is associated with a specific Resource and that Resource itself is deleted. The lifecycle of a Direct Grant is not tied to the lifecycle of any Action or Resource Type. + principals: A principal is any entity which can be assigned capabilities. Principal is abstract. User, Group, and Project are concrete Principals. A Principal can be assigned to zero or more Roles. + group: All Users in a Group are granted the capabilities of all Roles the Group is associated with. A Group is deleted when the Scope it belongs to is deleted. + groups: Groups are collections of Users used only for access control purposes. A Group is owned by one and only one Scope. A Group can contain zero or more Users. A Group inherits from Principal ("is-a" Principal) allowing it to be associated with zero or more Roles. The lifecycle of a Group is not tied to the lifecycle of any User or Role. + org: When an Organization is deleted, all resources owned by it are also deleted. + orgs: An Organization is a top-level container, and owns zero to many Projects and zero to many Authentication Methods. An Organization inherits from Scope ("is-a" Scope) allowing it to own zero to many Groups, Roles, Targets, or Host Catalogs. The lifecycle of an Organization is not tied to anything else. A Role at the Organization level can grant permissions at the Project level. + project: When a Project is deleted, all resources owned by it are also deleted. The lifecycle of a Project is not tied to any resource it contains. + projects: A project is a child scope of an organization and is owned by an Organization. A Project inherits from Scope ("is-a" Scope) allowing it to own zero to many Groups, Roles, Targets, or Host Catalogs. A Project also inherits from Principal ("is-a" Principal) allowing it to be associated with zero or more Roles. A Project has no additional relationships beyond those defined in Scope and Principal. A Project is deleted when the Organization it belongs to is deleted. + host-catalog: A host catalog is a permission boundary modeled as a container. + host-catalogs: A host catalog is a permission boundary modeled as a container. + auth-method: An Authentication Method creates and references zero or more Users. An Authentication Method is deleted when the Organization it belongs to is deleted. The lifecycle of an Authentication Method is not tied to the lifecycle of any User. + auth-methods: An authentication method is a method configured for an Organization to authenticate Users. An Authentication Method is owned by one and only one Organization. + host-set: Host Set is abstract. A Host Set is deleted when the Host Catalog it belongs to is deleted. + host-sets: A host set is a resource which provides a subset of Hosts from the set of Hosts of the Host Catalog it belongs to. A Host Set belongs to one and only one Host Catalog. A Host Set can contain zero or more Hosts. A Host Set can be contained by zero or more Targets. The lifecycle of a Host Set is not tied to the lifecycle of any Targets or Hosts. + host: Host is abstract. A Host is deleted when the Host Catalog it belongs to is deleted. + hosts: A host is a computing element with a network address reachable from Boundary. A Host belongs to one and only one Host Catalog. A Host can be contained by zero or more Host Sets. The lifecycle of a Host is not tied to the lifecycle of any Host Sets. + target: Target is abstract. A Target is deleted when the Scope it belongs to is deleted. + targets: A target is a networked service a User can connect to and interact with through Boundary. A Target does not directly contain any secrets. A Target contains a collection of Hosts. A Target belongs to one and only one Scope. A Target can contain zero or more Host Sets. The lifecycle of a Target is not tied to the lifecycle of any Host Set. + accounts: Accounts + account: Account + sessions: Sessions + session: Session titles: iam: IAM new: New @@ -114,6 +143,8 @@ titles: accounts: Accounts account: Account accounts-welcome: Welcome to Accounts + no-sessions: No Active Sessions + actions: Actions descriptions: host-catalogs: Overview of collections of hosts and host sets. empty-set: There are no items to display yet. You may be able to add items or try back later. @@ -136,6 +167,7 @@ descriptions: no-available-users: No users are available to add to this group. account-set-password: Set a password for the account. accounts-welcome: No accounts are available in this auth method. + no-sessions: There are no active sessions at this time. states: info: Info success: Success @@ -157,6 +189,8 @@ form: default_port: Default Port current_password: Current Password new_password: New Password + started: Started + status: Status errors: 401: title: You are not signed in @@ -180,3 +214,4 @@ notify: create-success: Created succesfully. save-success: Saved succesfully. delete-success: Deleted successfully. + session-canceled: Session canceled. diff --git a/yarn.lock b/yarn.lock index 5304e5ace..03ce48f88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4295,11 +4295,6 @@ ast-types@0.13.3: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7" integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA== -ast-types@0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" - integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= - astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -8434,7 +8429,7 @@ ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.11.0, ember-cli-babel@^6.12.0, ember-cli-version-checker "^2.1.2" semver "^5.5.0" -ember-cli-babel@^7.1.2, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.4.1, ember-cli-babel@^7.5.0, ember-cli-babel@^7.7.0, ember-cli-babel@^7.7.3: +ember-cli-babel@^7.1.2, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.4.1, ember-cli-babel@^7.5.0, ember-cli-babel@^7.7.3: version "7.20.0" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.20.0.tgz#aa77eadd15036a053534fc68f7933bb97c5a1e25" integrity sha512-DwV5Cbn8wlRFpG8zQXxYPRb0NRnnaj/2qVDODeUEjM/doDhoDDbTSSaEI5Pf0kIlkVIimyGB8qpGv1fkgj2IfQ== @@ -8923,6 +8918,15 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-co ember-cli-version-checker "^2.1.1" semver "^5.4.1" +ember-concurrency@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-1.3.0.tgz#66f90fb792687470bcee1172adc0ebf33f5e8b9c" + integrity sha512-DwGlfWFpYyAkTwsedlEtK4t1DznJSculAW6Vq5S1C0shVPc5b6tTpHB2FFYisannSYkm+wpm1f1Pd40qiNPtOQ== + dependencies: + ember-cli-babel "^7.7.3" + ember-compatibility-helpers "^1.2.0" + ember-maybe-import-regenerator "^0.1.6" + ember-cookies@^0.5.0, ember-cookies@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/ember-cookies/-/ember-cookies-0.5.2.tgz#06e33463f2f83254fefaf224cc944dec3fb9d3ba" @@ -9280,13 +9284,6 @@ ember-route-action-helper@^2.0.8: ember-cli-babel "^6.8.1" ember-getowner-polyfill "^2.0.0" -ember-router-generator@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.3.tgz#8ed2ca86ff323363120fc14278191e9e8f1315ee" - integrity sha1-jtLKhv8yM2MSD8FCeBkeno8TFe4= - dependencies: - recast "^0.11.3" - ember-router-generator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-2.0.0.tgz#d04abfed4ba8b42d166477bbce47fccc672dbde0" @@ -9324,26 +9321,6 @@ ember-source-channel-url@^2.0.1: dependencies: got "^8.0.1" -ember-source@~3.12.0: - version "3.12.3" - resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.12.3.tgz#112462b8c1dfa124c0d021ca7eeeb04b5a5488ca" - integrity sha512-uzyoOVg1ygTY/InjwP0sgIkHiMiRylDauMY9K+vM15WkmmfrMaetklxxnqWThvsL4onBRRW8MKCj+q41Ra/NQQ== - dependencies: - broccoli-funnel "^2.0.2" - broccoli-merge-trees "^3.0.2" - chalk "^2.4.2" - ember-cli-babel "^7.7.0" - ember-cli-get-component-path-option "^1.0.0" - ember-cli-is-package-missing "^1.0.0" - ember-cli-normalize-entity-name "^1.0.0" - ember-cli-path-utils "^1.0.0" - ember-cli-string-utils "^1.1.0" - ember-cli-version-checker "^3.1.3" - ember-router-generator "^1.2.3" - inflection "^1.12.0" - jquery "^3.4.1" - resolve "^1.11.1" - ember-source@~3.16.0: version "3.16.8" resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.16.8.tgz#83f1a55a2b9835b272d7ec5b121a7af490c86725" @@ -9930,11 +9907,6 @@ esprima@~3.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" integrity sha1-U88kes2ncxPlUcOqLnM0LT+099k= -esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - esquery@^1.0.1: version "1.3.1" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" @@ -15667,7 +15639,7 @@ prismjs@~1.17.0: optionalDependencies: clipboard "^2.0.0" -private@^0.1.6, private@^0.1.8, private@~0.1.5: +private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== @@ -16488,16 +16460,6 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" -recast@^0.11.3: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - recast@^0.17.3: version "0.17.6" resolved "https://registry.yarnpkg.com/recast/-/recast-0.17.6.tgz#64ae98d0d2dfb10ff92ff5fb9ffb7371823b69fa" @@ -17786,7 +17748,7 @@ source-map@0.4.x, source-map@^0.4.2: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=