refactor: Scope service into core addon and desktop routes

Ensure top-level scopes route within ui/admin and ui/desktop are exactly
the same in preparation for future refactor.

feat: Add projects/targets route

chore: yarn:format output in ui/desktop

test: Add basic acceptance tests for projects and targets
This commit is contained in:
Susmitha Girumala committed 2020-11-23 16:22:34 -05:00
1 parent 52ee7c9a04
commit 68107589bb
25 files changed
+439 -90

No files matched your search

+2 -2
View File
@@ -4,7 +4,7 @@ import { get } from '@ember/object';
/**
* A dead simple memory storage class used by `StorageService` when it
* is configued for memory-only.
* is configured for memory-only.
*/
class MemoryStorage {
/**
@@ -42,7 +42,7 @@ class MemoryStorage {
/**
* The storage service provides a simple way to persist data in the browser.
* Most of the time localStorage is used. Appplications may specify in-memory
* Most of the time localStorage is used. Applications may specify in-memory
* storage via the config, for example:
*
* ENV.storage = {
+18 -1
View File
@@ -69,7 +69,7 @@ export default class ExampleRoute extends Route {
}
```
In the application template, interate over confirmations, yielding the
In the application template, iterate over confirmations, yielding the
confirmation instance as well as accept and deny functions which, when called,
update the confirmation status.
@@ -108,6 +108,23 @@ async myAction() {
}
```
## Scope Service
This addon exposes a service to hold all active scopes (org and project where available).
To make use of the service, import it and initialize it using service injection.
```js
import { inject as service } from '@ember/service';
export default class ExampleRoute extends Route {
@service scope;
async myAction() {
// do something
}
}
```
## Yarn Commands
List of available project commands. `yarn run <command-name>`
@@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking';
/**
* This simple non-functional service is used to retain references to
* the currently active scope or scopes.
* the current active scope(s).
*/
export default class ScopeService extends Service {
// =attributes
+1
View File
@@ -0,0 +1 @@
export { default } from 'core/services/scope';
View File
Whitespace-only changes.
@@ -0,0 +1,18 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | scope', function (hooks) {
setupTest(hooks);
test('it can contain org scope', function (assert) {
let service = this.owner.lookup('service:scope');
service.org = {};
assert.ok(service.org, 'Service contains org scope');
});
test('it can contain project scope', function (assert) {
let service = this.owner.lookup('service:scope');
service.project = {};
assert.ok(service.project, 'Service contains project scope');
});
});
@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | scope', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let service = this.owner.lookup('service:scope');
assert.ok(service);
});
});
+3 -1
View File
@@ -9,10 +9,12 @@ export default class Router extends EmberRouter {
Router.map(function () {
this.route('scopes', function () {
this.route('scope', { path: ':scope_id' }, function () {
this.route('targets', function () {});
this.route('authenticate', function () {
this.route('method', { path: ':auth_method_id' });
});
this.route('projects', function () {
this.route('targets', function () {});
});
});
});
this.route('origin');
+29 -26
View File
@@ -5,6 +5,7 @@ import { A } from '@ember/array';
export default class ScopesScopeRoute extends Route {
// =services
@service intl;
@service session;
@service scope;
@@ -25,10 +26,12 @@ export default class ScopesScopeRoute extends Route {
// from ID because global has a fixed ID.
const type = id === 'global' ? 'global' : 'org';
return this.store.findRecord('scope', id).catch(() => {
console.error('caught error');
const maybeExistingScope = this.store.peekRecord('scope', id);
const scopeOptions = { id, type };
/* istanbul ignore else */
if (type === 'global') {
scopeOptions.name = this.intl.t('titles.global');
}
return (
maybeExistingScope || this.store.createRecord('scope', scopeOptions)
);
@@ -41,36 +44,25 @@ export default class ScopesScopeRoute extends Route {
* a project. These are used for scope navigation.
*/
async afterModel(model) {
// Load all orgs
let orgs;
// First, load orgs and, if necessary, projects
let orgs, projects;
orgs = await this.store
.query('scope', { scope_id: 'global' })
.catch(() => A([]));
// Then pull out the "selected" org, if relevant
let selectedOrg;
if (model.isGlobal || model.isOrg) selectedOrg = model;
let projects;
if (model.isOrg) {
projects = await this.store.query('scope', { scope_id: model.id });
if (model.isProject) {
projects = await this.store.query('scope', { scope_id: model.scopeID });
}
// Then pull out the "selected" scopes, if relevant
let selectedOrg, selectedProject;
if (model.isGlobal || model.isOrg) selectedOrg = model;
if (model.isProject) {
selectedProject = model;
selectedOrg = this.store.peekRecord('scope', model.scopeID);
}
// Update the scope service with the current scope(s);
this.scope.org = selectedOrg;
this.scope.projects = projects;
this.scopes = { orgs, selectedOrg };
}
/**
*
*/
redirect(model) {
const { isAuthenticated } = this.session;
const { isGlobal } = model;
if (isAuthenticated && isGlobal && this.scopes.orgs?.length) {
this.replaceWith('scopes.scope', this.scopes.orgs.firstObject.id);
}
this.scope.project = selectedProject;
this.scopes = { orgs, projects, selectedOrg, selectedProject };
}
/**
@@ -96,4 +88,15 @@ export default class ScopesScopeRoute extends Route {
outlet: 'header-nav',
});
}
/**
* In global scope, redirect to first org on authentication.
*/
redirect(model) {
const { isAuthenticated } = this.session;
const { isGlobal } = model;
if (isAuthenticated && isGlobal && this.scopes.orgs?.length) {
this.replaceWith('scopes.scope', this.scopes.orgs.firstObject.id);
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default class ScopesScopeIndexRoute extends Route {
if (!this.session.isAuthenticated) {
this.replaceWith('scopes.scope.authenticate');
} else {
this.replaceWith('scopes.scope.targets');
this.replaceWith('scopes.scope.projects.targets');
}
}
}
@@ -0,0 +1,28 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ScopesScopeProjectsRoute extends Route {
// =services
@service session;
// =methods
/**
* If arriving here unauthenticated, redirect to index for further processing.
*/
beforeModel() {
if (!this.session.isAuthenticated) this.transitionTo('index');
}
/**
* Loads project scopes for the current scope.
* @return {Promise
*/
model() {
const currentScope = this.modelFor('scopes.scope');
return this.store.query('scope', {
scope_id: currentScope.id,
});
}
}
@@ -2,11 +2,10 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { all } from 'rsvp';
export default class ScopesScopeTargetsRoute extends Route {
export default class ScopesScopeProjectsTargetsRoute extends Route {
// =services
@service session;
@service scope;
// =methods
@@ -22,8 +21,9 @@ export default class ScopesScopeTargetsRoute extends Route {
* @return {Promise{[TargetModel]}}
*/
async model() {
const projects = this.modelFor('scopes.scope.projects');
let projectTargets = await all(
this.scope.projects.map(({ id: scope_id }) =>
projects.map(({ id: scope_id }) =>
this.store.query('target', { scope_id })
)
);
@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ScopesScopeProjectsTargetsIndexRoute extends Route {}
@@ -1,3 +0,0 @@
import Route from '@ember/routing/route';
export default class ScopesScopeTargetsIndexRoute extends Route {}
-20
View File
@@ -1,20 +0,0 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
/**
* This simple non-functional service is used to retain references to
* the currently active scope or scopes.
*/
export default class ScopeService extends Service {
// =attributes
/**
* @type {ScopeModel}
*/
@tracked org;
/**
* @type {[ScopeModel]}
*/
@tracked projects;
}
@@ -0,0 +1 @@
{{outlet}}
+2 -4
View File
@@ -19,10 +19,8 @@ module.exports = function (defaults) {
importCss: false,
},
svg: {
paths: [
'../../addons/core/public'
]
}
paths: ['../../addons/core/public'],
},
});
// Use `app.import` to add additional libraries to the generated
@@ -0,0 +1,156 @@
import { module, test } from 'qunit';
import {
visit,
currentURL,
//fillIn,
//click,
//find,
//findAll,
//getRootElement
//setupOnerror,
} from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
//import { Response } from 'miragejs';
//import a11yAudit from 'ember-a11y-testing/test-support/audit';
import {
currentSession,
authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | projects', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let mockIPC;
let messageHandler;
const instances = {
scopes: {
global: null,
org: null,
project: null,
},
authMethods: {
global: null,
},
target: null,
};
const stubs = {
global: null,
org: null,
};
const urls = {
index: '/',
origin: '/origin',
scopes: {
global: null,
org: null,
},
authenticate: {
global: null,
methods: {
global: null,
},
},
projects: null,
};
const setDefaultOrigin = (test) => {
const origin = window.location.origin;
const session = test.owner.lookup('service:session');
session.set('data.origin', origin);
};
hooks.beforeEach(function () {
authenticateSession();
// create scopes
instances.scopes.global = this.server.create('scope', { id: 'global' });
stubs.global = { id: 'global', type: 'global' };
instances.scopes.org = this.server.create('scope', {
type: 'org',
scope: stubs.global,
});
stubs.org = { id: instances.scopes.org.id, type: 'org' };
instances.scopes.project = this.server.create('scope', {
type: 'project',
scope: stubs.org,
});
stubs.project = { id: instances.scopes.project.id, type: 'project' };
instances.authMethods.global = this.server.create('auth-method', {
scope: instances.scopes.global,
});
instances.hostCatalog = this.server.create(
'host-catalog',
{ scope: instances.scopes.project },
'withChildren'
);
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
urls.authenticate.global = `${urls.scopes.global}/authenticate`;
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
urls.projects = `${urls.scopes.org}/projects`;
class MockIPC {
origin = null;
invoke(method, payload) {
return this[method](payload);
}
getOrigin() {
return this.origin;
}
setOrigin(origin) {
this.origin = origin;
return this.origin;
}
}
mockIPC = new MockIPC();
messageHandler = async function (event) {
if (event.origin !== window.location.origin) return;
const { method, payload } = event.data;
if (method) {
const response = await mockIPC.invoke(method, payload);
event.ports[0].postMessage(response);
}
};
window.addEventListener('message', messageHandler);
setDefaultOrigin(this);
});
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
invalidateSession();
assert.expect(2);
await visit(urls.projects);
//await a11yAudit();
assert.notOk(currentSession().isAuthenticated);
assert.equal(currentURL(), urls.authenticate.methods.global);
});
test('visiting index', async function (assert) {
assert.expect(1);
await visit(urls.projects);
//await a11yAudit();
assert.equal(currentURL(), urls.projects);
});
});
+158
View File
@@ -0,0 +1,158 @@
import { module, test } from 'qunit';
import {
visit,
currentURL,
//fillIn,
//click,
//find,
//findAll,
//getRootElement
//setupOnerror,
} from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
//import { Response } from 'miragejs';
//import a11yAudit from 'ember-a11y-testing/test-support/audit';
import {
currentSession,
authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | targets', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let mockIPC;
let messageHandler;
const instances = {
scopes: {
global: null,
org: null,
project: null,
},
authMethods: {
global: null,
},
target: null,
};
const stubs = {
global: null,
org: null,
};
const urls = {
index: '/',
origin: '/origin',
scopes: {
global: null,
org: null,
},
authenticate: {
global: null,
methods: {
global: null,
},
},
projects: null,
targets: null,
};
const setDefaultOrigin = (test) => {
const origin = window.location.origin;
const session = test.owner.lookup('service:session');
session.set('data.origin', origin);
};
hooks.beforeEach(function () {
authenticateSession();
// create scopes
instances.scopes.global = this.server.create('scope', { id: 'global' });
stubs.global = { id: 'global', type: 'global' };
instances.scopes.org = this.server.create('scope', {
type: 'org',
scope: stubs.global,
});
stubs.org = { id: instances.scopes.org.id, type: 'org' };
instances.scopes.project = this.server.create('scope', {
type: 'project',
scope: stubs.org,
});
stubs.project = { id: instances.scopes.project.id, type: 'project' };
instances.authMethods.global = this.server.create('auth-method', {
scope: instances.scopes.global,
});
instances.hostCatalog = this.server.create(
'host-catalog',
{ scope: instances.scopes.project },
'withChildren'
);
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
urls.authenticate.global = `${urls.scopes.global}/authenticate`;
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
urls.projects = `${urls.scopes.org}/projects`;
urls.targets = `${urls.projects}/targets`;
class MockIPC {
origin = null;
invoke(method, payload) {
return this[method](payload);
}
getOrigin() {
return this.origin;
}
setOrigin(origin) {
this.origin = origin;
return this.origin;
}
}
mockIPC = new MockIPC();
messageHandler = async function (event) {
if (event.origin !== window.location.origin) return;
const { method, payload } = event.data;
if (method) {
const response = await mockIPC.invoke(method, payload);
event.ports[0].postMessage(response);
}
};
window.addEventListener('message', messageHandler);
setDefaultOrigin(this);
});
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
invalidateSession();
assert.expect(2);
await visit(urls.targets);
//await a11yAudit();
assert.notOk(currentSession().isAuthenticated);
assert.equal(currentURL(), urls.authenticate.methods.global);
});
test('visiting index', async function (assert) {
assert.expect(1);
await visit(urls.targets);
//await a11yAudit();
assert.equal(currentURL(), urls.targets);
});
});
@@ -1,11 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | scopes/scope/targets', function (hooks) {
module('Unit | Route | scopes/scope/projects', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:scopes/scope/targets');
let route = this.owner.lookup('route:scopes/scope/projects');
assert.ok(route);
});
});
@@ -1,11 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | scopes/scope/targets/index', function (hooks) {
module('Unit | Route | scopes/scope/projects/targets', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:scopes/scope/targets/index');
let route = this.owner.lookup('route:scopes/scope/projects/targets');
assert.ok(route);
});
});
@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | scopes/scope/projects/targets/index', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:scopes/scope/projects/targets/index');
assert.ok(route);
});
});
@@ -1,12 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | scope', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let service = this.owner.lookup('service:scope');
assert.ok(service);
});
});