@@ -24,8 +24,9 @@
{{yield (hash
- link=(component "link-to" class="rose-dropdown-link")
- button=(component "rose/button" style="dropdown")
+ link=(component "rose/dropdown/link" style=this.style)
+ button=(component "rose/dropdown/button" style=this.style)
+ separator=(component "rose/anonymous" tagName="hr" class="rose-dropdown-separator")
)}}
diff --git a/addons/rose/addon/templates/components/rose/dropdown/button.hbs b/addons/rose/addon/templates/components/rose/dropdown/button.hbs
new file mode 100644
index 000000000..364a2ef18
--- /dev/null
+++ b/addons/rose/addon/templates/components/rose/dropdown/button.hbs
@@ -0,0 +1,19 @@
+
diff --git a/addons/rose/addon/templates/components/rose/dropdown/link.hbs b/addons/rose/addon/templates/components/rose/dropdown/link.hbs
new file mode 100644
index 000000000..6b3bd413a
--- /dev/null
+++ b/addons/rose/addon/templates/components/rose/dropdown/link.hbs
@@ -0,0 +1,8 @@
+
+ {{yield}}
+
\ No newline at end of file
diff --git a/addons/rose/addon/templates/components/rose/header/dropdown.hbs b/addons/rose/addon/templates/components/rose/header/dropdown.hbs
deleted file mode 100644
index cc874c284..000000000
--- a/addons/rose/addon/templates/components/rose/header/dropdown.hbs
+++ /dev/null
@@ -1,32 +0,0 @@
-
diff --git a/addons/rose/addon/templates/components/rose/header/nav.hbs b/addons/rose/addon/templates/components/rose/header/nav.hbs
index 2b2978e00..a0858cc1a 100644
--- a/addons/rose/addon/templates/components/rose/header/nav.hbs
+++ b/addons/rose/addon/templates/components/rose/header/nav.hbs
@@ -1,6 +1,6 @@
diff --git a/addons/rose/addon/templates/components/rose/header/utilities.hbs b/addons/rose/addon/templates/components/rose/header/utilities.hbs
index 6ee49e33f..e00d45e75 100644
--- a/addons/rose/addon/templates/components/rose/header/utilities.hbs
+++ b/addons/rose/addon/templates/components/rose/header/utilities.hbs
@@ -1,5 +1,5 @@
diff --git a/addons/rose/addon/templates/components/rose/table.hbs b/addons/rose/addon/templates/components/rose/table.hbs
index d50ec970e..655e7dbd4 100644
--- a/addons/rose/addon/templates/components/rose/table.hbs
+++ b/addons/rose/addon/templates/components/rose/table.hbs
@@ -12,6 +12,6 @@
classNames='rose-table-body')
footer=(component 'rose/table/section'
tagName='tfoot'
- classNames='rose-table-foot')
+ classNames='rose-table-footer')
)}}
diff --git a/addons/rose/app/components/rose/dropdown/button.js b/addons/rose/app/components/rose/dropdown/button.js
new file mode 100644
index 000000000..0def7a1c0
--- /dev/null
+++ b/addons/rose/app/components/rose/dropdown/button.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/dropdown/button';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/dropdown/link.js b/addons/rose/app/components/rose/dropdown/link.js
new file mode 100644
index 000000000..99ec355d3
--- /dev/null
+++ b/addons/rose/app/components/rose/dropdown/link.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/dropdown/link';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/header/dropdown.js b/addons/rose/app/components/rose/header/dropdown.js
deleted file mode 100644
index 16d89ac10..000000000
--- a/addons/rose/app/components/rose/header/dropdown.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from 'rose/components/rose/header/dropdown';
diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs
index 5c6efcf85..529b6c7c5 100644
--- a/addons/rose/tests/dummy/app/templates/index.hbs
+++ b/addons/rose/tests/dummy/app/templates/index.hbs
@@ -1,23 +1,38 @@
-
-
-
- Address
- Host Set
- Name
- Description
-
-
-
-
- 1.1.1.1
- host-set-1, host-set-2
- optional name
- description
-
-
-
-
- Footer
-
-
-
\ No newline at end of file
+
+
+
+
+
+ Menu item
+ Menu item - danger
+
+ Button menu item
+
+
+ Section
+ Section
+
+
+
+
+ Menu item
+ Menu item - danger
+ Button - default
+
+ Button - danger
+
+
+
+
+
+ Menu item
+ Menu item - danger
+
+
+ Button menu item
+ Button - danger
+
diff --git a/addons/rose/tests/integration/components/rose/dropdown-test.js b/addons/rose/tests/integration/components/rose/dropdown-test.js
index 3d4597c95..0c0c22695 100644
--- a/addons/rose/tests/integration/components/rose/dropdown-test.js
+++ b/addons/rose/tests/integration/components/rose/dropdown-test.js
@@ -9,6 +9,7 @@ module('Integration | Component | rose/dropdown', function (hooks) {
test('it renders', async function (assert) {
await render(hbs``);
assert.ok(find('.rose-dropdown'));
+ assert.notOk(find('.rose-dropdown-right'));
});
test('it renders with html attributes', async function (assert) {
@@ -32,6 +33,13 @@ module('Integration | Component | rose/dropdown', function (hooks) {
assert.ok(find('.has-icon-only'));
});
+ test('it supports right aligned content', async function (assert) {
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-dropdown-right'));
+ });
+
test('it renders with content', async function (assert) {
await render(hbs`
diff --git a/addons/rose/tests/integration/components/rose/dropdown/button-test.js b/addons/rose/tests/integration/components/rose/dropdown/button-test.js
new file mode 100644
index 000000000..11fc945a6
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/dropdown/button-test.js
@@ -0,0 +1,38 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | rose/dropdown/button', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.rose-dropdown-button'));
+ });
+
+ test('it is type="button" by default', async function (assert) {
+ await render(hbs``);
+ assert.equal(find('button').type, 'button');
+ });
+
+ test('it is type="submit" when @submit={{true}}', async function (assert) {
+ await render(hbs``);
+ assert.equal(find('button').type, 'submit');
+ });
+
+ test('it is disabled when @disabled={{true}}', async function (assert) {
+ await render(hbs``);
+ assert.equal(find('button').disabled, true);
+ });
+
+ test('it adds a style class', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.rose-dropdown-button-warning'));
+ });
+
+ test('it supports an icon-only type', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.has-icon-only .rose-icon'));
+ });
+});
diff --git a/addons/rose/tests/integration/components/rose/dropdown/link-test.js b/addons/rose/tests/integration/components/rose/dropdown/link-test.js
new file mode 100644
index 000000000..a64b51bf8
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/dropdown/link-test.js
@@ -0,0 +1,31 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | rose/dropdown/link', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('a'));
+ assert.ok(find('.rose-dropdown-link'));
+ });
+
+ test('it renders with attributes', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('#dropdown'));
+ });
+
+ test('it renders with content', async function (assert) {
+ await render(hbs`
+ content
+ `);
+ assert.equal(find('.rose-dropdown-link').textContent.trim(), 'content');
+ });
+
+ test('it supports style', async function (assert) {
+ await render(hbs``);
+ assert.ok('.rose-dropdown-link-warning');
+ });
+});
diff --git a/addons/rose/tests/integration/components/rose/header-test.js b/addons/rose/tests/integration/components/rose/header-test.js
index 7e0f57da7..2feb23438 100644
--- a/addons/rose/tests/integration/components/rose/header-test.js
+++ b/addons/rose/tests/integration/components/rose/header-test.js
@@ -53,6 +53,6 @@ module('Integration | Component | rose/header', function (hooks) {
`);
- assert.ok(find('.rose-header-dropdown'));
+ assert.ok(find('.rose-dropdown'));
});
});
diff --git a/addons/rose/tests/integration/components/rose/header/dropdown-test.js b/addons/rose/tests/integration/components/rose/header/dropdown-test.js
deleted file mode 100644
index 96806b98b..000000000
--- a/addons/rose/tests/integration/components/rose/header/dropdown-test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'ember-qunit';
-import { render, click, find, findAll } from '@ember/test-helpers';
-import hbs from 'htmlbars-inline-precompile';
-
-module('Integration | Component | rose/header/dropdown', function (hooks) {
- setupRenderingTest(hooks);
-
- test('it renders', async function (assert) {
- await render(hbs``);
- assert.ok(find('.rose-header-dropdown'));
- });
-
- test('it renders with html attributes', async function (assert) {
- await render(
- hbs``
- );
- assert.ok(find('#custom-id'));
- assert.ok(find('.custom-class'));
- });
-
- test('it renders with trigger', async function (assert) {
- await render(hbs``);
- assert.equal(
- find('.rose-header-dropdown-trigger').textContent.trim(),
- 'Click me'
- );
- });
-
- test('it supports an icon', async function (assert) {
- await render(hbs``);
- assert.ok(find('svg'));
- });
-
- test('it supports icon-only display', async function (assert) {
- await render(hbs``);
- assert.ok(find('.icon-only'));
- });
-
- test('it supports right-aligned content', async function (assert) {
- await render(hbs``);
- assert.ok(find('.dropdown-right'));
- });
-
- test('it renders with content', async function (assert) {
- await render(hbs`
-
-
-
-
-
- `);
- assert.ok(find('.rose-header-dropdown-content'));
- assert.equal(findAll('a').length, 2);
- assert.equal(findAll('button').length, 3);
- });
-
- test('it is toggled on click on trigger content', async function (assert) {
- await render(hbs``);
-
- assert.notOk(find('#dropdown').open);
- await click('summary');
- assert.ok(find('#dropdown').open);
- await click('summary');
- assert.notOk(find('#dropdown').open);
- });
-
- test('it is closed with outside click is triggered', async function (assert) {
- await render(hbs`
-
-
-
- `);
-
- await click('summary');
- assert.ok(find('#dropdown').open);
- await click('#wrapper');
- assert.notOk(find('#dropdown').open);
- });
-});
diff --git a/addons/rose/tests/integration/components/rose/header/nav-test.js b/addons/rose/tests/integration/components/rose/header/nav-test.js
index d1c5a688d..fd713c0db 100644
--- a/addons/rose/tests/integration/components/rose/header/nav-test.js
+++ b/addons/rose/tests/integration/components/rose/header/nav-test.js
@@ -24,6 +24,6 @@ module('Integration | Component | rose/header/nav', function (hooks) {
`);
- assert.equal(findAll('.rose-header-dropdown').length, 2);
+ assert.equal(findAll('.rose-dropdown').length, 2);
});
});
diff --git a/addons/rose/tests/integration/components/rose/header/utilities-test.js b/addons/rose/tests/integration/components/rose/header/utilities-test.js
index 884266d8f..1bb44de8e 100644
--- a/addons/rose/tests/integration/components/rose/header/utilities-test.js
+++ b/addons/rose/tests/integration/components/rose/header/utilities-test.js
@@ -16,6 +16,13 @@ module('Integration | Component | rose/header/utilities', function (hooks) {
`);
- assert.equal(findAll('.rose-header-dropdown').length, 2);
+ assert.equal(findAll('.rose-dropdown').length, 2);
+ });
+
+ test('it renders nav dropdown elements as right aligned', async function (assert) {
+ await render(hbs`
+
+ `);
+ assert.ok(find('.rose-dropdown.rose-dropdown-right'));
});
});
diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js
index a70c89ac7..88bf63c0a 100644
--- a/ui/core/app/authenticators/password.js
+++ b/ui/core/app/authenticators/password.js
@@ -37,9 +37,52 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
* @param {string} scopeID
* @return {string}
*/
- buildDeauthEndpointURL({ org_id: scopeID }) {
+ buildDeauthEndpointURL({ scope: { id: scopeID } }) {
const adapter = this.store.adapterFor('application');
const options = { adapterOptions: { method: 'deauthenticate' } };
return adapter.buildURL('scope', scopeID, options, 'findRecord');
}
+
+ /**
+ * Intercepts the authenticate response, if any, and assigns the returned
+ * token to all future requests via `addTokenToAuthorization`.
+ * Returns the response data as normal.
+ * @override
+ */
+ authenticate() {
+ return super.authenticate(...arguments)
+ .then(data => {
+ const token = data?.token;
+ if (token) this.addTokenToAuthorization(token);
+ return data;
+ });
+ }
+
+ /**
+ * When restoring the session (say, on load), assigns the session token
+ * to all future requests via `addTokenToAuthorization`.
+ * @override
+ * @param {object} data
+ * @return {object}
+ */
+ restore(data) {
+ const token = data?.token;
+ if (token) this.addTokenToAuthorization(token);
+ return super.restore(data);
+ }
+
+ /**
+ * Assigns a token string all future requests via the `Authorization` header
+ * on the application adapter prototype. Only application and adapters that
+ * extend the application adapter receive this header (which is most).
+ * @param {string} token
+ */
+ addTokenToAuthorization(token) {
+ const adapterPrototype =
+ this.store.adapterFor('application').constructor.prototype;
+ const headers = adapterPrototype?.headers;
+ if (!headers) adapterPrototype.headers = {};
+ adapterPrototype.headers.Authorization = null;
+ if (token) adapterPrototype.headers.Authorization = `Bearer ${token}`;
+ }
}
diff --git a/ui/core/app/components/form/org/index.hbs b/ui/core/app/components/form/org/index.hbs
new file mode 100644
index 000000000..45adc26d0
--- /dev/null
+++ b/ui/core/app/components/form/org/index.hbs
@@ -0,0 +1,49 @@
+
+
+
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.description}}
+
+ {{#each @model.errors.description as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+
+
diff --git a/ui/core/app/controllers/scopes/scope/authenticate.js b/ui/core/app/controllers/scopes/scope/authenticate.js
new file mode 100644
index 000000000..b982a3c1c
--- /dev/null
+++ b/ui/core/app/controllers/scopes/scope/authenticate.js
@@ -0,0 +1,21 @@
+import Controller from '@ember/controller';
+import { sort } from '@ember/object/computed';
+
+export default class ScopesScopeAuthenticateController extends Controller {
+
+ // =attributes
+
+ /**
+ * @type {Array}
+ */
+ @sort('model.scopes', (a, b) => {
+ if (a.isGlobal) {
+ return -1;
+ } else if (b.isGlobal) {
+ return 1;
+ }
+ return 0;
+ })
+ sortedScopes;
+
+}
diff --git a/ui/core/app/controllers/scopes/scope/orgs.js b/ui/core/app/controllers/scopes/scope/orgs.js
new file mode 100644
index 000000000..9e553d5a3
--- /dev/null
+++ b/ui/core/app/controllers/scopes/scope/orgs.js
@@ -0,0 +1,17 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeOrgsController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('resources.orgs');
+ }
+}
diff --git a/ui/core/app/controllers/scopes/scope/orgs/new.js b/ui/core/app/controllers/scopes/scope/orgs/new.js
new file mode 100644
index 000000000..d41b81a96
--- /dev/null
+++ b/ui/core/app/controllers/scopes/scope/orgs/new.js
@@ -0,0 +1,17 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeOrgsNewController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('titles.new');
+ }
+}
diff --git a/ui/core/app/router.js b/ui/core/app/router.js
index 91a8aceca..b0ef0cbf6 100644
--- a/ui/core/app/router.js
+++ b/ui/core/app/router.js
@@ -9,17 +9,35 @@ export default class Router extends EmberRouter {
Router.map(function () {
this.route('scopes', function () {
this.route('scope', { path: ':scope_id' }, function () {
+
this.route('authenticate', function () {
this.route('method', { path: ':auth_method_id' });
});
+
+ this.route('orgs', function() {
+ this.route('new');
+ });
+
+ this.route('projects', function() {
+ this.route('project', { path: ':project_id' }, function() {
+ this.route('host-catalogs', function() {
+ this.route('new');
+ this.route('host-catalog', { path: ':host_catalog_id' }, function() {});
+ });
+ });
+ this.route('new');
+ });
+
this.route('users', function () {
this.route('user', { path: ':user_id' }, function () {});
this.route('new');
});
+
this.route('groups', function () {
this.route('group', { path: ':group_id' }, function () {});
this.route('new');
});
+
this.route('roles', function () {
this.route('role', { path: ':role_id' }, function () {
this.route('grants');
@@ -27,6 +45,7 @@ Router.map(function () {
});
this.route('new');
});
+
this.route('projects', function() {
this.route('project', { path: ':project_id' }, function() {
this.route('host-catalogs', function() {
@@ -42,6 +61,7 @@ Router.map(function () {
this.route('auth-method', { path: ':auth_method_id' }, function() {});
this.route('new');
});
+
});
});
});
diff --git a/ui/core/app/routes/scopes/index.js b/ui/core/app/routes/scopes/index.js
index 562a27a15..b1ec77f7f 100644
--- a/ui/core/app/routes/scopes/index.js
+++ b/ui/core/app/routes/scopes/index.js
@@ -15,15 +15,15 @@ export default class ScopesIndexRoute extends Route {
* @param {[ScopeModel]} model
* @param {?ScopeModel} model.firstObject
*/
- redirect({ firstObject }) {
+ redirect() {
const authenticatedScopeID = get(
this.session,
'data.authenticated.scope.id'
);
if (authenticatedScopeID) {
this.transitionTo('scopes.scope', authenticatedScopeID);
- } else if (firstObject) {
- this.transitionTo('scopes.scope', firstObject.id);
+ } else {
+ this.transitionTo('scopes.scope', 'global');
}
}
}
diff --git a/ui/core/app/routes/scopes/scope.js b/ui/core/app/routes/scopes/scope.js
index 2707a238b..08cd2d207 100644
--- a/ui/core/app/routes/scopes/scope.js
+++ b/ui/core/app/routes/scopes/scope.js
@@ -1,6 +1,12 @@
import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
export default class ScopesScopeRoute extends Route {
+
+ // =services
+
+ @service intl;
+
// =methods
/**
@@ -14,11 +20,18 @@ export default class ScopesScopeRoute extends Route {
* @return {Promise{ScopeModel}}
*/
model({ scope_id: id }) {
+ // Since only global and org scopes are authenticatable, we can infer type
+ // from ID because global has a fixed ID.
+ const type = id === 'global' ? 'global' : 'org';
return this.store.findRecord('scope', id).catch(() => {
const maybeExistingScope = this.store.peekRecord('scope', id);
- // TODO it's unclear if creating a temporary unsaved scope is reliable
- // we may need to revert to a POJO.
- return maybeExistingScope || this.store.createRecord('scope', { id });
+ const scopeOptions = { id, type };
+ if (type === 'global') {
+ scopeOptions.name = this.intl.t('titles.global');
+ }
+ return maybeExistingScope ||
+ this.store.createRecord('scope', scopeOptions);
});
}
+
}
diff --git a/ui/core/app/routes/scopes/scope/auth-methods.js b/ui/core/app/routes/scopes/scope/auth-methods.js
index ffad801fa..0e35b2c28 100644
--- a/ui/core/app/routes/scopes/scope/auth-methods.js
+++ b/ui/core/app/routes/scopes/scope/auth-methods.js
@@ -7,9 +7,17 @@ export default class ScopesScopeAuthMethodsRoute extends Route {
@service intl;
@service notify;
+ @service session;
// =methods
+ /**
+ * If arriving here unauthenticated, redirect to index for further processing.
+ */
+ beforeModel() {
+ if (!this.session.isAuthenticated) this.transitionTo('index');
+ }
+
/**
* Load all auth-methods under current scope
* @return {Promise[AuthMethodModel]}
diff --git a/ui/core/app/routes/scopes/scope/authenticate.js b/ui/core/app/routes/scopes/scope/authenticate.js
index d7dae560d..ef08a1566 100644
--- a/ui/core/app/routes/scopes/scope/authenticate.js
+++ b/ui/core/app/routes/scopes/scope/authenticate.js
@@ -7,12 +7,6 @@ export default class ScopesScopeAuthenticateRoute extends Route {
@service session;
- // =attributes
-
- // TODO: this assumes we're in an org scope, but the "landing"
- // for global scope is probably different.
- routeIfAlreadyAuthenticated = 'scopes.scope.projects';
-
// =methods
/**
@@ -37,7 +31,12 @@ export default class ScopesScopeAuthenticateRoute extends Route {
redirect() {
if (this.session.isAuthenticated) {
- this.transitionTo(this.routeIfAlreadyAuthenticated);
+ const scope = this.modelFor('scopes.scope');
+ if (scope.isGlobal) {
+ this.transitionTo('scopes.scope.orgs');
+ } else {
+ this.transitionTo('scopes.scope.projects');
+ }
}
}
}
diff --git a/ui/core/app/routes/scopes/scope/authenticate/method.js b/ui/core/app/routes/scopes/scope/authenticate/method.js
index 5208b152d..ed1ebc026 100644
--- a/ui/core/app/routes/scopes/scope/authenticate/method.js
+++ b/ui/core/app/routes/scopes/scope/authenticate/method.js
@@ -9,10 +9,6 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
@service notify;
@service intl;
- // =attributes
-
- routeAfterAuthentication = 'scopes.scope.projects';
-
// =methods
/**
@@ -55,7 +51,11 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
requestCookies,
{ scope, authMethod }
);
- this.transitionTo(this.routeAfterAuthentication);
+ if (scope.isGlobal) {
+ this.transitionTo('scopes.scope.orgs');
+ } else {
+ this.transitionTo('scopes.scope.projects');
+ }
} catch (e) {
const errorMessage = this.intl.t('errors.titles.authentication-failed');
this.notify.error(errorMessage, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/groups.js b/ui/core/app/routes/scopes/scope/groups.js
index 7eed4406e..c2c75c8fb 100644
--- a/ui/core/app/routes/scopes/scope/groups.js
+++ b/ui/core/app/routes/scopes/scope/groups.js
@@ -7,9 +7,17 @@ export default class ScopesScopeGroupsRoute extends Route {
@service intl;
@service notify;
+ @service session;
// =methods
+ /**
+ * If arriving here unauthenticated, redirect to index for further processing.
+ */
+ beforeModel() {
+ if (!this.session.isAuthenticated) this.transitionTo('index');
+ }
+
/**
* Load all groups under current scope
* @return {Promise[GroupModel]}
diff --git a/ui/core/app/routes/scopes/scope/orgs.js b/ui/core/app/routes/scopes/scope/orgs.js
new file mode 100644
index 000000000..c2fa56ffc
--- /dev/null
+++ b/ui/core/app/routes/scopes/scope/orgs.js
@@ -0,0 +1,64 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+export default class ScopesScopeOrgsRoute extends Route {
+
+ // =services
+
+ @service intl;
+ @service notify;
+ @service session;
+
+ // =methods
+
+ /**
+ * If arriving here unauthenticated, redirect to index for further processing.
+ */
+ beforeModel() {
+ if (!this.session.isAuthenticated) this.transitionTo('index');
+ }
+
+ /**
+ * Loads all scopes under the global scope.
+ * @return {Promise{[ScopeModel]}}
+ */
+ async model() {
+ return this.store.query('scope', { scope_id: 'global' });
+ }
+
+ // =actions
+
+ /**
+ * Rollback changes on org.
+ * @param {ScopeModel} org
+ */
+ @action
+ cancel(org) {
+ const { isNew } = org;
+ org.rollbackAttributes();
+ if (isNew) this.transitionTo('scopes.scope.orgs');
+ }
+
+ /**
+ * Handle save org.
+ * @param {ScopeModel} org
+ * @param {Event} e
+ */
+ @action
+ async save(org) {
+ const { isNew } = org;
+ try {
+ await org.save();
+ this.refresh();
+ this.notify.success(
+ this.intl.t(isNew ? 'notify.create-success' : 'notify.save-success')
+ );
+ this.transitionTo('scopes.scope', org);
+ } catch (error) {
+ // TODO: replace with translated strings
+ this.notify.error(error.message, { closeAfter: null });
+ }
+ }
+
+}
diff --git a/ui/core/app/routes/scopes/scope/orgs/index.js b/ui/core/app/routes/scopes/scope/orgs/index.js
new file mode 100644
index 000000000..4a344e13a
--- /dev/null
+++ b/ui/core/app/routes/scopes/scope/orgs/index.js
@@ -0,0 +1,4 @@
+import Route from '@ember/routing/route';
+
+export default class ScopesScopeOrgsIndexRoute extends Route {
+}
diff --git a/ui/core/app/routes/scopes/scope/orgs/new.js b/ui/core/app/routes/scopes/scope/orgs/new.js
new file mode 100644
index 000000000..0337f2dce
--- /dev/null
+++ b/ui/core/app/routes/scopes/scope/orgs/new.js
@@ -0,0 +1,18 @@
+import Route from '@ember/routing/route';
+
+export default class ScopesScopeOrgsNewRoute extends Route {
+
+ // =methods
+
+ /**
+ * Creates a new unsaved org scope belonging to the global scope.
+ * @return {ScopeModel}
+ */
+ model() {
+ return this.store.createRecord('scope', {
+ type: 'org',
+ scopeID: 'global',
+ });
+ }
+
+}
diff --git a/ui/core/app/routes/scopes/scope/roles.js b/ui/core/app/routes/scopes/scope/roles.js
index 84cf2ae0f..b0418e399 100644
--- a/ui/core/app/routes/scopes/scope/roles.js
+++ b/ui/core/app/routes/scopes/scope/roles.js
@@ -7,9 +7,17 @@ export default class ScopesScopeRolesRoute extends Route {
@service intl;
@service notify;
+ @service session;
// =methods
+ /**
+ * If arriving here unauthenticated, redirect to index for further processing.
+ */
+ beforeModel() {
+ if (!this.session.isAuthenticated) this.transitionTo('index');
+ }
+
/**
* Load all roles under current scope.
* @return {Promise{[RoleModel]}}
diff --git a/ui/core/app/routes/scopes/scope/users.js b/ui/core/app/routes/scopes/scope/users.js
index f1982b769..20731c90f 100644
--- a/ui/core/app/routes/scopes/scope/users.js
+++ b/ui/core/app/routes/scopes/scope/users.js
@@ -7,9 +7,17 @@ export default class ScopesScopeUsersRoute extends Route {
@service intl;
@service notify;
+ @service session;
// =methods
+ /**
+ * If arriving here unauthenticated, redirect to index for further processing.
+ */
+ beforeModel() {
+ if (!this.session.isAuthenticated) this.transitionTo('index');
+ }
+
/**
* Load all users under current scope.
* @return {Promise{[UserModel]}}
diff --git a/ui/core/app/session-stores/application.js b/ui/core/app/session-stores/application.js
index 401a98b44..681b7b12e 100644
--- a/ui/core/app/session-stores/application.js
+++ b/ui/core/app/session-stores/application.js
@@ -1,38 +1,6 @@
import LocalStorageSessionStore from 'auth/session-stores/local-storage';
-import { inject as service } from '@ember/service';
-import { get } from '@ember/object';
-//import config from '../config/environment';
-export default class ApplicationSessionStore extends LocalStorageSessionStore {
- @service store;
-
- /**
- * If the auth sessionData includes a token, apply it as a bearer token in the
- * application adapter's Authorization header. Otherwise clear the header.
- * @param {object} sessionData
- * @return {object}
- */
- persist(sessionData) {
- this.addTokenToAuthorization(sessionData);
- //this.addScopeToSession(sessionData);
- return super.persist(...arguments);
- }
-
- /**
- * If the session has a full token, it is added as a bearer token to the
- * `Authorization` header for all API requests.
- * @param {object} sessionData
- */
- addTokenToAuthorization(sessionData) {
- const token = get(sessionData, 'authenticated.token');
- const adapter = this.store.adapterFor('application');
- const headers = get(adapter, 'headers');
- if (!headers) adapter.headers = {};
- adapter.headers.Authorization = null;
- if (token) adapter.headers.Authorization = `Bearer ${token}`;
- }
-
-}
+export default class ApplicationSessionStore extends LocalStorageSessionStore {}
// import CookieSessionStore from 'auth/session-stores/cookie';
// import config from '../config/environment';
diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs
index 15d146e9a..2422a624e 100644
--- a/ui/core/app/templates/application.hbs
+++ b/ui/core/app/templates/application.hbs
@@ -14,7 +14,9 @@
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
-
+
{{t 'actions.deauthenticate'}}
diff --git a/ui/core/app/templates/scopes/scope.hbs b/ui/core/app/templates/scopes/scope.hbs
index 37530455c..5f60d2607 100644
--- a/ui/core/app/templates/scopes/scope.hbs
+++ b/ui/core/app/templates/scopes/scope.hbs
@@ -1,6 +1,7 @@
{{#if this.session.isAuthenticated}}
+ {{#unless @model.isGlobal}}
@@ -26,6 +27,7 @@
{{outlet "sidebar"}}
+ {{/unless}}
{{/if}}
diff --git a/ui/core/app/templates/scopes/scope/authenticate.hbs b/ui/core/app/templates/scopes/scope/authenticate.hbs
index 43b329188..75a60893f 100644
--- a/ui/core/app/templates/scopes/scope/authenticate.hbs
+++ b/ui/core/app/templates/scopes/scope/authenticate.hbs
@@ -14,7 +14,7 @@
@text={{t "actions.choose-different-org"}}
@icon="user-organization"
as |dropdown|>
- {{#each @model.scopes as |scope|}}
+ {{#each this.sortedScopes as |scope|}}
{{scope.displayName}}
diff --git a/ui/core/app/templates/scopes/scope/groups/group/index.hbs b/ui/core/app/templates/scopes/scope/groups/group/index.hbs
index ac8915f59..e78a0c548 100644
--- a/ui/core/app/templates/scopes/scope/groups/group/index.hbs
+++ b/ui/core/app/templates/scopes/scope/groups/group/index.hbs
@@ -16,20 +16,24 @@
+
+
+
+ {{t 'actions.delete-group'}}
+
+
+
+
-
-
- {{t 'actions.delete'}}
-
diff --git a/ui/core/app/templates/scopes/scope/orgs.hbs b/ui/core/app/templates/scopes/scope/orgs.hbs
new file mode 100644
index 000000000..35fbae66b
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/orgs.hbs
@@ -0,0 +1,3 @@
+{{title (t "resources.orgs")}}
+
+{{outlet}}
diff --git a/ui/core/app/templates/scopes/scope/orgs/index.hbs b/ui/core/app/templates/scopes/scope/orgs/index.hbs
new file mode 100644
index 000000000..9abc5722e
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/orgs/index.hbs
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+ {{t "resources.orgs"}}
+
+
+
+ {{#if @model}}
+
+ {{t 'titles.new'}}
+
+ {{/if}}
+
+
+
+
+ {{#if @model}}
+
+
+ {{#each @model as |org|}}
+
+ {{/each}}
+
+
+ {{else}}
+
+
+
+
+ {{t 'descriptions.orgs-welcome'}}
+
+
+
+ {{t 'titles.new'}}
+
+
+
+
+ {{/if}}
+
+
+
+
diff --git a/ui/core/app/templates/scopes/scope/orgs/new.hbs b/ui/core/app/templates/scopes/scope/orgs/new.hbs
new file mode 100644
index 000000000..d9fe8b361
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/orgs/new.hbs
@@ -0,0 +1,21 @@
+{{title (t 'titles.new')}}
+
+
+
+
+
+
+
+
+ {{t "titles.new"}}
+
+
+
+
+
+
+
diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs
index 0a979afcf..1055f4ea2 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs
@@ -10,4 +10,4 @@
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete'}}
-
\ No newline at end of file
+
diff --git a/ui/core/app/templates/scopes/scope/projects/project/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/index.hbs
index 5dccae834..c3bec3790 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/index.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/index.hbs
@@ -16,20 +16,24 @@
+
+
+
+ {{t 'actions.delete-project'}}
+
+
+
+
-
-
- {{t 'actions.delete'}}
-
diff --git a/ui/core/app/templates/scopes/scope/roles/role.hbs b/ui/core/app/templates/scopes/scope/roles/role.hbs
index 0cd189b0a..397e9f504 100644
--- a/ui/core/app/templates/scopes/scope/roles/role.hbs
+++ b/ui/core/app/templates/scopes/scope/roles/role.hbs
@@ -10,6 +10,18 @@
{{t "resources.role"}}
+
+
+
+ {{t 'actions.delete-role'}}
+
+
+
+
diff --git a/ui/core/app/templates/scopes/scope/roles/role/index.hbs b/ui/core/app/templates/scopes/scope/roles/role/index.hbs
index bbb671abe..e72610ec6 100644
--- a/ui/core/app/templates/scopes/scope/roles/role/index.hbs
+++ b/ui/core/app/templates/scopes/scope/roles/role/index.hbs
@@ -3,11 +3,4 @@
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
/>
-
-
- {{t 'actions.delete'}}
-
+
diff --git a/ui/core/app/templates/scopes/scope/users/index.hbs b/ui/core/app/templates/scopes/scope/users/index.hbs
index 1ee0f3ac2..fb0a06a09 100644
--- a/ui/core/app/templates/scopes/scope/users/index.hbs
+++ b/ui/core/app/templates/scopes/scope/users/index.hbs
@@ -21,21 +21,21 @@
- {{t 'form.label.name'}}
- {{t 'form.label.description'}}
+ {{t 'form.label.name'}}
+ {{t 'form.label.description'}}
{{#each @model as |user|}}
-
+
{{user.name}}
-
+
{{user.description}}
{{/each}}
diff --git a/ui/core/app/templates/scopes/scope/users/user/index.hbs b/ui/core/app/templates/scopes/scope/users/user/index.hbs
index d80978336..6392e84f7 100644
--- a/ui/core/app/templates/scopes/scope/users/user/index.hbs
+++ b/ui/core/app/templates/scopes/scope/users/user/index.hbs
@@ -8,6 +8,18 @@
{{t "resources.user"}}
+
+
+
+ {{t 'actions.delete-user'}}
+
+
+
+
@@ -17,19 +29,11 @@
-
-
-
- {{t 'actions.delete'}}
-
diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js
index 2e830c4ce..5a87be352 100644
--- a/ui/core/mirage/scenarios/default.js
+++ b/ui/core/mirage/scenarios/default.js
@@ -2,7 +2,12 @@ export default function(server) {
// Scope resources
- const globalScope = server.create('scope', { id: 'global' }); // creates a global scope
+ // creates a global scope
+ const globalScope = server.create('scope', {
+ id: 'global',
+ type: 'global',
+ name: 'Global'
+ });
const orgScope = server.createList('scope', 3, {
type: 'org',
scope: { id: globalScope.id, type: globalScope.type }
diff --git a/ui/core/tests/acceptance/authentication-test.js b/ui/core/tests/acceptance/authentication-test.js
index 478e2f1dd..fa43f2247 100644
--- a/ui/core/tests/acceptance/authentication-test.js
+++ b/ui/core/tests/acceptance/authentication-test.js
@@ -1,5 +1,5 @@
import { module, test } from 'qunit';
-import { visit, currentURL, fillIn, click, find } from '@ember/test-helpers';
+import { visit, currentURL, fillIn, click, find, findAll } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { Response } from 'miragejs';
@@ -23,8 +23,11 @@ module('Acceptance | authentication', function (hooks) {
let scope;
let authMethod;
let authMethodID;
+ let globalAuthenticateURL;
let authenticateURL;
let authMethodAuthenticateURL;
+ let authMethodGlobalAuthenticateURL;
+ let orgsURL;
let projectsURL;
hooks.beforeEach(function () {
@@ -45,21 +48,14 @@ module('Acceptance | authentication', function (hooks) {
authMethodID = authMethod.id;
scopesURL = `/scopes`;
orgScopeURL = `/scopes/${orgScopeID}`;
+ globalAuthenticateURL = `/scopes/global/authenticate`;
authenticateURL = `/scopes/${orgScopeID}/authenticate`;
+ authMethodGlobalAuthenticateURL = `/scopes/global/authenticate/${authMethodID}`;
authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`;
+ orgsURL = `/scopes/global/orgs`;
projectsURL = `/scopes/${orgScopeID}/projects`;
});
-
- test('visiting scopes when there are none shows a message', async function (assert) {
- assert.expect(2);
- this.server.get('/scopes', () => new Response(404));
- await visit(scopesURL);
- await a11yAudit();
- assert.equal(currentURL(), scopesURL);
- assert.ok(find('.rose-message'));
- });
-
test('visiting auth methods authenticate route redirects to first auth method', async function (assert) {
assert.expect(1);
await visit(authenticateURL);
@@ -96,19 +92,19 @@ module('Acceptance | authentication', function (hooks) {
assert.equal(currentURL(), authMethodAuthenticateURL);
});
- test('visiting any authentication parent route while unauthenticated redirects to first authenticate method', async function (assert) {
+ test('visiting any authentication parent route while unauthenticated redirects to first global authenticate method', async function (assert) {
assert.expect(3);
await visit(indexURL);
- assert.equal(currentURL(), authMethodAuthenticateURL);
+ assert.equal(currentURL(), authMethodGlobalAuthenticateURL);
await visit(scopesURL);
- assert.equal(currentURL(), authMethodAuthenticateURL);
+ assert.equal(currentURL(), authMethodGlobalAuthenticateURL);
assert.notOk(currentSession().isAuthenticated);
});
- test('visiting projects while unauthenticated redirects to first authenticate method', async function (assert) {
+ test('visiting projects while unauthenticated redirects to first global authenticate method', async function (assert) {
assert.expect(2);
await visit(projectsURL);
- assert.equal(currentURL(), authMethodAuthenticateURL);
+ assert.equal(currentURL(), authMethodGlobalAuthenticateURL);
assert.notOk(currentSession().isAuthenticated);
});
@@ -119,7 +115,7 @@ module('Acceptance | authentication', function (hooks) {
assert.equal(currentURL(), authMethodAuthenticateURL);
});
- test('visiting any authentication parent route while already authenticated redirects to projects', async function (assert) {
+ test('visiting any authentication parent route while already authenticated with an org redirects to projects', async function (assert) {
assert.expect(6);
authenticateSession({ scope });
await visit(indexURL);
@@ -135,6 +131,18 @@ module('Acceptance | authentication', function (hooks) {
assert.ok(currentSession().isAuthenticated);
});
+ test('visiting index or scopes routes while already authenticated with global redirects to orgs', async function (assert) {
+ assert.expect(4);
+ authenticateSession({ scope: { id: globalScope.id, type: globalScope.type } });
+ await visit(indexURL);
+ assert.equal(currentURL(), orgsURL);
+ await visit(scopesURL);
+ assert.equal(currentURL(), orgsURL);
+ await visit(globalAuthenticateURL);
+ assert.equal(currentURL(), orgsURL);
+ assert.ok(currentSession().isAuthenticated);
+ });
+
test('failed authentication shows a notification message', async function (assert) {
assert.expect(3);
await visit(authMethodAuthenticateURL);
@@ -157,15 +165,19 @@ module('Acceptance | authentication', function (hooks) {
assert.ok(currentSession().isAuthenticated);
});
- test('deauthentication redirects to first authenticate method', async function (assert) {
+ test('deauthentication redirects to first global authenticate method', async function (assert) {
assert.expect(3);
await visit(authMethodAuthenticateURL);
await fillIn('[name="identification"]', 'test');
await fillIn('[name="password"]', 'test');
await click('[type="submit"]');
assert.ok(currentSession().isAuthenticated);
- await click('.rose-button-header-dropdown');
+ // Open header utilities dropdown
+ await click('.rose-header-utilities .rose-dropdown summary');
+ // Find and click on last element in dropdown - should be deauthenticate button
+ const menu = findAll('.rose-header-utilities .rose-dropdown .rose-dropdown-content button');
+ await click(menu[menu.length - 1]);
assert.notOk(currentSession().isAuthenticated);
- assert.equal(currentURL(), authMethodAuthenticateURL);
+ assert.equal(currentURL(), authMethodGlobalAuthenticateURL);
});
});
diff --git a/ui/core/tests/acceptance/groups-test.js b/ui/core/tests/acceptance/groups-test.js
index b619d09ce..0d622f1f0 100644
--- a/ui/core/tests/acceptance/groups-test.js
+++ b/ui/core/tests/acceptance/groups-test.js
@@ -133,7 +133,7 @@ module('Acceptance | groups', function (hooks) {
assert.expect(1);
const groupsCount = this.server.db.groups.length;
await visit(groupURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.groups.length, groupsCount - 1);
});
@@ -210,7 +210,7 @@ module('Acceptance | groups', function (hooks) {
);
});
await visit(groupURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
diff --git a/ui/core/tests/acceptance/host-catalogs-test.js b/ui/core/tests/acceptance/host-catalogs-test.js
index 59bc08dcc..fada1730e 100644
--- a/ui/core/tests/acceptance/host-catalogs-test.js
+++ b/ui/core/tests/acceptance/host-catalogs-test.js
@@ -80,7 +80,7 @@ module('Acceptance | host catalogs', function (hooks) {
authenticateSession({});
await visit(urls.hostCatalog);
assert.equal(currentURL(), urls.hostCatalog);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.hostCatalogs.length, hostCatalogsCount - 1);
});
@@ -162,7 +162,7 @@ module('Acceptance | host catalogs', function (hooks) {
});
authenticateSession({});
await visit(urls.hostCatalog);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
diff --git a/ui/core/tests/acceptance/orgs-test.js b/ui/core/tests/acceptance/orgs-test.js
new file mode 100644
index 000000000..5871fd404
--- /dev/null
+++ b/ui/core/tests/acceptance/orgs-test.js
@@ -0,0 +1,76 @@
+import { module, test } from 'qunit';
+//import { visit, currentURL, click, fillIn, find } 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 | org', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ // let org;
+ // let orgID;
+ // let orgsURL;
+ // let newOrgURL;
+ // let existingOrg;
+ // let existingOrgURL;
+ // let getOrgScopesCount;
+ // let getFirstOrgScope;
+ // let initialOrgScopesCount;
+ //
+ // hooks.beforeEach(function () {
+ // authenticateSession({});
+ //
+ // });
+
+ test('visiting orgs', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('visiting orgs within a org scope redirects to the parent org scope', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('can create new orgs', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('can cancel create new orgs', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('saving a new org with invalid fields displays error messages', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('can save changes to existing org', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('can cancel changes to existing org', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('can delete org', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('saving an existing org with invalid fields displays error messages', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('errors are displayed when save org fails', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('errors are displayed when delete org fails', async function (assert) {
+ assert.expect(0);
+ });
+});
diff --git a/ui/core/tests/acceptance/projects-test.js b/ui/core/tests/acceptance/projects-test.js
index ced4a8f13..4af813592 100644
--- a/ui/core/tests/acceptance/projects-test.js
+++ b/ui/core/tests/acceptance/projects-test.js
@@ -135,7 +135,7 @@ module('Acceptance | projects', function (hooks) {
test('can delete project', async function (assert) {
assert.expect(1);
await visit(existingProjectURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getProjectScopesCount(), initialProjectScopesCount - 1);
});
@@ -212,7 +212,7 @@ module('Acceptance | projects', function (hooks) {
);
});
await visit(existingProjectURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js
index 2aa364f1b..14bd45a24 100644
--- a/ui/core/tests/acceptance/roles-test.js
+++ b/ui/core/tests/acceptance/roles-test.js
@@ -133,7 +133,7 @@ module('Acceptance | roles', function (hooks) {
assert.expect(1);
const rolesCount = this.server.db.roles.length;
await visit(roleURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.roles.length, rolesCount - 1);
});
@@ -210,7 +210,7 @@ module('Acceptance | roles', function (hooks) {
);
});
await visit(roleURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
diff --git a/ui/core/tests/acceptance/users-test.js b/ui/core/tests/acceptance/users-test.js
index 31fa5c98e..55e4ac108 100644
--- a/ui/core/tests/acceptance/users-test.js
+++ b/ui/core/tests/acceptance/users-test.js
@@ -133,7 +133,7 @@ module('Acceptance | users', function (hooks) {
assert.expect(1);
const usersCount = this.server.db.users.length;
await visit(userURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.users.length, usersCount - 1);
});
@@ -210,7 +210,7 @@ module('Acceptance | users', function (hooks) {
);
});
await visit(userURL);
- await click('.rose-button-warning');
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
diff --git a/ui/core/tests/integration/components/form/org-test.js b/ui/core/tests/integration/components/form/org-test.js
new file mode 100644
index 000000000..76c3d9b8f
--- /dev/null
+++ b/ui/core/tests/integration/components/form/org-test.js
@@ -0,0 +1,23 @@
+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 | form/org', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+ this.submit = () => {};
+ this.cancel = () => {};
+
+ await render(hbs`
+ `);
+
+ assert.ok(find('form'));
+ });
+});
diff --git a/ui/core/tests/unit/authenticators/password-test.js b/ui/core/tests/unit/authenticators/password-test.js
new file mode 100644
index 000000000..8bf59c9ee
--- /dev/null
+++ b/ui/core/tests/unit/authenticators/password-test.js
@@ -0,0 +1,44 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+
+module('Unit | Authenticator | password', function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+
+ let store;
+ let applicationAdapter;
+ let session;
+ let authenticator;
+
+ hooks.beforeEach(function () {
+ store = this.owner.lookup('service:store');
+ applicationAdapter = store.adapterFor('application');
+ session = this.owner.lookup('service:session');
+ authenticator = this.owner.lookup('authenticator:password');
+ session.invalidate();
+ });
+
+ hooks.afterEach(function () {
+ session.invalidate();
+ });
+
+ test('it adds an authorization header to application adapter on restore', async function (assert) {
+ assert.expect(1);
+ authenticator.restore({ token: 'token1234' });
+ assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234');
+ });
+
+ test('it adds an authorization header to application adapter on authenticate', async function (assert) {
+ assert.expect(1);
+ const creds = { identification: 'foo', password: 'bar' };
+ const scope = { id: 'global' };
+ const authMethod = { id: 'paum_1234' };
+ await authenticator.authenticate(
+ creds,
+ null,
+ { scope, authMethod }
+ );
+ assert.equal(applicationAdapter.headers.Authorization, 'Bearer thetokenstring');
+ });
+});
diff --git a/ui/core/tests/unit/controllers/scopes/scope/authenticate-test.js b/ui/core/tests/unit/controllers/scopes/scope/authenticate-test.js
new file mode 100644
index 000000000..41bd58b67
--- /dev/null
+++ b/ui/core/tests/unit/controllers/scopes/scope/authenticate-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Controller | scopes/scope/authenticate', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function(assert) {
+ let controller = this.owner.lookup('controller:scopes/scope/authenticate');
+ assert.ok(controller);
+ });
+});
diff --git a/ui/core/tests/unit/controllers/scopes/scope/orgs-test.js b/ui/core/tests/unit/controllers/scopes/scope/orgs-test.js
new file mode 100644
index 000000000..f89afd0fc
--- /dev/null
+++ b/ui/core/tests/unit/controllers/scopes/scope/orgs-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Controller | scopes/scope/orgs', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function(assert) {
+ let controller = this.owner.lookup('controller:scopes/scope/orgs');
+ assert.ok(controller);
+ });
+});
diff --git a/ui/core/tests/unit/controllers/scopes/scope/orgs/new-test.js b/ui/core/tests/unit/controllers/scopes/scope/orgs/new-test.js
new file mode 100644
index 000000000..c6631a1ab
--- /dev/null
+++ b/ui/core/tests/unit/controllers/scopes/scope/orgs/new-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Controller | scopes/scope/orgs/new', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function(assert) {
+ let controller = this.owner.lookup('controller:scopes/scope/orgs/new');
+ assert.ok(controller);
+ });
+});
diff --git a/ui/core/tests/unit/routes/scopes/scope/orgs-test.js b/ui/core/tests/unit/routes/scopes/scope/orgs-test.js
new file mode 100644
index 000000000..4b9d477c3
--- /dev/null
+++ b/ui/core/tests/unit/routes/scopes/scope/orgs-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | scopes/scope/orgs', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:scopes/scope/orgs');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/tests/unit/routes/scopes/scope/orgs/index-test.js b/ui/core/tests/unit/routes/scopes/scope/orgs/index-test.js
new file mode 100644
index 000000000..c62bbc625
--- /dev/null
+++ b/ui/core/tests/unit/routes/scopes/scope/orgs/index-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | scopes/scope/orgs/index', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:scopes/scope/orgs/index');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/tests/unit/routes/scopes/scope/orgs/new-test.js b/ui/core/tests/unit/routes/scopes/scope/orgs/new-test.js
new file mode 100644
index 000000000..3cfdb8522
--- /dev/null
+++ b/ui/core/tests/unit/routes/scopes/scope/orgs/new-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | scopes/scope/orgs/new', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:scopes/scope/orgs/new');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/tests/unit/session-stores/application-test.js b/ui/core/tests/unit/session-stores/application-test.js
deleted file mode 100644
index 6a3e908bc..000000000
--- a/ui/core/tests/unit/session-stores/application-test.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { module, test } from 'qunit';
-import { setupTest } from 'ember-qunit';
-
-module('Unit | Session Store | application', function (hooks) {
- setupTest(hooks);
-
- let store;
- let applicationAdapter;
- let sessionStore;
-
- hooks.beforeEach(function () {
- store = this.owner.lookup('service:store');
- applicationAdapter = store.adapterFor('application');
- sessionStore = this.owner.lookup('session-store:application');
- sessionStore.clear();
- });
-
- hooks.afterEach(function () {
- sessionStore.clear();
- });
-
- test('it does not add an authorization header to application adapter on persist if no token is present', async function (assert) {
- assert.expect(1);
- sessionStore.persist({ authenticated: { token: null } });
- assert.notOk(applicationAdapter.headers.Authorization);
- });
-
- test('it adds an authorization header to application adapter on persist', async function (assert) {
- assert.expect(1);
- sessionStore.persist({ authenticated: { token: 'token1234' } });
- assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234');
- });
-
- test('it adds an authorization header to application adapter which already has headers on persist', async function (assert) {
- assert.expect(2);
- applicationAdapter.headers = { foo: 'bar' };
- sessionStore.persist({ authenticated: { token: 'token1234' } });
- assert.equal(applicationAdapter.headers.foo, 'bar');
- assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234');
- });
-});
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index 2f2e4df01..31e60a02d 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -9,6 +9,11 @@ actions:
deauthenticate: Deauthenticate
choose-org: Choose Scope
choose-different-org: Choose a Different Scope
+ delete-user: Delete user
+ delete-group: Delete group
+ delete-role: Delete role
+ delete-project: Delete project
+ delete-host-catalog: Delete host catalog
auth:
titles:
no-scopes: No Scopes Available
@@ -27,6 +32,8 @@ resources:
roles: Roles
group: Group
groups: Groups
+ orgs: Orgs
+ org: Org
projects: Projects
project: Project
host-catalog: Host Catalog
@@ -53,6 +60,7 @@ titles:
new-grant: New Grant
principals: Principals
host-sets: Host Sets
+ global: Global
descriptions:
host-catalogs: Overview of collections of hosts and host sets.
empty-set: There are no items to display yet. You may be able to add items or try back later.
@@ -75,6 +83,7 @@ form:
type: Type
identification: Username
password: Password
+ manage: Manage
grant: Grant
principal: Principal
errors: