chore: 🤖 manual merge from develop
This commit is contained in:
99 files changed
+1541
-130
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: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<LinkTo
|
||||
...attributes
|
||||
class="rose-footer-brand"
|
||||
title={{@text}}
|
||||
@route={{if @route @route "index"}}
|
||||
>
|
||||
|
||||
{{#if @logo}}
|
||||
{{inline-svg @logo}}
|
||||
{{/if}}
|
||||
|
||||
</LinkTo>
|
||||
@@ -0,0 +1,11 @@
|
||||
<span class="rose-footer-copyright">
|
||||
<span class="rose-footer-copyright-symbol">
|
||||
©
|
||||
</span>
|
||||
<span class="rose-footer-copyright-year">
|
||||
{{this.currentYear}}
|
||||
</span>
|
||||
<span class="rose-footer-copyright-text">
|
||||
{{@text}}
|
||||
</span>
|
||||
</span>
|
||||
@@ -0,0 +1,8 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class RoseFooterCopyrightComponent extends Component {
|
||||
|
||||
// =attributes
|
||||
currentYear = new Date().getFullYear();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<footer
|
||||
...attributes
|
||||
class="rose-footer"
|
||||
>
|
||||
{{yield (hash
|
||||
nav=(component 'rose/footer/nav')
|
||||
brand=(component 'rose/footer/brand')
|
||||
text=(component "rose/anonymous" class="rose-footer-text")
|
||||
)}}
|
||||
</footer>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
|
||||
<Meta title="Rose|Footer" component="Footer" />
|
||||
|
||||
# Footer
|
||||
|
||||
A footer component that allows four distinct sections to be configured: brand, nav, and text.
|
||||
|
||||
```html
|
||||
<Rose::Footer as |footer|>
|
||||
<footer.brand @logo="logo" @text="Product Name" />
|
||||
<footer.text>Text</footer.text>
|
||||
<footer.nav as |nav|>
|
||||
<nav.link @route="index">Section</nav.link>
|
||||
<nav.link @href="index">Section</nav.link>
|
||||
</footer.nav>
|
||||
</Rose::Footer>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Story name="Basic">{{
|
||||
template: hbs`
|
||||
<Rose::Footer as |footer|>
|
||||
<footer.brand @logo="logo" @text={{brandName}} />
|
||||
<footer.text>{{footerText}}</footer.text>
|
||||
<footer.nav as |nav|>
|
||||
<nav.link @route="index">Section</nav.link>
|
||||
<nav.link @href="index">Section</nav.link>
|
||||
</footer.nav>
|
||||
</Rose::Footer>
|
||||
`,
|
||||
context: {
|
||||
brandName: text('brandName', 'Brand Name'),
|
||||
footerText: text('footerText', 'Text'),
|
||||
},
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -0,0 +1,5 @@
|
||||
<nav class="rose-footer-nav" aria-label="footer-nav">
|
||||
{{yield (hash
|
||||
link=(component 'rose/footer/nav/link')
|
||||
)}}
|
||||
</nav>
|
||||
@@ -0,0 +1,20 @@
|
||||
{{#if @href}}
|
||||
<a target="_blank"
|
||||
href={{@href}}
|
||||
class="rose-footer-nav-link"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{yield}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if @route}}
|
||||
<LinkTo
|
||||
...attributes
|
||||
@route={{@route}}
|
||||
@model={{@model}}
|
||||
class="rose-footer-nav-link"
|
||||
>
|
||||
{{yield}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
@@ -0,0 +1,11 @@
|
||||
<span class="rose-footer-product">
|
||||
<span class="rose-footer-product-text">
|
||||
{{@text}}
|
||||
</span>
|
||||
|
||||
{{#if @version}}
|
||||
<span class="rose-footer-product-version">
|
||||
v{{@version}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
@@ -2,5 +2,6 @@
|
||||
{{yield (hash
|
||||
header=(component "rose/anonymous" tagName="header" class="rose-layout-global-header")
|
||||
body=(component "rose/anonymous" class="rose-layout-global-body")
|
||||
footer=(component "rose/anonymous" class="rose-layout-global-footer")
|
||||
)}}
|
||||
</section>
|
||||
@@ -1,29 +1,29 @@
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { text, boolean, select } from '@storybook/addon-knobs';
|
||||
|
||||
<Meta title="Rose|Layout|Global" component="Global" />
|
||||
|
||||
# Layout|Global
|
||||
|
||||
A top-level layout/positioning component that allows two distinct sections to
|
||||
be configured: header and body.
|
||||
A top-level layout/positioning component that allows three distinct sections to
|
||||
be configured: header, body, and footer.
|
||||
|
||||
```htmlbars
|
||||
```html
|
||||
<Rose::Layout::Global as |layout|>
|
||||
<layout.header />
|
||||
<layout.body />
|
||||
<layout.footer />
|
||||
</Rose::Layout::Global>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Story name="Global">{{
|
||||
template: hbs`
|
||||
<Rose::Layout::Global as |layout|>
|
||||
<layout.header style="background: var(--brand-gray-5);">Header</layout.header>
|
||||
<layout.body style="background: var(--brand-gray-7);">Body</layout.body>
|
||||
</Rose::Layout::Global>
|
||||
<Rose::Layout::Global as |layout|>
|
||||
<layout.header style="background: var(--brand-gray-5);">Header</layout.header>
|
||||
<layout.body style="background: var(--brand-gray-7);">Body</layout.body>
|
||||
<layout.footer style="background: var(--brand-gray-5);">Footer</layout.footer>
|
||||
</Rose::Layout::Global>
|
||||
`,
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -2,6 +2,7 @@
|
||||
@import 'dropdown';
|
||||
@import 'form';
|
||||
@import 'header';
|
||||
@import 'footer';
|
||||
@import 'layout';
|
||||
@import 'icon';
|
||||
@import 'list';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
@use '../../variables/sizing';
|
||||
|
||||
.rose-footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ui-gray-starker-1);
|
||||
margin: sizing.rems(xs);
|
||||
|
||||
svg {
|
||||
$logoSize: sizing.rems(m);
|
||||
display: inline-block;
|
||||
width: $logoSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../variables/media';
|
||||
@use '../../utilities/type';
|
||||
|
||||
@import 'brand';
|
||||
@import 'nav';
|
||||
@import 'text';
|
||||
@import 'link';
|
||||
|
||||
.rose-footer {
|
||||
@include type.type(xs);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--ui-gray-starker-1);
|
||||
height: sizing.rems(xxl) + sizing.rems(xxs);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@use '../../variables/sizing';
|
||||
|
||||
.rose-footer-nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: underline;
|
||||
color: var(--ui-gray-starker-1);
|
||||
margin: 0 sizing.rems(xs);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@use '../../variables/sizing';
|
||||
|
||||
.rose-footer-nav {
|
||||
grid-area: nav;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-right: sizing.rems(s);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@use '../../variables/sizing';
|
||||
|
||||
.rose-footer-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 sizing.rems(xs);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/footer';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/footer/brand';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/footer/copyright';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/footer/nav';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/footer/nav/link';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/footer/product';
|
||||
@@ -0,0 +1,42 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/footer', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(4);
|
||||
await render(hbs`<Rose::Footer />`);
|
||||
assert.ok(find('.rose-footer'));
|
||||
assert.notOk(find('.rose-footer-nav'));
|
||||
assert.notOk(find('.rose-footer-text'));
|
||||
assert.notOk(find('.rose-footer-brand'));
|
||||
});
|
||||
|
||||
test('it renders with brand', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer as |footer| >
|
||||
<footer.brand />
|
||||
</Rose::Footer>`);
|
||||
assert.ok(find('.rose-footer-brand'));
|
||||
});
|
||||
|
||||
test('it renders with nav', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer as |footer| >
|
||||
<footer.nav />
|
||||
</Rose::Footer>`);
|
||||
assert.ok(find('.rose-footer-nav'));
|
||||
});
|
||||
|
||||
test('it renders with text', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer as |footer| >
|
||||
<footer.text />
|
||||
</Rose::Footer>`);
|
||||
assert.ok(find('.rose-footer-text'));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/footer/nav', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer::nav />`);
|
||||
assert.ok(find('.rose-footer-nav'));
|
||||
});
|
||||
|
||||
test('it renders nav external link elements', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer::nav as |nav| >
|
||||
<nav.link @href="index"/>
|
||||
<nav.link @route="about"/>
|
||||
</Rose::Footer::nav>`);
|
||||
assert.equal(findAll('a').length, 2);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/footer/nav/link', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders with @href', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Footer::Nav::Link @href="localhost" />`);
|
||||
assert.ok(find('a'));
|
||||
assert.ok(find('.rose-footer-nav-link'));
|
||||
});
|
||||
|
||||
test('it renders with @route', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(hbs`<Rose::Footer::Nav::Link @route="index" />`);
|
||||
assert.ok(find('a'));
|
||||
assert.ok(find('.rose-footer-nav-link'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/footer/text', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer as |footer|>
|
||||
<footer.text />
|
||||
</Rose::Footer>`);
|
||||
assert.ok(find('.rose-footer-text'));
|
||||
});
|
||||
|
||||
test('it renders content', async function (assert) {
|
||||
assert.expect(1);
|
||||
await render(hbs`<Rose::Footer as |footer|>
|
||||
<footer.text>content</footer.text>
|
||||
</Rose::Footer>`);
|
||||
assert.equal(find('.rose-footer-text').textContent.trim(), 'content');
|
||||
});
|
||||
});
|
||||
@@ -30,4 +30,11 @@ module('Integration | Component | rose/layout/global', function (hooks) {
|
||||
</Rose::Layout::Global>`);
|
||||
assert.ok(find('.rose-layout-global-body'));
|
||||
});
|
||||
|
||||
test('it renders with footer', async function (assert) {
|
||||
await render(hbs`<Rose::Layout::Global as |layout|>
|
||||
<layout.footer />
|
||||
</Rose::Layout::Global>`);
|
||||
assert.ok(find('.rose-layout-global-footer'));
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
{{#if this.isClipboardLibrarySupported}}
|
||||
<span
|
||||
id={{this.id}}
|
||||
aria-labelledby={{this.copyableButtonId}}
|
||||
class="copyable"
|
||||
{{did-insert this.register}}
|
||||
{{will-destroy this.destroy}}
|
||||
@@ -12,10 +11,12 @@
|
||||
|
||||
<Rose::Button
|
||||
id={{this.copyableButtonId}}
|
||||
aria-describedby={{this.id}}
|
||||
class="copyable-button"
|
||||
@style="ghost"
|
||||
@iconOnly={{this.icon}}
|
||||
>{{@text}}</Rose::Button>
|
||||
title={{this.copyableButtonText}}
|
||||
>{{this.copyableButtonText}}</Rose::Button>
|
||||
</span>
|
||||
{{else}}
|
||||
{{yield}}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Component from '@glimmer/component';
|
||||
import ClipboardJS from 'clipboard';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { generateComponentID } from 'rose/utilities/component-auto-id';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import { set } from '@ember/object';
|
||||
@@ -12,11 +11,9 @@ export default class CopyableComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
id = generateComponentID();
|
||||
copyableButtonId = `copyable-button-${this.id}`;
|
||||
icon = 'copy-action';
|
||||
|
||||
@tracked text;
|
||||
@tracked clipboard;
|
||||
copyableButtonId = `copyable-button-${this.id}`;
|
||||
copyableButtonText = this.args.buttonText;
|
||||
|
||||
/**
|
||||
* A Ember Concurrency-based task that updates copy icon to a success state
|
||||
@@ -30,8 +27,10 @@ export default class CopyableComponent extends Component {
|
||||
*/
|
||||
@task(function * () {
|
||||
set(this, 'icon', 'copy-success');
|
||||
set(this, 'copyableButtonText', this.args.acknowledgeText);
|
||||
yield timeout(1000);
|
||||
set(this, 'icon', 'copy-action');
|
||||
set(this, 'copyableButtonText', this.args.buttonText);
|
||||
}).drop() successIconTimer;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -94,6 +97,7 @@ Router.map(function () {
|
||||
});
|
||||
|
||||
this.route('account', function() {
|
||||
this.route('change-password');
|
||||
// Enable after full account feature implementation
|
||||
// this.route('change-password');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
+16
-12
@@ -65,17 +65,16 @@
|
||||
|
||||
&.loading {
|
||||
&::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
|
||||
background: repeating-linear-gradient(to right, var(--subtle), var(--subtle), var(--info), var(--subtle), var(--subtle));
|
||||
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; }
|
||||
@@ -85,6 +84,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.copyable .copyable-button {
|
||||
padding: 0rem;
|
||||
.copyable {
|
||||
.copyable-button {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
padding: 0 sizing.rems(xxs);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,9 @@
|
||||
@iconOnly={{true}}
|
||||
@icon="boundary-icons/user-profile" as |dropdown|
|
||||
>
|
||||
<dropdown.link @route="account.change-password">
|
||||
{{!-- <dropdown.link @route="account.change-password">
|
||||
{{t 'actions.change-password'}}
|
||||
</dropdown.link>
|
||||
</dropdown.link> --}}
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "invalidateSession")}}
|
||||
>
|
||||
@@ -37,6 +37,17 @@
|
||||
{{outlet}}
|
||||
</layout.body>
|
||||
|
||||
<layout.footer>
|
||||
<Rose::Footer as |footer|>
|
||||
<footer.brand @logo="logo-hashi" @text={{t "hashicorp.title"}} />
|
||||
<footer.text>{{t "hashicorp.copyright"}}</footer.text>
|
||||
<footer.text>{{t "hashicorp.product"}}</footer.text>
|
||||
<footer.nav as |nav|>
|
||||
<nav.link @href={{t "hashicorp.documentation.link"}}>{{t "hashicorp.documentation.title"}}</nav.link>
|
||||
</footer.nav>
|
||||
</Rose::Footer>
|
||||
</layout.footer>
|
||||
|
||||
</Rose::Layout::Global>
|
||||
|
||||
<EmberNotify @closeAfter={{this.notifyTimeout}} as |message close|>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.auth-method.title"}}</h2>
|
||||
<p>{{t "resources.auth-method.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
+7
-1
@@ -1,2 +1,8 @@
|
||||
<h2>{{t "resources.account.title"}}</h2>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
+7
-1
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.account.title"}}</h2>
|
||||
<p>{{t 'resources.account.messages.set-password.description'}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -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>
|
||||
@@ -24,7 +24,13 @@
|
||||
{{account.username}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{account.id}}</code>
|
||||
<Copyable
|
||||
@text={{account.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{account.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
@@ -46,7 +46,13 @@
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{authMethod.id}}</code>
|
||||
<Copyable
|
||||
@text={{authMethod.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{authMethod.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.group.title"}}</h2>
|
||||
<p>{{t "resources.group.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -17,7 +17,13 @@
|
||||
{{member.displayName}}
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<code>{{member.id}}</code>
|
||||
<Copyable
|
||||
@text={{member.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{member.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
|
||||
@@ -39,7 +39,13 @@
|
||||
<p>{{group.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<code>{{group.id}}</code>
|
||||
<Copyable
|
||||
@text={{group.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{group.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
+7
-1
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.host-catalog.title"}}</h2>
|
||||
<p>{{t "resources.host-catalog.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
+7
-1
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.host-set.title"}}</h2>
|
||||
<p>{{t "resources.host-set.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
+7
-1
@@ -25,7 +25,13 @@
|
||||
<Rose::Badge>{{host.type}}</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{host.id}}</code>
|
||||
<Copyable
|
||||
@text={{host.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{host.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
|
||||
+7
-1
@@ -26,7 +26,13 @@
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{hostSet.id}}</code>
|
||||
<Copyable
|
||||
@text={{hostSet.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{hostSet.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
+7
-1
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.host.title"}}</h2>
|
||||
<p>{{t "resources.host.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
+7
-1
@@ -27,7 +27,13 @@
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{host.id}}</code>
|
||||
<Copyable
|
||||
@text={{host.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{host.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{host.attributes.address}}</code>
|
||||
|
||||
@@ -47,7 +47,13 @@
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{hostCatalog.id}}</code>
|
||||
<Copyable
|
||||
@text={{hostCatalog.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{hostCatalog.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
<page.header>
|
||||
<h2>{{t "resources.project.title"}}</h2>
|
||||
<p>{{t "resources.project.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
|
||||
@@ -28,7 +28,13 @@
|
||||
{{#each @model as |sessionAggregate|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
<code>{{sessionAggregate.session.id}}</code>
|
||||
<Copyable
|
||||
@text={{sessionAggregate.session.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{sessionAggregate.session.id}}</code>
|
||||
</Copyable>
|
||||
</row.headerCell>
|
||||
<row.cell>{{sessionAggregate.user.displayName}}</row.cell>
|
||||
<row.cell>{{sessionAggregate.target.displayName}}</row.cell>
|
||||
|
||||
@@ -51,7 +51,13 @@
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{target.id}}</code>
|
||||
<Copyable
|
||||
@text={{target.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{target.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.target.title"}}</h2>
|
||||
<p>{{t "resources.target.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -22,7 +22,13 @@
|
||||
<Rose::Badge>{{t (concat 'resources.host-set.types.' hostSet.model.type)}}</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{hostSet.model.id}}</code>
|
||||
<Copyable
|
||||
@text={{hostSet.model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{hostSet.model.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
|
||||
@@ -41,7 +41,13 @@
|
||||
<p>{{role.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<code>{{role.id}}</code>
|
||||
<Copyable
|
||||
@text={{role.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{role.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.role.title"}}</h2>
|
||||
<p>{{t "resources.role.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.role.title"}}</h2>
|
||||
<p>{{t "resources.role.grant.description"}}</p>
|
||||
<p><code>{{@model.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -21,7 +21,13 @@
|
||||
{{t (concat 'resources.role.principal.types.' principal.type)}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{principal.model.id}}</code>
|
||||
<Copyable
|
||||
@text={{principal.model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{principal.model.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<h2>{{t "resources.role.title"}}</h2>
|
||||
<p>{{t "resources.role.principal.description"}}</p>
|
||||
<p><code>{{@model.role.id}}</code></p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -41,7 +41,13 @@
|
||||
<p>{{user.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<code>{{user.id}}</code>
|
||||
<Copyable
|
||||
@text={{user.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{user.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
|
||||
@@ -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,9 @@
|
||||
<h2>{{t "resources.user.title"}}</h2>
|
||||
<p>{{t "resources.user.description"}}</p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
// Enable after account workflow implementation
|
||||
/*
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
@@ -15,6 +17,7 @@ module('Acceptance | accounts | change password', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
@@ -114,4 +117,6 @@ module('Acceptance | accounts | change password', function (hooks) {
|
||||
await visit(urls.changePassword);
|
||||
assert.notEqual(currentURL(), urls.changePassword);
|
||||
});
|
||||
|
||||
});
|
||||
*/
|
||||
@@ -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"]'));
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ module('Integration | Component | copyable', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
await render(hbs`<Copyable @text="Copy me">text</Copyable>`);
|
||||
await render(hbs`<Copyable @buttonText="Copy me">text</Copyable>`);
|
||||
assert.ok(find('.copyable'));
|
||||
assert.equal(find('.copyable-content').textContent.trim(), 'text');
|
||||
assert.equal(find('.copyable-button').textContent.trim(), 'Copy me');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,8 @@ module('Unit | Route | account/change-password', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:account/change-password');
|
||||
assert.ok(route);
|
||||
// let route = this.owner.lookup('route:account/change-password');
|
||||
// assert.ok(route);
|
||||
assert.expect(0);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ save: Save
|
||||
manage: Manage
|
||||
help: Need help?
|
||||
back: Back
|
||||
copy-to-clipboard: Copy to Clipboard
|
||||
authenticate: Authenticate
|
||||
deauthenticate: Deauthenticate
|
||||
choose-org: Choose Scope
|
||||
@@ -27,4 +28,4 @@ add-members: Add Members
|
||||
add-hosts: Add Hosts
|
||||
set-password: Set Password
|
||||
change-password: Change Password
|
||||
edit-form: Edit Form
|
||||
edit-form: Edit Form
|
||||
@@ -21,3 +21,10 @@ descriptions:
|
||||
questions:
|
||||
delete-confirm: Are you sure you want to delete this resource?
|
||||
remove-confirm: Are you sure you want to remove this association?
|
||||
hashicorp:
|
||||
title: HashiCorp
|
||||
copyright: © 2020 HashiCorp
|
||||
product: Boundary v0.1
|
||||
documentation:
|
||||
title: Documentation
|
||||
link: https://boundaryproject.io
|
||||
@@ -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.
|
||||
@@ -2,3 +2,4 @@ info: Info
|
||||
success: Success
|
||||
warning: Warning
|
||||
error: Error
|
||||
copied: Copied
|
||||
Reference in new issue
Block a user