diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 61566937f..6dbe00b3b 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -7,6 +7,11 @@ export default class Router extends EmberRouter { } Router.map(function () { + + this.route('scopes', function() { + this.route('scope', { path: ':scope_id' }); + }); + this.route('orgs', function () { this.route('org', { path: ':org_id' }, function () { this.route('projects', function () { @@ -23,7 +28,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'); diff --git a/ui/core/app/routes/scopes.js b/ui/core/app/routes/scopes.js new file mode 100644 index 000000000..5fff74b2e --- /dev/null +++ b/ui/core/app/routes/scopes.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class ScopesRoute extends Route { +} diff --git a/ui/core/app/routes/scopes/scope.js b/ui/core/app/routes/scopes/scope.js new file mode 100644 index 000000000..68201166e --- /dev/null +++ b/ui/core/app/routes/scopes/scope.js @@ -0,0 +1,26 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeRoute extends Route { + + /** + * Attempts 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 its resources. + * 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 maybeExisting = this.store.peekRecord('scope', id); + // TODO it's unclear if this is reliable... + // we may need to revert to a POJO. + return maybeExisting || this.store.createRecord('scope', { id }); + } + } + +} diff --git a/ui/core/app/templates/scopes.hbs b/ui/core/app/templates/scopes.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope.hbs b/ui/core/app/templates/scopes/scope.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes/scope.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 781a607d5..421f87a8f 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,5 +1,5 @@ import config from '../config/environment'; -// import { Response } from 'miragejs'; +import { Response } from 'miragejs'; export default function() { @@ -16,66 +16,66 @@ 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.post('/scopes'); + // 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.', diff --git a/ui/core/mirage/factories/scope.js b/ui/core/mirage/factories/scope.js new file mode 100644 index 000000000..703e80ad4 --- /dev/null +++ b/ui/core/mirage/factories/scope.js @@ -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 } + }); + } + } + }) + +}); diff --git a/ui/core/mirage/generated/factories/scope.js b/ui/core/mirage/generated/factories/scope.js new file mode 100644 index 000000000..cc574d3cd --- /dev/null +++ b/ui/core/mirage/generated/factories/scope.js @@ -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(), + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index d4e4dc2b7..f143f6ec6 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -2,8 +2,7 @@ export default function(server) { // Scope resources - server.createList('org', 3); - server.createList('project', 6); + server.create('scope', null, 'withChildren'); // Auth diff --git a/ui/core/mirage/serializers/scope.js b/ui/core/mirage/serializers/scope.js new file mode 100644 index 000000000..bfb33f398 --- /dev/null +++ b/ui/core/mirage/serializers/scope.js @@ -0,0 +1,5 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + modelName: 'scope' +}); diff --git a/ui/core/package.json b/ui/core/package.json index 02bcc7ea3..f6ae93043 100644 --- a/ui/core/package.json +++ b/ui/core/package.json @@ -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", diff --git a/ui/core/tests/unit/routes/scopes-test.js b/ui/core/tests/unit/routes/scopes-test.js new file mode 100644 index 000000000..06a5af092 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes-test.js @@ -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); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope-test.js b/ui/core/tests/unit/routes/scopes/scope-test.js new file mode 100644 index 000000000..62c97d9f0 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope-test.js @@ -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); + }); +});