Merge remote-tracking branch 'origin' into populate-global-footer
No files matched your search
@@ -1,3 +1,82 @@
|
||||
import GeneratedUserModel from '../generated/models/user';
|
||||
import { attr } from '@ember-data/model';
|
||||
import { A } from '@ember/array';
|
||||
|
||||
export default class UserModel extends GeneratedUserModel {}
|
||||
export default class UserModel extends GeneratedUserModel {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Accounts is read-only under normal circumstances. But accounts can
|
||||
* be persisted via calls to `addAccounts()` or `removeAccounts()`.
|
||||
*/
|
||||
@attr({
|
||||
readOnly: true,
|
||||
defaultValue: () => A(),
|
||||
emptyArrayIfMissing: true,
|
||||
})
|
||||
account_ids;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Adds accounts via the `add-accounts` method.
|
||||
* See serializer and adapter for more information.
|
||||
* @param {[string]} accountIDs
|
||||
* @param {object} options
|
||||
* @param {object} options.adapterOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
addAccounts(accountIDs, options = { adapterOptions: {} }) {
|
||||
const defaultAdapterOptions = {
|
||||
method: 'add-accounts',
|
||||
accountIDs,
|
||||
};
|
||||
// There is no "deep merge" in ES.
|
||||
// All of this nonsense is here to ensure we get
|
||||
// a decent merge of `adapterOptions`.
|
||||
return this.save({
|
||||
...options,
|
||||
adapterOptions: {
|
||||
...defaultAdapterOptions,
|
||||
...options.adapterOptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove accounts via the `remove-accounts` method.
|
||||
* See serializer and adapter for more information.
|
||||
* @param {[string]} accountIDs
|
||||
* @param {object} options
|
||||
* @param {object} options.adapterOptions
|
||||
* @return {Promise}
|
||||
*/
|
||||
removeAccounts(accountIDs, options = { adapterOptions: {} }) {
|
||||
const defaultAdapterOptions = {
|
||||
method: 'remove-accounts',
|
||||
accountIDs,
|
||||
};
|
||||
// There is no "deep merge" in ES.
|
||||
// All of this nonsense is here to ensure we get
|
||||
// a decent merge of `adapterOptions`.
|
||||
return this.save({
|
||||
...options,
|
||||
adapterOptions: {
|
||||
...defaultAdapterOptions,
|
||||
...options.adapterOptions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a single account via the `remove-accounts` method.
|
||||
* @param {number} accountID
|
||||
* @param {object} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
removeAccount(accountID, options) {
|
||||
return this.removeAccounts([accountID], options);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class UserSerializer extends ApplicationSerializer {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* If `adapterOptions.accountIDs` is set (to an array of user IDs),
|
||||
* then the payload is serialized via `serializewithAccounts`.
|
||||
* @override
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {object}
|
||||
*/
|
||||
serialize(snapshot) {
|
||||
let serialized = super.serialize(...arguments);
|
||||
const accountIDs = snapshot?.adapterOptions?.accountIDs;
|
||||
if (accountIDs) serialized =
|
||||
this.serializewithAccounts(snapshot, accountIDs);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a payload containing only the `account_ids` array (and version).
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {[string]} account_ids
|
||||
* @return {object}
|
||||
*/
|
||||
serializewithAccounts(snapshot, account_ids) {
|
||||
return {
|
||||
version: snapshot.attr('version'),
|
||||
account_ids,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/user';
|
||||
@@ -1,12 +1,115 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Model | user', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('user', {});
|
||||
assert.ok(model);
|
||||
test('it has an `addAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:add-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['123_abc', 'foobar'],
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
type: 'user',
|
||||
attributes: {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
account_ids: ['1', '2'],
|
||||
version: 1,
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('user', '123abc');
|
||||
await model.addAccounts(['123_abc', 'foobar']);
|
||||
});
|
||||
|
||||
test('it has a `removeAccounts` method that users a specific POST API endpoint and serialization', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['1', '3'],
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
type: 'user',
|
||||
attributes: {
|
||||
name: 'user',
|
||||
description: 'Description',
|
||||
account_ids: ['4', '5'],
|
||||
version: 1,
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('user', '123abc');
|
||||
await model.removeAccounts(['1', '3']);
|
||||
});
|
||||
|
||||
test('it has a `removeAccount` method that deletes a single account set using `removeAccounts` method', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/users/123abc:remove-accounts', (schema, request) => {
|
||||
const body = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(body, {
|
||||
account_ids: ['3'],
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: '123abc',
|
||||
type: 'user',
|
||||
attributes: {
|
||||
name: 'user',
|
||||
description: 'Description',
|
||||
account_ids: ['1', '3'],
|
||||
version: 1,
|
||||
scope: {
|
||||
scope_id: 'o_1',
|
||||
type: 'scope',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const model = store.peekRecord('user', '123abc');
|
||||
await model.removeAccount('3');
|
||||
});
|
||||
|
||||
test('it defaults `accounts_ids` to an empty array when model instance does not define it', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
store.push({
|
||||
data: {
|
||||
id: 'user_123',
|
||||
type: 'user',
|
||||
},
|
||||
});
|
||||
const user = store.peekRecord('user', 'user_123');
|
||||
assert.equal(
|
||||
user.account_ids.length,
|
||||
0,
|
||||
'User has empty account_ids by default'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -154,7 +154,11 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'user',
|
||||
attributes: { name: 'User 1', scope: { scope_id: 'o_123' } },
|
||||
attributes: {
|
||||
name: 'User 1',
|
||||
account_ids: [],
|
||||
scope: { scope_id: 'o_123' }
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | user', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it serializes users normally, without accounts', 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',
|
||||
account_ids: ['1', '2'],
|
||||
version: 1,
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {};
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
assert.deepEqual(serializedRecord, {
|
||||
name: 'user',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('it serializes only host sets and version when an `adapterOptions.accountIDs` array is passed', 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',
|
||||
account_ids: ['1', '2'],
|
||||
version: 1,
|
||||
});
|
||||
const snapshot = record._createSnapshot();
|
||||
snapshot.adapterOptions = {
|
||||
accountIDs: ['4', '5'],
|
||||
};
|
||||
const serializedRecord = serializer.serialize(snapshot);
|
||||
assert.deepEqual(serializedRecord, {
|
||||
account_ids: ['4', '5'],
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('it normalizes missing account_ids to empty array', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('user');
|
||||
const user = store.createRecord('user').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
name: 'user 1',
|
||||
scope: { id: 'o_123' },
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
user,
|
||||
payload
|
||||
);
|
||||
assert.deepEqual(normalized, {
|
||||
included: [],
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'user',
|
||||
attributes: {
|
||||
name: 'user 1',
|
||||
scope: { scope_id: 'o_123' },
|
||||
account_ids: [],
|
||||
},
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,11 +25,11 @@
|
||||
}
|
||||
|
||||
&.rose-dropdown-button-danger {
|
||||
--background-color: var(--white);
|
||||
--background-color: var(--subtle);
|
||||
--color: var(--failure);
|
||||
|
||||
&:hover {
|
||||
--color: var(--white);
|
||||
--color: var(--subtle);
|
||||
--background-color: var(--failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ $chevron: "data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231b1f26' xm
|
||||
align-items: center;
|
||||
$yPadding: sizing.rems(xxs) + sizing.rems(xxxs);
|
||||
padding: $yPadding sizing.rems(m) $yPadding sizing.rems(l);
|
||||
color: var(--black);
|
||||
color: var(--stark);
|
||||
border-radius: sizing.rems(xxxs);
|
||||
|
||||
// Hide default twistie triangle
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
}
|
||||
|
||||
&.rose-dropdown-link-danger {
|
||||
--background-color: var(--white);
|
||||
--background-color: var(--subtle);
|
||||
--color: var(--failure);
|
||||
|
||||
&:hover {
|
||||
--color: var(--white);
|
||||
--color: var(--subtle);
|
||||
--background-color: var(--failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
&:focus {
|
||||
~ .rose-form-checkbox-label {
|
||||
&::before {
|
||||
border-color: var(--info);
|
||||
border-color: var(--action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
&:checked {
|
||||
~ .rose-form-checkbox-label {
|
||||
&::before {
|
||||
border-color: var(--info);
|
||||
border-color: var(--action);
|
||||
}
|
||||
&::after {
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 8 8'><path fill='%231563ff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/></svg>");
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
line-height: $lineHeight;
|
||||
padding: 0 sizing.rems(m);
|
||||
margin-bottom: $threePX;
|
||||
border-bottom: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
|
||||
border-bottom: sizing.rems(xxxxs) solid var(--ui-border-subtler-1);
|
||||
}
|
||||
|
||||
.rose-nav-link {
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
//text-decoration: underline;
|
||||
color: var(--action);
|
||||
background-color: var(--action-subtler-1h);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: sizing.rems(m);
|
||||
box-shadow: inset 0 -#{sizing.rems(xxxxs)} 0 var(--ui-gray-subtler-2);
|
||||
box-shadow: inset 0 -#{sizing.rems(xxxxs)} 0 var(--ui-border);
|
||||
|
||||
.rose-nav-link {
|
||||
--color: var(--ui-gray-starker-1);
|
||||
@@ -22,13 +22,17 @@
|
||||
|
||||
&:hover {
|
||||
--box-shadow-color: var(--ui-gray-subtler-2);
|
||||
--background: var(--ui-gray-subtler-5);
|
||||
--background: var(--ui-gray-subtler-6);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
--color: var(--action);
|
||||
--box-shadow-color: currentColor;
|
||||
|
||||
&:hover {
|
||||
--color: var(--action-starker-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,64 +79,63 @@
|
||||
--ui-header-subtler: #{color(ui-cool-gray, 700)};
|
||||
}
|
||||
|
||||
// NOTE: dark mode is disabled for now.
|
||||
// Dark Mode
|
||||
// @media (prefers-color-scheme: dark) {
|
||||
// :root {
|
||||
// // poles
|
||||
// --subtle: #{darken(color(ui-gray, 900), 2%)};
|
||||
// --stark: var(--white);
|
||||
// --stark-components: 255, 255, 255;
|
||||
//
|
||||
// // theme colors
|
||||
// --info-subtler-3: #{color(blue, 800)};
|
||||
// --info-subtler-2: #{color(blue, 700)};
|
||||
// --info-subtler-1: #{color(blue, 600)};
|
||||
// --info: #{color(blue)};
|
||||
// --info-starker-1: #{color(blue, 300)};
|
||||
// --info-starker-2: #{color(blue, 200)};
|
||||
//
|
||||
// --alert-subtler-2: #{color(orange, 900)};
|
||||
// --alert-subtler-1: #{color(orange, 800)};
|
||||
// --alert: #{color(orange)};
|
||||
// --alert-starker-1: #{color(orange, 200)};
|
||||
//
|
||||
// --warning-subtler-2: #{color(yellow, 900)};
|
||||
// --warning-subtler-1: #{color(yellow, 800)};
|
||||
// --warning: #{color(yellow)};
|
||||
// --warning-starker-1: #{color(yellow, 200)};
|
||||
//
|
||||
// --failure-subtler-2: #{color(red, 900)};
|
||||
// --failure-subtler-1: #{color(red, 800)};
|
||||
// --failure: #{lighten(color(red, 400), 2.5%)};
|
||||
// --failure-starker-1: #{color(red, 300)};
|
||||
//
|
||||
// --success-subtler-2: #{color(green, 900)};
|
||||
// --success-subtler-1: #{color(green, 800)};
|
||||
// --success: #{color(green)};
|
||||
// --success-starker-1: #{color(green, 200)};
|
||||
//
|
||||
// --action-subtler-2: #{color(blue, 900)};
|
||||
// --action-subtler-1h: #{color(blue, 800)};
|
||||
// --action-subtler-1: #{color(blue, 800)};
|
||||
// --action: #{color(blue, 300)};
|
||||
// --action-starker-1: #{color(blue, 200)};
|
||||
//
|
||||
// --ui-border-subtler-1: #{color(ui-gray, 800)};
|
||||
// --ui-border: #{color(ui-gray, 700)};
|
||||
//
|
||||
// --ui-gray-subtler-6: #{color(ui-gray, 900)};
|
||||
// --ui-gray-subtler-5: #{color(ui-gray, 800)};
|
||||
// --ui-gray-subtler-4: #{color(ui-gray, 700)};
|
||||
// --ui-gray-subtler-3: #{color(ui-gray, 600)};
|
||||
// --ui-gray-subtler-2: #{color(ui-gray, 500)};
|
||||
// --ui-gray-subtler-1: #{color(ui-gray, 400)};
|
||||
// --ui-gray: #{color(ui-gray, 300)};
|
||||
// --ui-gray-starker-1: #{color(ui-gray, 200)};
|
||||
// --ui-gray-starker-2: #{color(ui-gray, 100)};
|
||||
// --ui-gray-starker-3: #{color(ui-gray, 050)};
|
||||
// --ui-gray-starker-4: #{color(ui-gray, 010)};
|
||||
//
|
||||
// --ui-header-subtler: #{color(ui-cool-gray, 100)};
|
||||
// }
|
||||
// }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
// poles
|
||||
--subtle: #{darken(color(ui-gray, 900), 3%)};
|
||||
--stark: #{color(ui-gray, 100)};
|
||||
--stark-components: 255, 255, 255;
|
||||
|
||||
// theme colors
|
||||
--info-subtler-3: #{color(blue, 800)};
|
||||
--info-subtler-2: #{color(blue, 700)};
|
||||
--info-subtler-1: #{color(blue, 600)};
|
||||
--info: #{color(blue)};
|
||||
--info-starker-1: #{color(blue, 300)};
|
||||
--info-starker-2: #{color(blue, 200)};
|
||||
|
||||
--alert-subtler-2: #{color(orange, 900)};
|
||||
--alert-subtler-1: #{color(orange, 800)};
|
||||
--alert: #{color(orange)};
|
||||
--alert-starker-1: #{color(orange, 200)};
|
||||
|
||||
--warning-subtler-2: #{color(yellow, 900)};
|
||||
--warning-subtler-1: #{color(yellow, 800)};
|
||||
--warning: #{color(yellow)};
|
||||
--warning-starker-1: #{color(yellow, 200)};
|
||||
|
||||
--failure-subtler-2: #{color(red, 900)};
|
||||
--failure-subtler-1: #{color(red, 800)};
|
||||
--failure: #{color(red, 300)};
|
||||
--failure-starker-1: #{color(red, 200)};
|
||||
|
||||
--success-subtler-2: #{color(green, 900)};
|
||||
--success-subtler-1: #{color(green, 800)};
|
||||
--success: #{color(green, 700)};
|
||||
--success-starker-1: #{color(green, 400)};
|
||||
|
||||
--action-subtler-2: #{darken(color(blue, 900), 10%)};
|
||||
--action-subtler-1h: #{darken(color(blue, 900), 5%)};
|
||||
--action-subtler-1: #{color(blue, 800)};
|
||||
--action: #{color(blue, 400)};
|
||||
--action-starker-1: #{color(blue, 200)};
|
||||
|
||||
--ui-border-subtler-1: #{color(ui-gray, 800)};
|
||||
--ui-border: #{color(ui-gray, 700)};
|
||||
|
||||
--ui-gray-subtler-6: #{color(ui-gray, 900)};
|
||||
--ui-gray-subtler-5: #{color(ui-gray, 800)};
|
||||
--ui-gray-subtler-4: #{color(ui-gray, 700)};
|
||||
--ui-gray-subtler-3: #{color(ui-gray, 600)};
|
||||
--ui-gray-subtler-2: #{color(ui-gray, 500)};
|
||||
--ui-gray-subtler-1: #{color(ui-gray, 400)};
|
||||
--ui-gray: #{color(ui-gray, 300)};
|
||||
--ui-gray-starker-1: #{color(ui-gray, 200)};
|
||||
--ui-gray-starker-2: #{color(ui-gray, 100)};
|
||||
--ui-gray-starker-3: #{color(ui-gray, 050)};
|
||||
--ui-gray-starker-4: #{color(ui-gray, 010)};
|
||||
|
||||
--ui-header-subtler: #{color(ui-cool-gray, 100)};
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
@value={{@model.attributes.login_name}}
|
||||
@label={{t "form.login_name.label"}}
|
||||
@error={{@model.errors.login_name}}
|
||||
@helperText={{t "form.login_name.help"}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
{{#if @model.errors.login_name}}
|
||||
@@ -74,7 +73,6 @@
|
||||
@value={{this.password}}
|
||||
@label={{t "form.password.label"}}
|
||||
@error={{@model.errors.password}}
|
||||
@helperText={{t "form.password.help"}}
|
||||
readonly={{false}}
|
||||
autocomplete="new-password"
|
||||
as |field|>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'resources.host-catalog.title_plural'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'resources.host-catalog.title'}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
@@ -57,4 +57,4 @@
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
@@ -0,0 +1,56 @@
|
||||
{{#if this.hasAvailableAccounts}}
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@error={{@model.errors.base}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "resources.user.actions.add-accounts"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.username.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'resources.auth-method.title'}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each this.filteredAccounts as |account|}}
|
||||
<body.row as |row|>
|
||||
<row.cell>
|
||||
<form.checkbox
|
||||
@label={{account.id}}
|
||||
@description={{account.description}}
|
||||
onChange={{fn this.toggleAccount account}} />
|
||||
</row.cell>
|
||||
<row.cell>{{account.username}}</row.cell>
|
||||
<row.cell>
|
||||
<code>{{account.auth_method_id}}</code>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
</Rose::Form>
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.hasAvailableAccounts}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.user.messages.no-accounts.title"}} as |message|>
|
||||
<message.description>
|
||||
{{t "resources.user.messages.no-accounts.description"}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.users.user.accounts">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { computed, action } from '@ember/object';
|
||||
import { A } from '@ember/array';
|
||||
|
||||
export default class FormUserAddAccountsComponent extends Component {
|
||||
// =properties
|
||||
|
||||
/**
|
||||
* Array of selected account IDs.
|
||||
* @type {EmberArray}
|
||||
*/
|
||||
selectedAccountIDs = A();
|
||||
|
||||
/**
|
||||
* Checks for unassigned accounts.
|
||||
* @param {[AccountModel]} filteredAccounts
|
||||
* @type {boolean}
|
||||
*/
|
||||
@computed('filteredAccounts')
|
||||
get hasAvailableAccounts() {
|
||||
return this.filteredAccounts.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts not already added to the user.
|
||||
* @type {[AccountModel]}
|
||||
*/
|
||||
@computed('args.{accounts.[],model.account_ids.[]}')
|
||||
get filteredAccounts() {
|
||||
// Get IDs for accounts already added to the current user
|
||||
const alreadyAddedAccountIDs = this.args.model.account_ids;
|
||||
const notAddedAccounts = this.args.accounts.filter(
|
||||
({ id }) => !alreadyAddedAccountIDs.includes(id)
|
||||
);
|
||||
return notAddedAccounts;
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
@action
|
||||
toggleAccount(account) {
|
||||
if (!this.selectedAccountIDs.includes(account.id)) {
|
||||
this.selectedAccountIDs.addObject(account.id);
|
||||
} else {
|
||||
this.selectedAccountIDs.removeObject(account.id);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
submit(fn) {
|
||||
fn(this.selectedAccountIDs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeUsersUserAddAccountsController extends Controller {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('resources.user.messages.add-accounts.title');
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,10 @@ Router.map(function () {
|
||||
});
|
||||
|
||||
this.route('users', function () {
|
||||
this.route('user', { path: ':user_id' }, function () {});
|
||||
this.route('user', { path: ':user_id' }, function () {
|
||||
this.route('accounts');
|
||||
this.route('add-accounts');
|
||||
});
|
||||
this.route('new');
|
||||
});
|
||||
|
||||
|
||||
@@ -12,4 +12,27 @@ export default class ScopesScopeUsersUserRoute extends Route {
|
||||
async model({ user_id }) {
|
||||
return this.store.findRecord('user', user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the route-set specific page sections
|
||||
* @override
|
||||
*/
|
||||
renderTemplate() {
|
||||
super.renderTemplate(...arguments);
|
||||
|
||||
this.render('scopes/scope/users/user/-header', {
|
||||
into: 'scopes/scope/users/user',
|
||||
outlet: 'header',
|
||||
});
|
||||
|
||||
this.render('scopes/scope/users/user/-actions', {
|
||||
into: 'scopes/scope/users/user',
|
||||
outlet: 'actions',
|
||||
});
|
||||
|
||||
this.render('scopes/scope/users/user/-navigation', {
|
||||
into: 'scopes/scope/users/user',
|
||||
outlet: 'navigation',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash, all } from 'rsvp';
|
||||
import loading from 'ember-loading/decorator';
|
||||
|
||||
export default class ScopesScopeUsersUserAccountsRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@service notify;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns users associated with this user.
|
||||
* @return {Promise{user: UserModel, accounts: Promise{[AccountModel]}}}
|
||||
*/
|
||||
model() {
|
||||
const { id: scopeID } = this.modelFor('scopes.scope');
|
||||
const user = this.modelFor('scopes.scope.users.user');
|
||||
return hash({
|
||||
user,
|
||||
accounts: all(
|
||||
user.account_ids.map((id) =>
|
||||
this.store.findRecord('account', id, { adapterOptions: { scopeID } })
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Remove a account from the current role and redirect to accounts index.
|
||||
* @param {UserModel} user
|
||||
* @param {AccountModel} account
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
async removeAccount(user, account) {
|
||||
try {
|
||||
await user.removeAccount(account.id);
|
||||
await this.refresh();
|
||||
this.notify.success(this.intl.t('notifications.remove-success'));
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { all } from 'rsvp';
|
||||
import loading from 'ember-loading/decorator';
|
||||
|
||||
export default class ScopesScopeUsersUserAddAccountsRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
@service notify;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Emtpy out any previously loaded acconuts.
|
||||
*/
|
||||
beforeModel() {
|
||||
this.store.unloadAll('account');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current target, all host catalogs, and all host sets.
|
||||
* @return {{target: TargetModel, hostCatalogs: [HostCatalogModel], hostSets: [HostSetModel]}}
|
||||
*/
|
||||
async model() {
|
||||
const user = this.modelFor('scopes.scope.users.user');
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
const authMethods = await this.store.query('auth-method', { scope_id });
|
||||
await all(
|
||||
authMethods.map(({ id: auth_method_id }) =>
|
||||
this.store.query('account', { auth_method_id })
|
||||
)
|
||||
);
|
||||
const accounts = this.store.peekAll('account');
|
||||
return { user, authMethods, accounts };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the route-set specific page sections
|
||||
* @override
|
||||
*/
|
||||
renderTemplate() {
|
||||
super.renderTemplate(...arguments);
|
||||
|
||||
this.render('scopes/scope/users/user/add-accounts/-header', {
|
||||
into: 'scopes/scope/users/user',
|
||||
outlet: 'header',
|
||||
});
|
||||
|
||||
this.render('-empty', {
|
||||
into: 'scopes/scope/users/user',
|
||||
outlet: 'actions',
|
||||
});
|
||||
|
||||
this.render('-empty', {
|
||||
into: 'scopes/scope/users/user',
|
||||
outlet: 'navigation',
|
||||
});
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Adds accounts to the user and saves, replaces with the accounts index
|
||||
* route, and notifies the user of success or error.
|
||||
* @param {UserModel} user
|
||||
* @param {[string]} accountIDs
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
async addAccounts(user, accountIDs) {
|
||||
try {
|
||||
await user.addAccounts(accountIDs);
|
||||
await this.replaceWith('scopes.scope.users.user.accounts');
|
||||
this.notify.success(this.intl.t('notifications.add-success'));
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to user accounts as if nothing ever happened.
|
||||
*/
|
||||
@action
|
||||
cancel() {
|
||||
this.replaceWith('scopes.scope.users.user.accounts');
|
||||
}
|
||||
}
|
||||
@@ -61,19 +61,19 @@
|
||||
.ember-notify-show {
|
||||
-webkit-animation-duration: 500ms;
|
||||
-webkit-animation-name: ember-notify-show;
|
||||
-webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
-webkit-animation-timing-function: cubic-bezier(.175, .885, .32, 1.275);
|
||||
animation-duration: 500ms;
|
||||
animation-name: ember-notify-show;
|
||||
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
animation-timing-function: cubic-bezier(.175, .885, .32, 1.275);
|
||||
}
|
||||
|
||||
.ember-notify-hide {
|
||||
-webkit-animation-duration: 250ms;
|
||||
-webkit-animation-name: ember-notify-hide;
|
||||
-webkit-animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045);
|
||||
-webkit-animation-timing-function: cubic-bezier(.6, -.28, .735, .045);
|
||||
animation-duration: 250ms;
|
||||
animation-name: ember-notify-hide;
|
||||
animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045);
|
||||
animation-timing-function: cubic-bezier(.6, -.28, .735, .045);
|
||||
|
||||
&:hover {
|
||||
-moz-animation-play-state: paused;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
& + .rose-nav-sidebar {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.rose-icon {
|
||||
margin-right: sizing.rems(xs);
|
||||
}
|
||||
}
|
||||
|
||||
.rose-form {
|
||||
@@ -61,17 +65,16 @@
|
||||
|
||||
&.loading {
|
||||
&::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
|
||||
background: repeating-linear-gradient(to right, #fff, #fff, var(--info), #fff, #fff);
|
||||
background-size: 200% auto;
|
||||
background-position: 0 100%;
|
||||
animation: gradient 1s linear infinite;
|
||||
background: repeating-linear-gradient(to right, var(--white), var(--white), var(--info), var(--white), var(--white));
|
||||
background-position: 0 100%;
|
||||
background-size: 200% auto;
|
||||
content: ' ';
|
||||
height: 3px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
|
||||
@keyframes gradient {
|
||||
0% { background-position: 0 0; }
|
||||
@@ -81,6 +84,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.copyable .copyable-button {
|
||||
padding: 0rem;
|
||||
.copyable {
|
||||
.copyable-button {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
<utilities.dropdown
|
||||
@text="user@example.net"
|
||||
@iconOnly={{true}}
|
||||
@icon="user-square-fill" as |dropdown|
|
||||
@icon="boundary-icons/user-profile" as |dropdown|
|
||||
>
|
||||
<dropdown.link @route="account.change-password">
|
||||
{{t 'actions.change-password'}}
|
||||
|
||||
@@ -5,21 +5,36 @@
|
||||
<sidebarLayout.sidebar>
|
||||
<Rose::Nav::Sidebar @title={{t "titles.general"}} as |nav|>
|
||||
<nav.link @route="scopes.scope.projects">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/project"
|
||||
@size="medium" />
|
||||
{{t "resources.project.title_plural"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
|
||||
<Rose::Nav::Sidebar @title={{t "titles.iam"}} as |nav|>
|
||||
<nav.link @route="scopes.scope.users">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/users"
|
||||
@size="medium" />
|
||||
{{t "resources.user.title_plural"}}
|
||||
</nav.link>
|
||||
<nav.link @route="scopes.scope.groups">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/group"
|
||||
@size="medium" />
|
||||
{{t "resources.group.title_plural"}}
|
||||
</nav.link>
|
||||
<nav.link @route="scopes.scope.roles">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/roles"
|
||||
@size="medium" />
|
||||
{{t "resources.role.title_plural"}}
|
||||
</nav.link>
|
||||
<nav.link @route="scopes.scope.auth-methods">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/auth-method"
|
||||
@size="medium" />
|
||||
{{t "resources.auth-method.title_plural"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.login_name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.username.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
<Rose::Nav::Sidebar @title={{t "titles.resources"}} as |nav|>
|
||||
<nav.link @route="scopes.scope.projects.project.sessions">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/action-session"
|
||||
@size="medium" />
|
||||
{{t "resources.session.title_plural"}}
|
||||
</nav.link>
|
||||
<nav.link @route="scopes.scope.projects.project.targets">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/target"
|
||||
@size="medium" />
|
||||
{{t "resources.target.title_plural"}}
|
||||
</nav.link>
|
||||
<nav.link @route="scopes.scope.projects.project.host-catalogs">
|
||||
<Rose::Icon
|
||||
@name="boundary-icons/host-catalogs"
|
||||
@size="medium" />
|
||||
{{t "resources.host-catalog.title_plural"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
@@ -1,3 +1,25 @@
|
||||
{{title @model.displayName}}
|
||||
|
||||
{{outlet}}
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
{{outlet "header"}}
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
{{outlet "navigation"}}
|
||||
</page.navigation>
|
||||
|
||||
<page.actions>
|
||||
{{outlet "actions"}}
|
||||
</page.actions>
|
||||
|
||||
<page.body>
|
||||
{{outlet}}
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -0,0 +1,12 @@
|
||||
<Rose::Dropdown @text="{{t 'actions.manage'}}" @dropdownRight={{true}} as |dropdown|>
|
||||
<dropdown.link @route="scopes.scope.users.user.add-accounts">
|
||||
{{t "resources.user.actions.add-accounts"}}
|
||||
</dropdown.link>
|
||||
<dropdown.button
|
||||
@style="danger"
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "delete" @model)}}
|
||||
>
|
||||
{{t 'actions.delete-user'}}
|
||||
</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
@@ -0,0 +1,3 @@
|
||||
<h2>{{t "resources.user.title"}}</h2>
|
||||
<p>{{t "resources.user.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
@@ -0,0 +1,8 @@
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
<nav.link @route='scopes.scope.users.user.index'>
|
||||
{{t 'titles.details'}}
|
||||
</nav.link>
|
||||
<nav.link @route='scopes.scope.users.user.accounts'>
|
||||
{{t 'resources.user.messages.accounts.title'}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Tabs>
|
||||
@@ -0,0 +1,53 @@
|
||||
{{title (t 'resources.user.messages.accounts.title')}}
|
||||
|
||||
{{#if @model.accounts}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'form.username.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t "titles.actions"}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model.accounts as |account|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
{{account.username}}
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<code>{{account.id}}</code>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
@icon="more-horizontal"
|
||||
@iconOnly={{true}}
|
||||
@showCaret={{false}}
|
||||
@dropdownRight={{true}}
|
||||
@text="{{t "actions.manage"}}" as |dropdown|
|
||||
>
|
||||
<dropdown.button
|
||||
@style="danger"
|
||||
{{on "click" (route-action "removeAccount" @model.user account)}}
|
||||
>
|
||||
{{t "actions.remove"}}
|
||||
</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
{{else}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.user.messages.accounts.title"}} as |message|>
|
||||
<message.description>
|
||||
{{t "resources.user.messages.accounts.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
|
||||
{{/if}}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{title (t 'resources.user.actions.add-accounts')}}
|
||||
|
||||
{{#if @model.accounts}}
|
||||
<Form::User::AddAccounts
|
||||
@model={{@model.user}}
|
||||
@authMethods={{@model.authMethods}}
|
||||
@accounts={{@model.accounts}}
|
||||
@submit={{route-action "addAccounts" @model.user}}
|
||||
@cancel={{route-action "cancel"}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @model.accounts}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.user.messages.no-accounts.title"}} as |message|>
|
||||
<message.description>
|
||||
{{t "resources.user.messages.no-accounts.description"}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.users.user.accounts">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
@@ -0,0 +1,2 @@
|
||||
<h2>{{t "resources.user.messages.add-accounts.title"}}</h2>
|
||||
<p>{{t "resources.user.messages.add-accounts.description"}}</p>
|
||||
@@ -1,41 +1,5 @@
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "resources.user.title"}}</h2>
|
||||
<p>{{t "resources.user.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
</page.header>
|
||||
|
||||
<page.actions>
|
||||
<Rose::Dropdown @text="{{t 'actions.manage'}}" @dropdownRight={{true}} as |dropdown|>
|
||||
<dropdown.button
|
||||
@style="danger"
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "delete" @model)}}
|
||||
>
|
||||
{{t 'actions.delete-user'}}
|
||||
</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
</page.actions>
|
||||
|
||||
<page.navigation>
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
<nav.link @route='scopes.scope.users.user'>
|
||||
{{t 'titles.details'}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Tabs>
|
||||
</page.navigation>
|
||||
|
||||
<page.body>
|
||||
<Form::User
|
||||
@model={{@model}}
|
||||
@submit={{route-action "save" @model}}
|
||||
@cancel={{route-action "cancel" @model}}
|
||||
/>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
<Form::User
|
||||
@model={{@model}}
|
||||
@submit={{route-action "save" @model}}
|
||||
@cancel={{route-action "cancel" @model}}
|
||||
/>
|
||||
@@ -90,6 +90,31 @@ export default function() {
|
||||
this.get('/users/:id');
|
||||
this.patch('/users/:id');
|
||||
this.del('/users/:id');
|
||||
this.post('/users/:idMethod', function ({ users }, { params: { idMethod } }) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
const id = idMethod.split(':')[0];
|
||||
const method = idMethod.split(':')[1];
|
||||
const user = users.find(id);
|
||||
const updatedAttrs = {
|
||||
version: attrs.version,
|
||||
accountIds: user.accountIds
|
||||
};
|
||||
// If adding accounts, push them into the array
|
||||
if (method === 'add-accounts') {
|
||||
attrs.accountIds.forEach(id => {
|
||||
if (!updatedAttrs.accountIds.includes(id)) {
|
||||
updatedAttrs.accountIds.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
// If deleting accounts, filter them out of the array
|
||||
if (method === 'remove-accounts') {
|
||||
updatedAttrs.accountIds = updatedAttrs.accountIds.filter(id => {
|
||||
return !attrs.accountIds.includes(id);
|
||||
});
|
||||
}
|
||||
return user.update(updatedAttrs);
|
||||
});
|
||||
|
||||
// IAM: Groups
|
||||
this.get('/groups', ({ groups }, { queryParams: { scope_id: scopeId } }) => {
|
||||
|
||||
@@ -5,12 +5,15 @@ export default factory.extend({
|
||||
id: (i) => `auth-method-id-${i}`,
|
||||
|
||||
/**
|
||||
* Adds accounts to auth method.
|
||||
* Adds accounts (with associated users) to auth method.
|
||||
*/
|
||||
withAccounts: trait({
|
||||
withAccountsAndUsers: trait({
|
||||
afterCreate(authMethod, server) {
|
||||
const { scope } = authMethod;
|
||||
server.createList('account', 5, { scope, authMethod });
|
||||
server.createList('user', 5, { scope }).map(user => {
|
||||
const { id } = server.create('account', { scope, authMethod });
|
||||
user.update({ accountIds: [id] });
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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(),
|
||||
accounts: hasMany('account')
|
||||
|
||||
});
|
||||
@@ -15,7 +15,7 @@ export default function(server) {
|
||||
|
||||
// Auth
|
||||
server.createList('auth-method', 3, { scope: globalScope });
|
||||
server.createList('auth-method', 3, { scope: orgScope }, 'withAccounts');
|
||||
server.createList('auth-method', 3, { scope: orgScope }, 'withAccountsAndUsers');
|
||||
|
||||
// Groups and Users
|
||||
server.createList('group', 5, { scope: orgScope }, 'withMembers');
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import ApplicationSerializer from './application';
|
||||
import { underscore } from '@ember/string';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'user'
|
||||
modelName: 'user',
|
||||
|
||||
keyForRelationshipIds(relationshipName) {
|
||||
return `${this._container.inflector.singularize(
|
||||
underscore(relationshipName)
|
||||
)}_ids`;
|
||||
},
|
||||
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments);
|
||||
if (model.accountIds.length) json.account_ids = model.accountIds;
|
||||
return json;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.78339 14.5C11.3732 14.5 14.2834 11.5899 14.2834 8C14.2834 4.41015 11.3732 1.5 7.78339 1.5C5.61088 1.5 3.6873 2.56583 2.50703 4.20312H0.740068C2.09209 1.70035 4.73917 0 7.78339 0C12.2017 0 15.7834 3.58172 15.7834 8C15.7834 12.4183 12.2017 16 7.78339 16C4.66999 16 1.97198 14.2215 0.649902 11.625H2.38728C3.55457 13.3592 5.53576 14.5 7.78339 14.5Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.32991 7.20313L7.03211 4.90533L8.09277 3.84467L12.2481 7.99999L8.09277 12.1553L7.03211 11.0947L9.42364 8.70313H0V7.20313H9.32991Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 728 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4211 5.60361H16V7.10361H10.4211V5.60361ZM10.4211 8.76566H16V10.2657H10.4211V8.76566ZM16 11.9277H10.4211V13.4277H16V11.9277Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.72412 5.35944V3.73138C1.72412 2.35067 2.84341 1.23138 4.22412 1.23138H4.77561C6.15632 1.23138 7.27561 2.35067 7.27561 3.73138V5.35944H9V13.6719H0V5.35944H1.72412ZM5.76994 3.62429C5.77369 3.65948 5.77561 3.69521 5.77561 3.73138V5.12429H3.22412V3.73138C3.22412 3.69521 3.22604 3.65948 3.22979 3.62429C3.28323 3.12236 3.70801 2.73138 4.22412 2.73138H4.77561C5.29172 2.73138 5.71651 3.12236 5.76994 3.62429ZM7.5 6.85944V12.1719H1.5V6.85944H7.5ZM3.74976 8.44012V10.5912H5.24976V8.44012H3.74976Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 868 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.94246 0H10.0691L10.4813 2.74643C10.706 2.86098 10.9213 2.98933 11.1279 3.1302L13.7956 2.20814L15.8584 5.8437L13.3258 7.67296V8.32704L15.8584 10.1563L13.784 13.8124L11.2211 12.8199C11.0167 12.9579 10.8018 13.0867 10.5757 13.2021L10.0442 16H5.94469L5.46307 13.1994C5.23939 13.0859 5.02508 12.9588 4.81941 12.8192L2.21391 13.8087L0.141602 10.1563L2.67417 8.32704V7.67296L0.141602 5.8437L2.22032 2.18L4.8305 3.23078C5.03053 3.09607 5.24058 2.97008 5.46135 2.85692L5.94246 0ZM7.21098 1.5L6.80637 3.90261L6.42448 4.06206C6.06265 4.21314 5.71961 4.42319 5.3882 4.67642L5.04889 4.93568L2.87741 4.06151L2.10958 5.4148L4.17417 6.90604V9.09396L2.10958 10.5852L2.88381 11.9498L5.06229 11.1225L5.39546 11.3836C5.71672 11.6353 6.05737 11.839 6.42448 11.9923L6.8048 12.1511L7.20875 14.5H8.80233L9.24919 12.1478L9.6217 11.9923C9.98353 11.8412 10.3266 11.6312 10.658 11.378L10.9921 11.1227L13.1183 11.9461L13.8904 10.5852L11.8258 9.09396V6.90604L13.8904 5.4148L13.1066 4.03336L10.8611 4.80949L10.5395 4.55751C10.2183 4.30575 9.87761 4.10206 9.51051 3.94878L9.12051 3.78595L8.77743 1.5H7.21098Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.65356 8.00001C6.65356 8.79715 7.28506 9.40001 8.00011 9.40001C8.71516 9.40001 9.34665 8.79715 9.34665 8.00001C9.34665 7.20286 8.71516 6.60001 8.00011 6.60001C7.28506 6.60001 6.65356 7.20286 6.65356 8.00001ZM5.15356 8.00001C5.15356 9.59915 6.43044 10.9 8.00011 10.9C9.56977 10.9 10.8467 9.59915 10.8467 8.00001C10.8467 6.40086 9.56977 5.10001 8.00011 5.10001C6.43044 5.10001 5.15356 6.40086 5.15356 8.00001Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.04165 3.95312L9.04294 3.95515H9.86719H13.5V6H15V3.95515V2.45515H13.5H9.86719L8.30774 0H7.05H0V1.5V1.83333V13.5V15H1.5H3.71899V13.5H3.69834H3.66922H3.6401H3.61095H3.5818H3.55264H3.52346H3.49427H3.46506H3.43585H3.40662H3.37738H3.34813H3.31886H3.28958H3.26029H3.23098H3.20166H3.17233H3.14298H3.11362H3.08425H3.05486H3.02546H2.99605H2.96662H2.93717H2.90771H2.87824H2.84876H2.81926H2.78974H2.76021H2.73067H2.70111H2.67153H2.64194H2.61234H2.58272H2.55308H2.52343H2.49377H2.46408H2.43439H2.40467H2.37495H2.3452H2.31544H2.28566H2.25587H2.22606H2.19624H2.16639H2.13654H2.10666H2.07677H2.04686H2.01693H1.98699H1.95703H1.92705H1.89706H1.86705H1.83702H1.80697H1.77691H1.74683H1.71673H1.68661H1.65648H1.62632H1.59615H1.56596H1.53575H1.50553H1.5V3.95312H9.04165ZM8.08889 2.45312H1.5V1.83333V1.82742V1.82151V1.8156V1.8097V1.80381V1.79791V1.79202V1.78614V1.78026V1.77439V1.76852V1.76265V1.75679V1.75093V1.74508V1.73924V1.73339V1.72756V1.72172V1.7159V1.71007V1.70425V1.69844V1.69263V1.68683V1.68103V1.67524V1.66945V1.66367V1.65789V1.65211V1.64635V1.64058V1.63483V1.62907V1.62333V1.61758V1.61185V1.60612V1.60039V1.59467V1.58895V1.58325V1.57754V1.57184V1.56615V1.56046V1.55478V1.5491V1.54343V1.53777V1.53211V1.52646V1.52081V1.51517V1.50953V1.5039V1.5H7.05H7.051H7.05203H7.05307H7.05413H7.05522H7.05633H7.05745H7.0586H7.05977H7.06095H7.06216H7.06339H7.06464H7.0659H7.06719H7.0685H7.06982H7.07117H7.07254H7.07392H7.07533H7.07675H7.07819H7.07965H7.08113H7.08263H7.08415H7.08569H7.08724H7.08881H7.0904H7.09201H7.09364H7.09529H7.09695H7.09864H7.10033H7.10205H7.10379H7.10554H7.10731H7.1091H7.1109H7.11272H7.11456H7.11642H7.11829H7.12018H7.12209H7.12401H7.12595H7.12791H7.12988H7.13187H7.13387H7.1359H7.13793H7.13999H7.14205H7.14414H7.14624H7.14836H7.15049H7.15263H7.1548H7.15697H7.15916H7.16137H7.16359H7.16583H7.16808H7.17035H7.17263H7.17493H7.17724H7.17956H7.1819H7.18425H7.18662H7.189H7.19139H7.1938H7.19623H7.19866H7.20111H7.20357H7.20605H7.20854H7.21104H7.21355H7.21608H7.21862H7.22118H7.22374H7.22632H7.22892H7.23152H7.23414H7.23676H7.23941H7.24206H7.24472H7.2474H7.25009H7.25279H7.2555H7.25822H7.26096H7.2637H7.26646H7.26923H7.27201H7.2748H7.2776H7.28042H7.28324H7.28607H7.28892H7.29177H7.29464H7.29751H7.3004H7.30329H7.3062H7.30911H7.31204H7.31497H7.31792H7.32087H7.32384H7.32681H7.32979H7.33278H7.33578H7.33879H7.34181H7.34484H7.34788H7.35092H7.35398H7.35704H7.36011H7.36319H7.36627H7.36937H7.37247H7.37558H7.3787H7.38183H7.38496H7.3881H7.39125H7.39441H7.39757H7.40074H7.40392H7.40711H7.4103H7.4135H7.4167H7.41992H7.42314H7.42636H7.42959H7.43283H7.43608H7.43933H7.44258H7.44585H7.44912H7.45239H7.45567H7.45896H7.46225H7.46554H7.46885H7.47215H7.47546H7.47878H7.4821H7.48349L8.08889 2.45312ZM7.26562 8.75684C7.26562 7.38438 8.37754 6.27246 9.75 6.27246C11.1225 6.27246 12.2344 7.38438 12.2344 8.75684C12.2344 10.1293 11.1225 11.2412 9.75 11.2412C8.37754 11.2412 7.26562 10.1293 7.26562 8.75684ZM9.75 7.77246C9.20597 7.77246 8.76562 8.21281 8.76562 8.75684C8.76562 9.30087 9.20597 9.74121 9.75 9.74121C10.294 9.74121 10.7344 9.30087 10.7344 8.75684C10.7344 8.21281 10.294 7.77246 9.75 7.77246ZM14 16V12.125H5.5V16H7V13.625H12.5V16H14Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 1.5H14.4375V10.5625H15.9375V0H6.5V1.5ZM11.5938 3.9375H3.65625V2.4375H13.0938V13.875H11.5938V3.9375ZM8.125 6.5625H1.5V14.5H8.125V6.5625ZM0 5.0625V16H9.625V5.0625H0ZM6.5 9.1875H2.75V7.6875H6.5V9.1875ZM4.0625 13.375H5.5625V11.875H4.0625V13.375Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.15625 12.1875H5V16H0V0H12V5.125H15.5V16H12H10.5H7.15625V12.1875ZM10.5 14.5H8.65625V10.6875H3.5V14.5H1.5V1.5H10.5V5.125V14.5ZM12 14.5H14V6.625H12V14.5ZM3 3H4.88235V5H3V3ZM4.88235 7H3V9H4.88235V7ZM7 3H8.88235V5H7V3ZM8.88235 7H7V9H8.88235V7Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 417 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 1.5H5.50461V5.63615H1.5V1.5ZM0 7.13615V0H7.00461V7.13615H0ZM1.5 10.3639H5.50461V14.5H1.5V10.3639ZM0 16V8.86386H7.00461V16H0ZM14.5 1.5H10.4954V5.63615H14.5V1.5ZM8.99536 0V7.13615H16V0H8.99536ZM10.4954 10.3639H14.5V14.5H10.4954V10.3639ZM8.99536 16V8.86386H16V16H8.99536Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 2.2641H1.5V13.6315H2.6875V10.3063H9.1875V13.6315H14.5V2.2641ZM7.6875 13.6315V11.8063H4.1875V13.6315H7.6875ZM0 0.764099V15.1315H16V0.764099H0ZM3.1875 6.98122C3.1875 5.45447 4.40519 4.19366 5.9375 4.19366C7.46981 4.19366 8.6875 5.45447 8.6875 6.98122C8.6875 8.50797 7.46981 9.76878 5.9375 9.76878C4.40519 9.76878 3.1875 8.50797 3.1875 6.98122ZM5.9375 5.69366C5.25981 5.69366 4.6875 6.25647 4.6875 6.98122C4.6875 7.70598 5.25981 8.26878 5.9375 8.26878C6.61519 8.26878 7.1875 7.70598 7.1875 6.98122C7.1875 6.25647 6.61519 5.69366 5.9375 5.69366ZM10.025 5.33713L13.625 5.33713V6.83713L10.025 6.83713L10.025 5.33713ZM13.625 8.62527H11.125V10.1253H13.625V8.62527Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 838 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.25 6V3.94293C5.57242 4.25072 4.25072 5.57241 3.94293 7.25H6V8.75H3.94293C4.25072 10.4276 5.57241 11.7493 7.25 12.0571V10H8.75V12.0571C10.4276 11.7493 11.7493 10.4276 12.0571 8.75H10V7.25H12.0571C11.7493 5.57241 10.4276 4.25072 8.75 3.94293V6H7.25ZM13.5755 7.25C13.2417 4.74255 11.2574 2.75829 8.75 2.42452V0H7.25V2.42452C4.74255 2.75829 2.75829 4.74255 2.42452 7.25H0V8.75H2.42452C2.75829 11.2575 4.74255 13.2417 7.25 13.5755V16H8.75V13.5755C11.2575 13.2417 13.2417 11.2575 13.5755 8.75H16V7.25H13.5755Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 1.5H1.5V14.5H4V10H12V14.5H14.5V1.5ZM16 16H0V0H16V16ZM10.25 5.75C10.25 6.995 9.245 8 8 8C6.755 8 5.75 6.995 5.75 5.75C5.75 4.505 6.755 3.5 8 3.5C9.245 3.5 10.25 4.505 10.25 5.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.875 0C1.50254 0 0.390625 1.11192 0.390625 2.48438C0.390625 3.85683 1.50254 4.96875 2.875 4.96875C4.24746 4.96875 5.35938 3.85683 5.35938 2.48438C5.35938 1.11192 4.24746 0 2.875 0ZM1.89062 2.48438C1.89062 1.94035 2.33097 1.5 2.875 1.5C3.41903 1.5 3.85938 1.94035 3.85938 2.48438C3.85938 3.0284 3.41903 3.46875 2.875 3.46875C2.33097 3.46875 1.89062 3.0284 1.89062 2.48438ZM9.69536 2.48022C8.14523 2.48022 6.8894 3.73605 6.8894 5.28618C6.8894 6.83631 8.14523 8.09214 9.69536 8.09214C11.2455 8.09214 12.5013 6.83631 12.5013 5.28618C12.5013 3.73605 11.2455 2.48022 9.69536 2.48022ZM8.3894 5.28618C8.3894 4.56448 8.97366 3.98022 9.69536 3.98022C10.4171 3.98022 11.0013 4.56448 11.0013 5.28618C11.0013 6.00788 10.4171 6.59214 9.69536 6.59214C8.97366 6.59214 8.3894 6.00788 8.3894 5.28618ZM4.5 10.2872H14.8906V16H13.3906V11.7872H6V16H4.5V10.2872ZM5.26562 6.59213H0V11.79H1.5V8.09213H5.26562V6.59213Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,142 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, find, findAll } 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 { Response } from 'miragejs';
|
||||
import {
|
||||
authenticateSession,
|
||||
// These are left here intentionally for future reference.
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
|
||||
module('Acceptance | users | accounts', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
},
|
||||
user: null,
|
||||
};
|
||||
const urls = {
|
||||
orgScope: null,
|
||||
users: null,
|
||||
user: null,
|
||||
accounts: null,
|
||||
addAccounts: null,
|
||||
};
|
||||
let accountsCount;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
authenticateSession({});
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
this.server.create('auth-method', {
|
||||
scope: instances.scopes.org
|
||||
}, 'withAccountsAndUsers');
|
||||
instances.user = this.server.schema.users.all().models[0];
|
||||
accountsCount = instances.user.accountIds.length;
|
||||
urls.users = `/scopes/${instances.scopes.org.id}/users`;
|
||||
urls.user = `${urls.users}/${instances.user.id}`;
|
||||
urls.accounts = `${urls.user}/accounts`;
|
||||
urls.addAccounts = `${urls.user}/add-accounts`;
|
||||
});
|
||||
|
||||
test('visiting user accounts', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.accounts);
|
||||
await a11yAudit();
|
||||
assert.equal(currentURL(), urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, accountsCount);
|
||||
});
|
||||
|
||||
test('can remove a account', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, accountsCount);
|
||||
await click('tbody tr .rose-dropdown-button-danger');
|
||||
assert.equal(findAll('tbody tr').length, accountsCount - 1);
|
||||
});
|
||||
|
||||
test('shows error message on account remove', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.post('/users/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, accountsCount);
|
||||
await click('tbody tr .rose-dropdown-button-danger');
|
||||
assert.ok(find('[role="alert"]'));
|
||||
});
|
||||
|
||||
test('visiting account add accounts', async function(assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.addAccounts);
|
||||
await a11yAudit();
|
||||
assert.equal(currentURL(), urls.addAccounts);
|
||||
});
|
||||
|
||||
test('select and save accounts to add', async function (assert) {
|
||||
assert.expect(3);
|
||||
instances.user.update({ accountIds: [] });
|
||||
await visit(urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, 0);
|
||||
await click('.rose-layout-page-actions a')
|
||||
assert.equal(currentURL(), urls.addAccounts);
|
||||
await click('tbody label');
|
||||
await click('form [type="submit"]');
|
||||
await visit(urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, 1);
|
||||
});
|
||||
|
||||
test('select and cancel accounts to add', async function (assert) {
|
||||
assert.expect(4);
|
||||
await visit(urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, accountsCount);
|
||||
await click('tbody tr .rose-dropdown-button-danger');
|
||||
assert.equal(findAll('tbody tr').length, accountsCount - 1);
|
||||
await click('.rose-layout-page-actions a')
|
||||
assert.equal(currentURL(), urls.addAccounts);
|
||||
await click('tbody label');
|
||||
await click('form [type="button"]');
|
||||
await visit(urls.accounts);
|
||||
assert.equal(findAll('tbody tr').length, accountsCount - 1);
|
||||
});
|
||||
|
||||
test('shows error message on account add', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/users/:idMethod', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {},
|
||||
}
|
||||
);
|
||||
});
|
||||
instances.user.update({ accountIds: [] });
|
||||
await visit(urls.addAccounts);
|
||||
await click('tbody label');
|
||||
await click('form [type="submit"]');
|
||||
assert.ok(find('[role="alert"]'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
// import { render } from '@ember/test-helpers';
|
||||
// import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | form/user/add-accounts', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
assert.expect(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/users/user/add-accounts', 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');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/users/user/accounts', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/users/user/accounts');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/users/user/add-accounts', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/users/user/add-accounts');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -10,14 +10,14 @@ type:
|
||||
label: Type
|
||||
help: The type of the resource, to help differentiate schemas
|
||||
login_name:
|
||||
label: Login Name
|
||||
username:
|
||||
label: Username
|
||||
help: The username for the account
|
||||
set_password:
|
||||
label: Password
|
||||
help: Create a new password for the account
|
||||
help: Create a new password
|
||||
password:
|
||||
label: Password
|
||||
help: The password for the account
|
||||
grant:
|
||||
label: Grant
|
||||
help: A permissions grant string for a role
|
||||
|
||||
@@ -122,6 +122,17 @@ user:
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Users
|
||||
accounts:
|
||||
title: Accounts
|
||||
description: Users may be associated with any number of accounts.
|
||||
add-accounts:
|
||||
title: Add Accounts
|
||||
description: Select accounts to associate with this user.
|
||||
no-accounts:
|
||||
title: No Accounts Available
|
||||
description: There are no accounts available to add to this user.
|
||||
actions:
|
||||
add-accounts: Add Accounts
|
||||
role:
|
||||
title: Role
|
||||
title_plural: Roles
|
||||
@@ -180,4 +191,4 @@ target:
|
||||
description: No hosts sets available to add to target.
|
||||
add:
|
||||
title: Add Host Sets
|
||||
description: Select host sets to assign to this target.
|
||||
description: Select host sets to assign to this target.
|
||||