Merge pull request #92 from hashicorp/refactor-scopes-ui
Refactor scopes ui
This commit is contained in:
49 files changed
+780
-200
No files matched your search
@@ -59,8 +59,13 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Prepends a scope to the URL prefix, if a scope is passed via the
|
||||
* snapshot's `adapterOptions.scopeID` field.
|
||||
* Prepends a scope to the URL prefix. Precedence is given to the ID passed
|
||||
* via `adapterOptions.scopeID`. If this value isn't found in
|
||||
* `adapterOptions`, we look to the `scope.scope_id` field on the snapshot,
|
||||
* which most resources have.
|
||||
*
|
||||
* Note: scopes themselves never receive a scope URL prefix.
|
||||
*
|
||||
* @override
|
||||
* @param {string} path
|
||||
* @param {string} parentURL
|
||||
@@ -69,12 +74,27 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @param {object} snapshot
|
||||
* @return {string}
|
||||
*/
|
||||
urlPrefix(path, parentURL, modelName, id, snapshot={}) {
|
||||
urlPrefix(path, parentURL, modelName, id, snapshot) {
|
||||
const prefix = super.urlPrefix(...arguments);
|
||||
let scopePath = getWithDefault(snapshot, 'adapterOptions.scopeID', '');
|
||||
const isScope = modelName === 'scope';
|
||||
let scopePath = '';
|
||||
let scopeID = '';
|
||||
if (snapshot) {
|
||||
// Not all snapshots have `attr` (such as array snapshots),
|
||||
// so we do this sort of ugly check.
|
||||
if (snapshot.attr) {
|
||||
const parentScope = snapshot.attr('scope');
|
||||
if (parentScope) scopeID = parentScope.attr('scope_id');
|
||||
}
|
||||
// Attempt to get scopeID from adapterOptions and fallback on its current
|
||||
// value if it wasn't passed through options.
|
||||
scopeID = getWithDefault(snapshot, 'adapterOptions.scopeID', scopeID);
|
||||
}
|
||||
// Only non-scope resources need a scope path, since scope resources
|
||||
// aren't technically "scoped" the same way.
|
||||
// Ensure a slash is added between prefix + scope path if needed.
|
||||
if (scopePath && prefix.charAt(prefix.length - 1) !== '/') {
|
||||
scopePath = `/scopes/${scopePath}`;
|
||||
if (!isScope && scopeID && prefix.charAt(prefix.length - 1) !== '/') {
|
||||
scopePath = `/scopes/${scopeID}`;
|
||||
}
|
||||
return `${prefix}${scopePath}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import ApplicationAdapter from './application';
|
||||
import { serializeIntoHash } from '@ember-data/adapter/-private';
|
||||
|
||||
export default class ScopeAdapter extends ApplicationAdapter {
|
||||
|
||||
/**
|
||||
* Identical to default method except that we pass a query parameter
|
||||
* indicating the parent scope.
|
||||
* @override
|
||||
* @param {Store} store
|
||||
* @param {Model} type
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Promise} promise
|
||||
*/
|
||||
createRecord(store, type, snapshot) {
|
||||
const parentScopeID = snapshot.attr('scope').attr('scope_id');
|
||||
let url =
|
||||
this.buildURL(type.modelName, null, snapshot, 'createRecord');
|
||||
// TODO this should be generalized into `buildURL`, but currently Ember Data
|
||||
// doesn't provide a way to manage query parameters. They are passed on
|
||||
// to the underlying ajax/fetch service.
|
||||
url = `${url}?scope_id=${parentScopeID}`;
|
||||
const data = serializeIntoHash(store, type, snapshot);
|
||||
return this.ajax(url, 'POST', { data });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -84,32 +84,4 @@ export default class BaseModel extends Model {
|
||||
return !this.canSave;
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* If this model instance has a related scope, the scope's ID will be passed
|
||||
* via `adapterOptions.scopeID` automatically. End users may optionally
|
||||
* pass a custom scope ID to override.
|
||||
*
|
||||
* @example
|
||||
* model.save(); // passes the model's current scope ID
|
||||
* model.save({adapterOptions: {scopeID: 'global'}}); // override scope ID
|
||||
*
|
||||
* End users may specify a _custom method_ via `adapterOptions.method`.
|
||||
* Custom methods are appended to the record's URL by the adapter.
|
||||
*
|
||||
* @example
|
||||
* // Results in a URL: `/v1/model-name/id:my-custom-method`
|
||||
* model.save({adapterOptions: {method: 'my-custom-method'}});
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
save(options={adapterOptions: {}}) {
|
||||
const scopeID = this.scopeID;
|
||||
if (scopeID && !options.adapterOptions.scopeID) {
|
||||
options.adapterOptions.scopeID = scopeID;
|
||||
}
|
||||
return super.save(options);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import RESTSerializer from '@ember-data/serializer/rest';
|
||||
import { underscore } from '@ember/string';
|
||||
import { get } from '@ember/object';
|
||||
import { copy } from 'ember-copy';
|
||||
|
||||
/**
|
||||
@@ -117,7 +118,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
primaryModelClass.modelName
|
||||
);
|
||||
// Copy the unrooted payload under the expected root key name.
|
||||
transformedPayload[payloadKey] = copy(payload);
|
||||
transformedPayload[payloadKey] = copy(payload, true);
|
||||
// Return the result of normalizing the transformed payload.
|
||||
return super.normalizeSingleResponse(
|
||||
store,
|
||||
@@ -128,4 +129,21 @@ export default class ApplicationSerializer extends RESTSerializer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One consequence of using model fragments to represent the embedded scope
|
||||
* is that they cannot have `id` fields. We still need to know scope IDs,
|
||||
* so we copy the `scope.id` value into the `scope.scope_id` field.
|
||||
* @override
|
||||
* @see FragmentScope
|
||||
* @param {Model} typeClass
|
||||
* @param {Object} hash
|
||||
* @return {Object}
|
||||
*/
|
||||
normalize(typeClass, hash) {
|
||||
const normalizedHash = copy(hash, true);
|
||||
const scopeID = get(normalizedHash, 'scope.id');
|
||||
if (scopeID) normalizedHash.scope.scope_id = scopeID;
|
||||
return super.normalize(typeClass, normalizedHash);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class ScopeSerializer extends ApplicationSerializer {
|
||||
|
||||
// =attributes
|
||||
|
||||
attrs = {
|
||||
scope: { serialize: true }
|
||||
};
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* While most resources do not serialize scope into a request body (scope
|
||||
* is expressed in the URL), scopes themselves _do serialize scope_ as a
|
||||
* string field `scope_id`.
|
||||
* Otherwise delegate to default attribute serializer.
|
||||
*
|
||||
* @override
|
||||
* @method serializeAttribute
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Object} json
|
||||
* @param {String} key
|
||||
* @param {Object} attribute
|
||||
*/
|
||||
serializeAttribute(snapshot, json, key) {
|
||||
const value = super.serializeAttribute(...arguments);
|
||||
const parentScope = snapshot.attr('scope');
|
||||
if (key === 'scope' && this._canSerialize(key) && parentScope) {
|
||||
json.scope_id = parentScope.attr('scope_id');
|
||||
delete json[key];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/adapters/scope';
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from 'api/serializers/scope';
|
||||
@@ -29,12 +29,22 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
const scopeID = 'global';
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const prefix = adapter.urlPrefix(null, null, null, null, {
|
||||
adapterOptions: {scopeID}
|
||||
adapterOptions: { scopeID }
|
||||
});
|
||||
assert.ok(config.api.namespace);
|
||||
assert.equal(prefix, `v1/scopes/${scopeID}`);
|
||||
});
|
||||
|
||||
test('it does not prefixes URLs with `scopeID` for scope resources', function (assert) {
|
||||
assert.expect(1);
|
||||
const scopeID = 'global';
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const prefix = adapter.urlPrefix(null, null, 'scope', null, {
|
||||
adapterOptions: { scopeID }
|
||||
});
|
||||
assert.equal(prefix, `v1`);
|
||||
});
|
||||
|
||||
test('it generates correct default URL suffixes', function (assert) {
|
||||
assert.expect(1);
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
@@ -46,7 +56,7 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
const method = 'set-something';
|
||||
const adapter = this.owner.lookup('adapter:application');
|
||||
const suffix = adapter.urlSuffix(null, null, {
|
||||
adapterOptions: {method}
|
||||
adapterOptions: { method }
|
||||
});
|
||||
assert.ok(config.api.namespace);
|
||||
assert.equal(suffix, ':set-something');
|
||||
@@ -85,7 +95,9 @@ module('Unit | Adapter | application', function (hooks) {
|
||||
assert.ok(true, 'Scoped resource URL was requested.');
|
||||
return {items: []};
|
||||
});
|
||||
await store.findAll('group', {adapterOptions: {scopeID: 'p_456'}});
|
||||
await store.findAll('group', {
|
||||
adapterOptions: { scopeID: 'p_456' }
|
||||
});
|
||||
});
|
||||
|
||||
test('it rewrites PUT to PATCH, but leaves others unchanged', function (assert) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Adapter | scope', function(hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it generates correct createRecord URLs with a scope_id query parameter', function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/v1/scopes', (schema, { queryParams }) =>{
|
||||
assert.equal(queryParams.scope_id, 'o_1');
|
||||
});
|
||||
const store = this.owner.lookup('service:store');
|
||||
const scope = store.createRecord('scope', {
|
||||
scopeID: 'o_1'
|
||||
});
|
||||
const type = scope.constructor;
|
||||
const snapshot = scope._createSnapshot();
|
||||
const adapter = this.owner.lookup('adapter:scope');
|
||||
adapter.createRecord(store, type, snapshot);
|
||||
});
|
||||
});
|
||||
@@ -36,9 +36,9 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
const userModelClass = store.createRecord('user').constructor;
|
||||
const payload = {
|
||||
items: [
|
||||
{ id: 1, name: 'User 1' },
|
||||
{ id: 2, name: 'User 2' },
|
||||
{ id: 3, name: 'User 3' },
|
||||
{ id: 1, name: 'User 1', scope: { id: 'o_123' } },
|
||||
{ id: 2, name: 'User 2', scope: { id: 'o_123' } },
|
||||
{ id: 3, name: 'User 3', scope: { id: 'o_123' } },
|
||||
],
|
||||
};
|
||||
const normalizedArray = serializer.normalizeArrayResponse(
|
||||
@@ -52,19 +52,19 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
{
|
||||
id: '1',
|
||||
type: 'user',
|
||||
attributes: { name: 'User 1' },
|
||||
attributes: { name: 'User 1', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'user',
|
||||
attributes: { name: 'User 2' },
|
||||
attributes: { name: 'User 2', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'user',
|
||||
attributes: { name: 'User 3' },
|
||||
attributes: { name: 'User 3', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
],
|
||||
@@ -79,6 +79,7 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
const payload = {
|
||||
id: '1',
|
||||
name: 'User 1',
|
||||
scope: { id: 'o_123' }
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
@@ -90,7 +91,7 @@ module('Unit | Serializer | application', function (hooks) {
|
||||
data: {
|
||||
id: '1',
|
||||
type: 'user',
|
||||
attributes: { name: 'User 1' },
|
||||
attributes: { name: 'User 1', scope: { scope_id: 'o_123' } },
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | scope', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('scope');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('scope', {
|
||||
type: 'org',
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
scopeID: 'global'
|
||||
});
|
||||
const serializedRecord = record.serialize();
|
||||
assert.deepEqual(serializedRecord, {
|
||||
type: 'org',
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
scope_id: 'global'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeProjectsController extends Controller {
|
||||
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('resources.projects');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeProjectsNewController extends Controller {
|
||||
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.intl.t('titles.new');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeProjectsProjectController extends Controller {
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
|
||||
}
|
||||
+11
-1
@@ -7,6 +7,16 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
|
||||
Router.map(function () {
|
||||
|
||||
this.route('scopes', function() {
|
||||
this.route('scope', { path: ':scope_id' }, function() {
|
||||
this.route('projects', function() {
|
||||
this.route('project', { path: ':project_id' }, function() {});
|
||||
this.route('new');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.route('orgs', function () {
|
||||
this.route('org', { path: ':org_id' }, function () {
|
||||
this.route('projects', function () {
|
||||
@@ -23,7 +33,7 @@ Router.map(function () {
|
||||
this.route('user', { path: ':user_id' }, function() {});
|
||||
this.route('new');
|
||||
});
|
||||
|
||||
|
||||
this.route('roles', function() {
|
||||
this.route('role', { path: ':role_id' }, function() {});
|
||||
this.route('new');
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesRoute extends Route {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeRoute extends Route {
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Attempt to load the specified scope from the API. This is allowed
|
||||
* to fail, since in some cases the user may not have permission to read a
|
||||
* scope directly, but may have permission to read resources under it.
|
||||
* If the scope fails to load, we still proceed using a temporary scope object
|
||||
* consisting of only the specified ID.
|
||||
* @param {object} params
|
||||
* @param {string} params.scope_id
|
||||
* @return {Promise{ScopeModel}}
|
||||
*/
|
||||
async model({ scope_id: id }) {
|
||||
try {
|
||||
return await 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 });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ScopesScopeProjectsRoute extends Route {
|
||||
|
||||
// =services
|
||||
|
||||
@service notify;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Loads all scopes under the current scope.
|
||||
* @return {Promise{[ScopeModel]}}
|
||||
*/
|
||||
async model() {
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
return this.store.query('scope', { scope_id });
|
||||
}
|
||||
|
||||
/**
|
||||
* If arriving here within a verifiable project scope, redirect to its
|
||||
* parent, which is necessarily an org, in order to list projects.
|
||||
*
|
||||
* TODO: this conditional redirect based on the scope type is probably
|
||||
* necessary for many routes directly under scope, so we may want
|
||||
* to move this into a mixin.
|
||||
* @param {ScopeModel}
|
||||
*/
|
||||
redirect() {
|
||||
const { isProject, scopeID } = this.modelFor('scopes.scope');
|
||||
if (isProject) this.transitionTo('scopes.scope.projects', scopeID);
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Rollback changes on project.
|
||||
* @param {ProjectModel} project
|
||||
*/
|
||||
@action
|
||||
cancel(project) {
|
||||
const { isNew } = project;
|
||||
project.rollbackAttributes();
|
||||
if (isNew) this.transitionTo('scopes.scope.projects');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save project.
|
||||
* @param {ProjectModel} project
|
||||
* @param {Event} e
|
||||
*/
|
||||
@action
|
||||
async save(project) {
|
||||
try {
|
||||
await project.save();
|
||||
this.refresh();
|
||||
// TODO: replace with translated strings
|
||||
this.notify.success('Save succeeded.');
|
||||
this.transitionTo('scopes.scope.projects.project', project);
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the project and redirects to index.
|
||||
* @param {ProjectModel} project
|
||||
*/
|
||||
@action
|
||||
async delete(project) {
|
||||
try {
|
||||
await project.destroyRecord();
|
||||
this.refresh();
|
||||
this.transitionTo('scopes.scope.projects');
|
||||
// TODO: replace with translated strings
|
||||
this.notify.success('Deleted project succesfully.');
|
||||
} catch (error) {
|
||||
// TODO: replace with translated strings
|
||||
this.notify.error(error.message, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeProjectsIndexRoute extends Route {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeProjectsNewRoute extends Route {
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Creates a new unsaved project scope belonging to the current org scope.
|
||||
* @return {ScopeModel}
|
||||
*/
|
||||
model() {
|
||||
const { id } = this.modelFor('scopes.scope');
|
||||
return this.store.createRecord('scope', {
|
||||
type: 'project',
|
||||
scopeID: id
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeProjectsProjectRoute extends Route {
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Loads a scope by ID (ostensibly a project scope).
|
||||
* @param {object} params
|
||||
* @param {string} params.project_id
|
||||
* @return {ScopeModel}
|
||||
*/
|
||||
model({ project_id }) {
|
||||
return this.store.findRecord('scope', project_id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeProjectsProjectIndexRoute extends Route {
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
<HeadLayout />
|
||||
{{title (app-name)}}
|
||||
|
||||
<Rose::Layout::Global as |layout|>
|
||||
|
||||
<layout.header>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,37 @@
|
||||
<Rose::Layout::Sidebar as |sidebarLayout|>
|
||||
|
||||
<sidebarLayout.sidebar>
|
||||
|
||||
<Rose::Nav::Sidebar @title={{t "titles.general"}} as |nav|>
|
||||
<nav.link @route="scopes.scope.projects">
|
||||
{{t "resources.projects"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
|
||||
{{outlet "sidebar"}}
|
||||
|
||||
{{!--
|
||||
<Rose::Nav::Sidebar @title={{t "titles.iam"}} as |nav|>
|
||||
<nav.link @route="orgs.org.users">
|
||||
{{t "resources.users"}}
|
||||
</nav.link>
|
||||
|
||||
<nav.link @route="orgs.org.groups">
|
||||
{{t "resources.groups"}}
|
||||
</nav.link>
|
||||
|
||||
<nav.link @route="orgs.org.roles">
|
||||
{{t "resources.roles"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
--}}
|
||||
|
||||
</sidebarLayout.sidebar>
|
||||
|
||||
<sidebarLayout.body>
|
||||
<main>
|
||||
{{outlet}}
|
||||
</main>
|
||||
</sidebarLayout.body>
|
||||
|
||||
</Rose::Layout::Sidebar>
|
||||
@@ -0,0 +1,3 @@
|
||||
{{title (t "resources.projects")}}
|
||||
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,56 @@
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "resources.projects"}}</h2>
|
||||
</page.header>
|
||||
|
||||
<page.actions>
|
||||
{{#if @model}}
|
||||
<Rose::LinkButton
|
||||
@route="scopes.scope.projects.new"
|
||||
@style="primary"
|
||||
>
|
||||
{{t 'titles.new'}}
|
||||
</Rose::LinkButton>
|
||||
{{/if}}
|
||||
</page.actions>
|
||||
|
||||
<page.body>
|
||||
|
||||
{{#if @model}}
|
||||
|
||||
<Rose::Cards as |cards|>
|
||||
{{#each @model as |project|}}
|
||||
<cards.link
|
||||
@title={{project.name}}
|
||||
@subtitle={{project.id}}
|
||||
@description={{project.description}}
|
||||
@route="scopes.scope.projects.project"
|
||||
@model={{project}}
|
||||
/>
|
||||
{{/each}}
|
||||
</Rose::Cards>
|
||||
|
||||
{{else}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t 'titles.projects-welcome'}} as |message|>
|
||||
<message.description>
|
||||
{{t 'descriptions.projects-welcome'}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.projects.new">
|
||||
<Rose::Icon @name="plus-circle-outline" />
|
||||
{{t 'titles.new'}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
|
||||
{{/if}}
|
||||
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -0,0 +1,21 @@
|
||||
{{title (t 'titles.new')}}
|
||||
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "titles.new"}}</h2>
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
<Form::Project
|
||||
@model={{@model}}
|
||||
@submit={{route-action "save" @model}}
|
||||
@cancel={{route-action "cancel" @model}}
|
||||
/>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -0,0 +1,3 @@
|
||||
{{title @model.displayName}}
|
||||
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "resources.project"}}</h2>
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
<nav.link @route='scopes.scope.projects.project'>
|
||||
{{t 'titles.details'}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Tabs>
|
||||
</page.navigation>
|
||||
|
||||
<page.body>
|
||||
<Form::Project
|
||||
@model={{@model}}
|
||||
@submit={{route-action "save" @model}}
|
||||
@cancel={{route-action "cancel" @model}}
|
||||
/>
|
||||
<Rose::Separator />
|
||||
<Rose::Button
|
||||
@style="warning"
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "delete" @model)}}
|
||||
>
|
||||
{{t 'actions.delete'}}
|
||||
</Rose::Button>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
+58
-43
@@ -1,5 +1,5 @@
|
||||
import config from '../config/environment';
|
||||
// import { Response } from 'miragejs';
|
||||
import { Response } from 'miragejs';
|
||||
|
||||
export default function() {
|
||||
|
||||
@@ -16,66 +16,81 @@ export default function() {
|
||||
this.passthrough();
|
||||
|
||||
// Scope resources
|
||||
// org
|
||||
|
||||
this.get('/orgs');
|
||||
this.post('/orgs');
|
||||
this.get('/orgs/:id');
|
||||
this.patch('/orgs/:id');
|
||||
this.del('/orgs/:id');
|
||||
|
||||
// project
|
||||
|
||||
this.get('/orgs/:org_id/projects');
|
||||
this.post('/orgs/:org_id/projects');
|
||||
this.get('/orgs/:org_id/projects/:id');
|
||||
this.patch('/orgs/:org_id/projects/:id');
|
||||
this.del('/orgs/:org_id/projects/:id');
|
||||
this.get('/scopes', ({ scopes }, { queryParams: { scope_id } }) => {
|
||||
// Default parent scope is global
|
||||
if (!scope_id) scope_id = 'global';
|
||||
return scopes.where(scope => {
|
||||
return scope.scope ? scope.scope.id === scope_id : false
|
||||
});
|
||||
});
|
||||
this.post('/scopes', function ({ scopes }, request) {
|
||||
// Parent scope comes through the payload via `scope_id`, but this needs
|
||||
// to be expanded to a scope object `scope: {id:..., type:...}` before
|
||||
// saving, which is the gist of this function.
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
const parentScopeAttrs = this.serialize(scopes.find(request.queryParams.scope_id));
|
||||
attrs.scope = parentScopeAttrs;
|
||||
return scopes.create(attrs);
|
||||
});
|
||||
// To simulate a possible real-world case, org scopes are not returned,
|
||||
// but project scopes are.
|
||||
// TODO this should be expanded to support other scenarios.
|
||||
this.get('/scopes/:id', ({ scopes }, { params: { id: scopeID } }) => {
|
||||
const scope = scopes.find(scopeID);
|
||||
if (scope.type !== 'project') {
|
||||
return new Response(404, {}, {});
|
||||
} else {
|
||||
return scope;
|
||||
}
|
||||
});
|
||||
this.patch('/scopes/:id');
|
||||
this.del('/scopes/:id');
|
||||
|
||||
// Auth & IAM resources
|
||||
|
||||
this.get('/orgs/:org_id/auth-methods');
|
||||
this.post('/orgs/:org_id/auth-methods');
|
||||
this.get('/orgs/:org_id/auth-methods/:id');
|
||||
this.patch('/orgs/:org_id/auth-methods/:id');
|
||||
this.del('/orgs/:org_id/auth-methods/:id');
|
||||
this.get('/scopes/:scope_id/auth-methods');
|
||||
this.post('/scopes/:scope_id/auth-methods');
|
||||
this.get('/scopes/:scope_id/auth-methods/:id');
|
||||
this.patch('/scopes/:scope_id/auth-methods/:id');
|
||||
this.del('/scopes/:scope_id/auth-methods/:id');
|
||||
|
||||
// IAM : Users
|
||||
this.get('/orgs/:org_id/users');
|
||||
this.post('/orgs/:org_id/users');
|
||||
this.get('/orgs/:org_id/users/:id');
|
||||
this.patch('/orgs/:org_id/users/:id');
|
||||
this.del('/orgs/:org_id/users/:id');
|
||||
|
||||
this.get('/scopes/:scope_id/users');
|
||||
this.post('/scopes/:scope_id/users');
|
||||
this.get('/scopes/:scope_id/users/:id');
|
||||
this.patch('/scopes/:scope_id/users/:id');
|
||||
this.del('/scopes/:scope_id/users/:id');
|
||||
|
||||
// IAM: Roles
|
||||
this.get('/orgs/:org_id/roles');
|
||||
this.post('/orgs/:org_id/roles');
|
||||
this.get('/orgs/:org_id/roles/:id');
|
||||
this.patch('/orgs/:org_id/roles/:id');
|
||||
this.del('/orgs/:org_id/roles/:id');
|
||||
this.get('/scopes/:scope_id/roles');
|
||||
this.post('/scopes/:scope_id/roles');
|
||||
this.get('/scopes/:scope_id/roles/:id');
|
||||
this.patch('/scopes/:scope_id/roles/:id');
|
||||
this.del('/scopes/:scope_id/roles/:id');
|
||||
|
||||
// group
|
||||
this.get('/orgs/:org_id/groups');
|
||||
this.post('/orgs/:org_id/groups');
|
||||
this.get('/orgs/:org_id/groups/:id');
|
||||
this.patch('/orgs/:org_id/groups/:id');
|
||||
this.del('/orgs/:org_id/groups/:id');
|
||||
this.get('/scopes/:scope_id/groups');
|
||||
this.post('/scopes/:scope_id/groups');
|
||||
this.get('/scopes/:scope_id/groups/:id');
|
||||
this.patch('/scopes/:scope_id/groups/:id');
|
||||
this.del('/scopes/:scope_id/groups/:id');
|
||||
|
||||
// Other resources
|
||||
// host-catalog
|
||||
|
||||
this.get('/orgs/:org_id/projects/:project_id/host-catalogs');
|
||||
this.post('/orgs/:org_id/projects/:project_id/host-catalogs');
|
||||
this.get('/orgs/:org_id/projects/:project_id/host-catalogs/:id');
|
||||
this.patch('/orgs/:org_id/projects/:project_id/host-catalogs/:id');
|
||||
this.del('/orgs/:org_id/projects/:project_id/host-catalogs/:id');
|
||||
this.get('/scopes/:scope_id/host-catalogs');
|
||||
this.post('/scopes/:scope_id/host-catalogs');
|
||||
this.get('/scopes/:scope_id/host-catalogs/:id');
|
||||
this.patch('/scopes/:scope_id/host-catalogs/:id');
|
||||
this.del('/scopes/:scope_id/host-catalogs/:id');
|
||||
|
||||
// Uncomment the following line and the Response import above
|
||||
// Then change the response code to simulate error responses.
|
||||
// this.get('/orgs/:org_id/projects', () => new Response(505));
|
||||
// this.get('/scopes/:scope_id/projects', () => new Response(505));
|
||||
|
||||
// Update error payload to simulate specific error responses.
|
||||
// this.get('/orgs/:org_id/projects', () => new Response(505, {}, {
|
||||
// this.get('/scopes/:scope_id/projects', () => new Response(505, {}, {
|
||||
// errors: [{
|
||||
// status: 505,
|
||||
// message: 'HTTP version not supported.',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import factory from '../generated/factories/scope';
|
||||
import { trait } from 'ember-cli-mirage';
|
||||
|
||||
export default factory.extend({
|
||||
|
||||
type: 'global',
|
||||
|
||||
withChildren: trait({
|
||||
afterCreate(record, server) {
|
||||
if (record.type === 'global') {
|
||||
server.createList('scope', 1, {
|
||||
type: 'org',
|
||||
scope: { id: record.id, type: record.type }
|
||||
}, 'withChildren');
|
||||
}
|
||||
if (record.type === 'org') {
|
||||
server.createList('scope', 5, {
|
||||
type: 'project',
|
||||
scope: { id: record.id, type: record.type }
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedScopeModelFactory
|
||||
*/
|
||||
export default Factory.extend({
|
||||
|
||||
name: () => random.words(),
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
|
||||
});
|
||||
@@ -2,8 +2,7 @@ export default function(server) {
|
||||
|
||||
// Scope resources
|
||||
|
||||
server.createList('org', 3);
|
||||
server.createList('project', 6);
|
||||
server.create('scope', null, 'withChildren');
|
||||
|
||||
// Auth
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'scope'
|
||||
});
|
||||
@@ -54,6 +54,7 @@
|
||||
"ember-cli-template-lint": "^1.0.0-beta.3",
|
||||
"ember-cli-uglify": "^3.0.0",
|
||||
"ember-data": "~3.16.0",
|
||||
"ember-data-model-fragments": "5.0.0-beta.0",
|
||||
"ember-exam": "^5.0.1",
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
"ember-fetch": "^7.0.0",
|
||||
|
||||
@@ -9,41 +9,65 @@ module('Acceptance | projects', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
let org;
|
||||
let orgID;
|
||||
let projectsURL;
|
||||
let newProjectURL;
|
||||
let existingProject;
|
||||
let existingProjectURL;
|
||||
let getProjectScopesCount;
|
||||
let getFirstProjectScope;
|
||||
let initialProjectScopesCount;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.server.create('org');
|
||||
org = this.server.create('scope', {type: 'org'}, 'withChildren');
|
||||
getProjectScopesCount = () => this.server.schema.scopes.where({type: 'project'}).length;
|
||||
getFirstProjectScope = () => this.server.schema.scopes.where({type: 'project'}).models[0];
|
||||
initialProjectScopesCount = getProjectScopesCount();
|
||||
orgID = org.id;
|
||||
projectsURL = `/scopes/${orgID}/projects`;
|
||||
newProjectURL = `/scopes/${orgID}/projects/new`;
|
||||
existingProject = getFirstProjectScope();
|
||||
existingProjectURL = `/scopes/${orgID}/projects/${existingProject.id}`;
|
||||
});
|
||||
|
||||
test('visiting projects', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.createList('project', 1);
|
||||
await visit('/orgs/1/projects');
|
||||
await visit(projectsURL);
|
||||
await a11yAudit();
|
||||
assert.equal(currentURL(), '/orgs/1/projects');
|
||||
assert.equal(currentURL(), projectsURL);
|
||||
});
|
||||
|
||||
test('visiting projects within a project scope redirects to the parent org scope', async function (assert) {
|
||||
assert.expect(2);
|
||||
const wrongProjectsURL = `/scopes/${existingProject.id}/projects`;
|
||||
assert.notEqual(wrongProjectsURL, projectsURL);
|
||||
await visit(wrongProjectsURL);
|
||||
await a11yAudit();
|
||||
assert.equal(currentURL(), projectsURL, 'Wrong projects path was redirected to correct path.');
|
||||
});
|
||||
|
||||
test('can create new projects', async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.equal(this.server.db.projects.length, 0);
|
||||
await visit('/orgs/1/projects/new');
|
||||
assert.expect(1);
|
||||
await visit(newProjectURL);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[type="submit"]');
|
||||
assert.equal(currentURL(), '/orgs/1/projects/1');
|
||||
assert.equal(this.server.db.projects.length, 1);
|
||||
assert.equal(getProjectScopesCount(), initialProjectScopesCount + 1);
|
||||
});
|
||||
|
||||
test('can cancel create new projects', async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.equal(this.server.db.projects.length, 0);
|
||||
await visit('/orgs/1/projects/new');
|
||||
assert.equal(getProjectScopesCount(), initialProjectScopesCount);
|
||||
await visit(newProjectURL);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
assert.equal(currentURL(), '/orgs/1/projects');
|
||||
assert.equal(this.server.db.projects.length, 0);
|
||||
assert.equal(currentURL(), projectsURL);
|
||||
assert.equal(getProjectScopesCount(), initialProjectScopesCount);
|
||||
});
|
||||
|
||||
test('saving a new project with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.post('/orgs/:org_id/projects', () => {
|
||||
this.server.post('/scopes', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
@@ -62,8 +86,7 @@ module('Acceptance | projects', function (hooks) {
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit('/orgs/1/projects/new');
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await visit(newProjectURL);
|
||||
await click('[type="submit"]');
|
||||
assert.ok(
|
||||
find('[role="alert"]').textContent.trim(),
|
||||
@@ -78,37 +101,34 @@ module('Acceptance | projects', function (hooks) {
|
||||
});
|
||||
|
||||
test('can save changes to existing project', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.createList('project', 1, {name: 'MyProject'});
|
||||
await visit('/orgs/1/projects/1');
|
||||
assert.expect(3);
|
||||
assert.notEqual(existingProject.name, 'random string');
|
||||
await visit(existingProjectURL);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="submit"]');
|
||||
assert.equal(currentURL(), '/orgs/1/projects/1');
|
||||
assert.equal(this.server.db.projects[0].name, 'random string');
|
||||
assert.equal(currentURL(), existingProjectURL);
|
||||
assert.equal(getFirstProjectScope().name, 'random string');
|
||||
});
|
||||
|
||||
test('can cancel changes to existing project', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.createList('project', 1, {name: 'MyProject'});
|
||||
await visit('/orgs/1/projects/1');
|
||||
assert.expect(2);
|
||||
await visit(existingProjectURL);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
assert.equal(find('[name="name"]').value, 'MyProject');
|
||||
assert.notEqual(existingProject.name, 'random string');
|
||||
assert.equal(find('[name="name"]').value, existingProject.name);
|
||||
});
|
||||
|
||||
test('can delete project', async function(assert) {
|
||||
assert.expect(2);
|
||||
this.server.createList('project', 1);
|
||||
assert.equal(this.server.db.projects.length, 1);
|
||||
await visit('/orgs/1/projects/1');
|
||||
assert.expect(1);
|
||||
await visit(existingProjectURL);
|
||||
await click('.rose-button-warning');
|
||||
assert.equal(this.server.db.projects.length, 0);
|
||||
assert.equal(getProjectScopesCount(), initialProjectScopesCount - 1);
|
||||
});
|
||||
|
||||
test('saving an existing project with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.createList('project', 1);
|
||||
this.server.patch('/orgs/:org_id/projects/:id', () => {
|
||||
this.server.patch('/scopes/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
@@ -127,7 +147,7 @@ module('Acceptance | projects', function (hooks) {
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit('/orgs/1/projects/1');
|
||||
await visit(existingProjectURL);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[type="submit"]');
|
||||
assert.ok(
|
||||
@@ -144,8 +164,7 @@ module('Acceptance | projects', function (hooks) {
|
||||
|
||||
test('errors are displayed when save project fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.createList('project', 1);
|
||||
this.server.patch('/orgs/:org_id/projects/:id', () => {
|
||||
this.server.patch('/scopes/:id', () => {
|
||||
return new Response(
|
||||
490,
|
||||
{},
|
||||
@@ -156,7 +175,7 @@ module('Acceptance | projects', function (hooks) {
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit('/orgs/1/projects/1');
|
||||
await visit(existingProjectURL);
|
||||
await fillIn('[name="name"]', 'random string');
|
||||
await click('[type="submit"]');
|
||||
assert.ok(
|
||||
@@ -168,8 +187,7 @@ module('Acceptance | projects', function (hooks) {
|
||||
|
||||
test('errors are displayed when delete project fails', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.createList('project', 1);
|
||||
this.server.del('/orgs/:org_id/projects/:id', () => {
|
||||
this.server.del('/scopes/:id', () => {
|
||||
return new Response(
|
||||
490,
|
||||
{},
|
||||
@@ -180,7 +198,7 @@ module('Acceptance | projects', function (hooks) {
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit('/orgs/1/projects/1');
|
||||
await visit(existingProjectURL);
|
||||
await click('.rose-button-warning');
|
||||
assert.ok(
|
||||
find('[role="alert"]').textContent.trim(),
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/projects', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/projects');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/projects/new', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/projects/new');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/projects/project', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/projects/project');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects/index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects/index');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects/new', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects/new');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects/project', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects/project');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects/project/index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects/project/index');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ actions:
|
||||
help: Need help?
|
||||
resources:
|
||||
user: User
|
||||
users: Users
|
||||
users: Users
|
||||
role: Role
|
||||
roles: Roles
|
||||
group: Group
|
||||
@@ -18,6 +18,7 @@ resources:
|
||||
titles:
|
||||
iam: IAM
|
||||
new: New
|
||||
details: Details
|
||||
general: General
|
||||
overview: Overview
|
||||
settings: Settings
|
||||
|
||||
Reference in new issue
Block a user