From d501ad4bb37bd9b7cf484c845afca324867c7f0e Mon Sep 17 00:00:00 2001 From: Paul Tannenbaum Date: Tue, 15 Dec 2015 18:03:23 -0800 Subject: [PATCH] Search Schema model, associations, and views --- app/adapters/search-index.js | 2 +- app/pods/cluster/model.js | 7 ++ app/pods/cluster/template.hbs | 4 +- app/pods/search-index/model.js | 36 ++++---- app/pods/search-index/template.hbs | 4 +- app/pods/search-schema/edit/model.js | 5 + app/pods/search-schema/edit/route.js | 55 +++++++++++ app/pods/search-schema/edit/template.hbs | 28 ++++++ app/pods/search-schema/model.js | 28 ++++++ app/pods/search-schema/route.js | 23 +++++ app/pods/search-schema/template.hbs | 21 +++++ app/router.js | 2 + app/routes/application.js | 2 +- app/serializers/search-index.js | 8 ++ app/services/explorer.js | 92 +++++++++++++------ app/styles/app.scss | 1 + app/styles/components/_schema-actions.scss | 22 +++++ app/templates/components/search-indexes.hbs | 6 +- package.json | 3 +- tests/unit/models/cluster-test.js | 10 +- tests/unit/models/search-index-test.js | 11 ++- tests/unit/models/search-schema-test.js | 24 +++++ .../pods/search-schema/edit/model-test.js | 12 +++ .../pods/search-schema/edit/route-test.js | 11 +++ tests/unit/serializers/search-index-test.js | 2 +- 25 files changed, 359 insertions(+), 60 deletions(-) create mode 100644 app/pods/search-schema/edit/model.js create mode 100644 app/pods/search-schema/edit/route.js create mode 100644 app/pods/search-schema/edit/template.hbs create mode 100644 app/pods/search-schema/model.js create mode 100644 app/pods/search-schema/route.js create mode 100644 app/pods/search-schema/template.hbs create mode 100644 app/styles/components/_schema-actions.scss create mode 100644 tests/unit/models/search-schema-test.js create mode 100644 tests/unit/pods/search-schema/edit/model-test.js create mode 100644 tests/unit/pods/search-schema/edit/route-test.js diff --git a/app/adapters/search-index.js b/app/adapters/search-index.js index ea0a552..9fe3ca7 100644 --- a/app/adapters/search-index.js +++ b/app/adapters/search-index.js @@ -10,7 +10,6 @@ var SearchIndexAdapter = DS.RESTAdapter.extend({ let url = this.buildURL(type.modelName, null, null, 'query', query); let promise = this.ajax(url, 'GET').then(function(indexes) { - indexes.forEach(function(index) { index.id = `${query.clusterId}/${index.name}`; }); @@ -23,3 +22,4 @@ var SearchIndexAdapter = DS.RESTAdapter.extend({ }); export default SearchIndexAdapter; + diff --git a/app/pods/cluster/model.js b/app/pods/cluster/model.js index e686283..1c0ecbf 100644 --- a/app/pods/cluster/model.js +++ b/app/pods/cluster/model.js @@ -32,6 +32,13 @@ var Cluster = DS.Model.extend({ */ searchIndexes: DS.hasMany('search-index', { async: true }), + /** + * Search schemas created on the cluster + * @property searchSchemas + * @type Array + */ + searchSchemas: DS.hasMany('search-schema', { async: true }), + /** * Is this cluster in Dev Mode? Set in the Explorer config file. * Dev mode allows expensive operations like list keys, delete bucket, etc. diff --git a/app/pods/cluster/template.hbs b/app/pods/cluster/template.hbs index 02fbf01..55126e6 100644 --- a/app/pods/cluster/template.hbs +++ b/app/pods/cluster/template.hbs @@ -59,9 +59,7 @@ {{#dashboard-module label='Search Indexes'}} {{#if model.searchIndexes}} - {{search-indexes - indexes=model.searchIndexes - clusterProxyUrl=model.proxyUrl}} + {{search-indexes indexes=model.searchIndexes}} {{else}}

No search indexes found

{{/if}} diff --git a/app/pods/search-index/model.js b/app/pods/search-index/model.js index 69a7fe3..973c5ef 100644 --- a/app/pods/search-index/model.js +++ b/app/pods/search-index/model.js @@ -2,13 +2,22 @@ import DS from 'ember-data'; var SearchIndex = DS.Model.extend({ /** - * Riak cluster in the search index wascreated on + * Riak cluster the search index was created on * * @property cluster * @type {DS.Model} Cluster * @writeOnce */ - cluster: DS.belongsTo('cluster'), + cluster: DS.belongsTo('cluster', { async: true }), + + /** + * Schema the search index is using + * + * @property schema + * @type {DS.Model} Search Schema + * @writeOnce + */ + schema: DS.belongsTo('search-schema', { async: true }), /** * Returns the search index name/id @@ -25,11 +34,12 @@ var SearchIndex = DS.Model.extend({ nVal: DS.attr('number', {defaultValue: 3}), /** - * Name of the schema the index is using - * @property schema - * @type String + * Holds the value of the schema name that index is using. + * Temporary hack until basho-labs/riak_explorer#89 is completed + * @property nVal + * @type Integer */ - schema: DS.attr('string'), + schemaRef: DS.attr('string'), /** * Ember.Array of bucket types on the current cluster using the index @@ -40,19 +50,7 @@ var SearchIndex = DS.Model.extend({ let bucketTypes = this.get('cluster').get('bucketTypes'); return bucketTypes.filterBy('index.name', this.get('name')); - }.property('cluster.bucketTypes'), - - /** - * Returns a formatted schema url - * @property schemaUrl - * @type String - */ - schemaUrl: function() { - let proxyURL = this.get('cluster').get('proxyUrl'); - let schema = this.get('schema'); - - return `${proxyURL}/search/schema/${schema}`; - }.property('schema', 'cluster.proxyUrl') + }.property('cluster.bucketTypes') }); export default SearchIndex; diff --git a/app/pods/search-index/template.hbs b/app/pods/search-index/template.hbs index 56bda36..16b2da7 100644 --- a/app/pods/search-index/template.hbs +++ b/app/pods/search-index/template.hbs @@ -22,7 +22,9 @@ Schema - {{model.schema}} + {{#link-to 'search-schema' model.cluster.id model.schema.name}} + {{model.schema.name}} + {{/link-to}} diff --git a/app/pods/search-schema/edit/model.js b/app/pods/search-schema/edit/model.js new file mode 100644 index 0000000..ca6bd1b --- /dev/null +++ b/app/pods/search-schema/edit/model.js @@ -0,0 +1,5 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + +}); diff --git a/app/pods/search-schema/edit/route.js b/app/pods/search-schema/edit/route.js new file mode 100644 index 0000000..ea8c1ce --- /dev/null +++ b/app/pods/search-schema/edit/route.js @@ -0,0 +1,55 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + model(params) { + return this.explorer.getCluster(params.clusterId, this.store) + .then(function(cluster){ + return cluster.get('searchSchemas').findBy('name', params.searchSchemaId); + }); + }, + + afterModel(model, transition) { + if (!model.get('content')) { + return Ember.$.ajax({ + type: 'GET', + url: model.get('url'), + dataType: 'xml' + }).then(function(data) { + let xmlString = (new XMLSerializer()).serializeToString(data); + model.set('content', xmlString); + }); + } + }, + + actions: { + updateSchema: function(schema) { + let xmlString = schema.get('content'); + let self = this; + let xmlDoc = null; + let clusterId = schema.get('cluster').get('id'); + let schemaId = schema.get('name'); + + try { + xmlDoc = Ember.$.parseXML(xmlString); + } catch(error) { + // TODO: Put in proper error messaging + alert('Invalid XML. Please check and make sure schema is valid xml.'); + return; + } + + return Ember.$.ajax({ + type: 'PUT', + url: schema.get('url'), + contentType: 'application/xml', + processData: false, + data: xmlDoc + }).then(function(data) { + self.transitionTo('search-schema', clusterId, schemaId); + }, function(error) { + // TODO: Put in proper error messaging + alert('Something went wrong, schema was not saved.'); + self.transitionTo('search-schema', clusterId, schemaId); + }); + } + } +}); diff --git a/app/pods/search-schema/edit/template.hbs b/app/pods/search-schema/edit/template.hbs new file mode 100644 index 0000000..74e89b7 --- /dev/null +++ b/app/pods/search-schema/edit/template.hbs @@ -0,0 +1,28 @@ +
+ {{breadcrumb-component + clusterId=model.cluster.id + pageTitle=model.name + }} + {{view-label + pre-label='Search Schema' + label=model.name}} +
+ +{{#dashboard-module}} +
+ + + Update Schema + + + {{#link-to 'search-schema' model.cluster.id model.name class='cancel schema-action' }} + + Cancel + {{/link-to}} +
+ + {{content-editable + value=model.content + type="html" + tagName="pre"}} +{{/dashboard-module}} diff --git a/app/pods/search-schema/model.js b/app/pods/search-schema/model.js new file mode 100644 index 0000000..26622b1 --- /dev/null +++ b/app/pods/search-schema/model.js @@ -0,0 +1,28 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + /** + * Riak cluster the search schema was created on + * + * @property cluster + * @type {DS.Model} Cluster + * @writeOnce + */ + cluster: DS.belongsTo('cluster', { async: true }), + + name: DS.attr('string'), + + content: DS.attr(), + + /** + * Returns a formatted schema url + * @method url + * @returns String + */ + url: function() { + let proxyURL = this.get('cluster').get('proxyUrl'); + let name = this.get('name'); + + return `${proxyURL}/search/schema/${name}`; + }.property('name', 'cluster.proxyUrl') +}); diff --git a/app/pods/search-schema/route.js b/app/pods/search-schema/route.js new file mode 100644 index 0000000..af3d837 --- /dev/null +++ b/app/pods/search-schema/route.js @@ -0,0 +1,23 @@ +import Ember from 'ember'; +import $ from 'jquery'; + +export default Ember.Route.extend({ + model(params) { + return this.explorer.getCluster(params.clusterId, this.store) + .then(function(cluster){ + return cluster.get('searchSchemas').findBy('name', params.searchSchemaId); + }); + }, + + // TODO: Move to init??? + afterModel(model, transition) { + return Ember.$.ajax({ + type: 'GET', + url: model.get('url'), + dataType: 'xml' + }).then(function(data) { + let xmlString = (new XMLSerializer()).serializeToString(data); + model.set('content', xmlString); + }); + } +}); diff --git a/app/pods/search-schema/template.hbs b/app/pods/search-schema/template.hbs new file mode 100644 index 0000000..fe182bb --- /dev/null +++ b/app/pods/search-schema/template.hbs @@ -0,0 +1,21 @@ +
+ {{breadcrumb-component + clusterId=model.cluster.id + pageTitle=model.name + }} + {{view-label + pre-label='Search Schema' + label=model.name}} +
+ +{{#dashboard-module}} +
+ {{#link-to 'search-schema.edit' model.cluster.id model.name class='edit schema-action' }} + + Edit Schema + {{/link-to}} +
+
+        {{model.content}}
+    
+{{/dashboard-module}} diff --git a/app/router.js b/app/router.js index 75decf5..8a74cc3 100644 --- a/app/router.js +++ b/app/router.js @@ -30,4 +30,6 @@ export default Router.map(function() { this.route('service-not-found'); }); this.route('search-index', { path: '/cluster/:clusterId/index/:searchIndexId' }); + this.route('search-schema', { path: '/cluster/:clusterId/schema/:searchSchemaId' }); + this.route('search-schema.edit', { path: '/cluster/:clusterId/schema/:searchSchemaId/edit' }); }); diff --git a/app/routes/application.js b/app/routes/application.js index f1de7b3..5f0431f 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -5,7 +5,7 @@ export default Ember.Route.extend({ error: function(error) { // An error has occurred that wasn't handled by any route. console.log('Unknown error: %O', error); - this.transitionTo('errors.unknown'); + this.transitionTo('error.unknown'); } }, diff --git a/app/serializers/search-index.js b/app/serializers/search-index.js index 09fe057..5f2358d 100644 --- a/app/serializers/search-index.js +++ b/app/serializers/search-index.js @@ -7,5 +7,13 @@ export default ApplicationSerializer.extend({ }; return this._super(store, primaryModelClass, newPayload, id, requestType); + }, + + // TODO: Remove once basho-labs/riak_explorer#89 is completed + normalize(modelClass, resourceHash, prop) { + resourceHash.schema_ref = resourceHash.schema; + delete resourceHash.schema; + + return this._super(modelClass, resourceHash, prop); } }); diff --git a/app/services/explorer.js b/app/services/explorer.js index d753aa8..d527277 100644 --- a/app/services/explorer.js +++ b/app/services/explorer.js @@ -88,15 +88,15 @@ export default Ember.Service.extend({ }, /** - * Refreshes a key list cache or bucket list cache on the Explorer API side. - * Usually invoked when the user presses the 'Refresh List' button on the UI. - * @see bucketCacheRefresh - * @see keyCacheRefresh - * - * @method cacheRefresh - * @param {String} url - * @return Ember.RSVP.Promise - */ + * Refreshes a key list cache or bucket list cache on the Explorer API side. + * Usually invoked when the user presses the 'Refresh List' button on the UI. + * @see bucketCacheRefresh + * @see keyCacheRefresh + * + * @method cacheRefresh + * @param {String} url + * @return Ember.RSVP.Promise + */ cacheRefresh(url) { return new Ember.RSVP.Promise(function(resolve, reject) { Ember.$.ajax({ @@ -197,7 +197,7 @@ export default Ember.Service.extend({ // This `field` becomes the `parentMap` for the nested fields. // `rootMap` stays the same let mapFields = this.collectMapFields(rootMap, field, - payload[fieldName], store); + payload[fieldName], store); field.value = mapFields; contents.maps[fieldName] = field; } @@ -293,6 +293,30 @@ export default Ember.Service.extend({ }); }, + /** + * Creates a Schema instance if it does not exist, + * and the returns instance. + * + * @method createSchema + * @param name {String} + * @param cluster {Cluster} + * @param store {DS.Store} + * @return {Schema} + */ + createSchema(name, cluster, store) { + let schema = cluster.get('searchSchemas').findBy('name', name); + + if (!schema) { + schema = store.createRecord('search-schema', { + id: `${cluster.get('id')}/${name}`, + cluster: cluster, + name: name + }); + } + + return schema; + }, + /** * Parses and returns the contents/value of a Riak Object, depending on * whether it's a CRDT or a plain object. @@ -791,11 +815,11 @@ export default Ember.Service.extend({ */ getBucketTypeWithBucketList(bucketType, cluster, store, start, row) { return this - .getBucketList(cluster, bucketType, store, start, row) - .then(function(bucketList) { - bucketType.set('bucketList', bucketList); - return bucketType; - }); + .getBucketList(cluster, bucketType, store, start, row) + .then(function(bucketList) { + bucketType.set('bucketList', bucketList); + return bucketType; + }); }, /** @@ -815,7 +839,7 @@ export default Ember.Service.extend({ // (via a bookmark and not from a link), bucket types are likely // to be not loaded yet. Load them. return store.query('bucket-type', - {clusterId: cluster.get('clusterId')}) + {clusterId: cluster.get('clusterId')}) .then(function(bucketTypes) { cluster.set('bucketTypes', bucketTypes); return bucketTypes; @@ -866,6 +890,14 @@ export default Ember.Service.extend({ .then(function(PromiseArray) { let cluster = PromiseArray[0].value; + // Create search-schemas from index references + // and set the schema/index association + cluster.get('searchIndexes').forEach(function(index) { + let schema = self.createSchema(index.get('schemaRef'), cluster, store); + + index.set('schema', schema); + }); + return cluster; }); }, @@ -1175,20 +1207,20 @@ export default Ember.Service.extend({ // if the header value has the string ": " in it. var index = headerLine.indexOf(': '); if (index > 0) { - var key = headerLine.substring(0, index).toLowerCase(); - var val = headerLine.substring(index + 2); - var header = { - key: key, - value: val - }; - - if(key.startsWith('x-riak-meta')) { - custom.push(header); - } else if(key.startsWith('x-riak-index')) { - indexes.push(header); - } else { - other_headers[key] = val; - } + var key = headerLine.substring(0, index).toLowerCase(); + var val = headerLine.substring(index + 2); + var header = { + key: key, + value: val + }; + + if(key.startsWith('x-riak-meta')) { + custom.push(header); + } else if(key.startsWith('x-riak-index')) { + indexes.push(header); + } else { + other_headers[key] = val; + } } } return { diff --git a/app/styles/app.scss b/app/styles/app.scss index ec26969..fd4b342 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -24,6 +24,7 @@ @import "components/button-list"; @import "components/cluster-resource-link"; @import "components/pagination-component"; +@import "components/schema-actions"; // View specific styling @import "views/object-counter-container"; diff --git a/app/styles/components/_schema-actions.scss b/app/styles/components/_schema-actions.scss new file mode 100644 index 0000000..2fecfd9 --- /dev/null +++ b/app/styles/components/_schema-actions.scss @@ -0,0 +1,22 @@ +.schema-actions { + text-align: right; + margin-bottom: 10px; + + .schema-action { + @extend .btn; + @extend .btn-sm; + margin-left: 10px; + } + + .edit { + @extend .btn-primary; + } + + .cancel { + @extend .btn-danger; + } + + .update { + @extend .btn-primary; + } +} diff --git a/app/templates/components/search-indexes.hbs b/app/templates/components/search-indexes.hbs index 9d7eb45..5778a0f 100644 --- a/app/templates/components/search-indexes.hbs +++ b/app/templates/components/search-indexes.hbs @@ -10,7 +10,11 @@ {{#each indexes as |index|}} {{link.link-index searchIndex=index}} - {{index.schema}} + + {{#link-to 'search-schema' index.cluster.id index.schema.name}} + {{index.schema.name}} + {{/link-to}} + {{index.nVal}} {{else}} diff --git a/package.json b/package.json index ac4c4ef..f8f06b8 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "ember-cli-sass": "^5.1.0", - "ember-idx-tabs": "^0.1.4" + "ember-idx-tabs": "^0.1.4", + "ember-content-editable": "0.7.0" } } diff --git a/tests/unit/models/cluster-test.js b/tests/unit/models/cluster-test.js index ae8c57f..5344832 100644 --- a/tests/unit/models/cluster-test.js +++ b/tests/unit/models/cluster-test.js @@ -2,7 +2,7 @@ import { moduleForModel, test, pending } from 'ember-qunit'; import Ember from 'ember'; moduleForModel('cluster', 'Unit | Model | cluster', { - needs: ['model:bucketType', 'model:riakNode', 'model:searchIndex'] + needs: ['model:bucketType', 'model:riakNode', 'model:searchIndex', 'model:searchSchema'] }); test('it exists', function(assert) { @@ -37,6 +37,14 @@ test('searchIndexes relationship', function(assert) { assert.equal(relationship.kind, 'hasMany'); }); +test('searchSchemas relationship', function(assert) { + let klass = this.subject({}).constructor; + let relationship = Ember.get(klass, 'relationshipsByName').get('searchSchemas'); + + assert.equal(relationship.key, 'searchSchemas'); + assert.equal(relationship.kind, 'hasMany'); +}); + pending('getting active bucket types', function() {}); pending('getting inactive bucket types', function() {}); diff --git a/tests/unit/models/search-index-test.js b/tests/unit/models/search-index-test.js index e1e5f98..05d0854 100644 --- a/tests/unit/models/search-index-test.js +++ b/tests/unit/models/search-index-test.js @@ -2,7 +2,7 @@ import { moduleForModel, test } from 'ember-qunit'; import Ember from 'ember'; moduleForModel('search-index', 'Unit | Model | search index', { - needs: ['model:cluster'] + needs: ['model:cluster', 'model:searchSchema'] }); test('it exists', function(assert) { @@ -21,3 +21,12 @@ test('cluster relationship', function (assert) { assert.equal(relationship.kind, 'belongsTo'); }); + +test('schema relationship', function (assert) { + let klass = this.subject({}).constructor; + let relationship = Ember.get(klass, 'relationshipsByName').get('schema'); + + assert.equal(relationship.key, 'schema'); + + assert.equal(relationship.kind, 'belongsTo'); +}); diff --git a/tests/unit/models/search-schema-test.js b/tests/unit/models/search-schema-test.js new file mode 100644 index 0000000..466c271 --- /dev/null +++ b/tests/unit/models/search-schema-test.js @@ -0,0 +1,24 @@ +import { moduleForModel, test } from 'ember-qunit'; +import Ember from 'ember'; + +moduleForModel('search-schema', 'Unit | Model | search schema', { + // Specify the other units that are required for this test. + needs: ['model:cluster'] +}); + +test('it exists', function(assert) { + let model = this.subject(); + let store = this.store(); + + assert.ok(!!model); + assert.ok(!!store); +}); + +test('cluster relationship', function (assert) { + let klass = this.subject({}).constructor; + let relationship = Ember.get(klass, 'relationshipsByName').get('cluster'); + + assert.equal(relationship.key, 'cluster'); + + assert.equal(relationship.kind, 'belongsTo'); +}); diff --git a/tests/unit/pods/search-schema/edit/model-test.js b/tests/unit/pods/search-schema/edit/model-test.js new file mode 100644 index 0000000..603aee2 --- /dev/null +++ b/tests/unit/pods/search-schema/edit/model-test.js @@ -0,0 +1,12 @@ +import { moduleForModel, test } from 'ember-qunit'; + +moduleForModel('search-schema/edit', 'Unit | Model | search schema/edit', { + // Specify the other units that are required for this test. + needs: [] +}); + +test('it exists', function(assert) { + var model = this.subject(); + // var store = this.store(); + assert.ok(!!model); +}); diff --git a/tests/unit/pods/search-schema/edit/route-test.js b/tests/unit/pods/search-schema/edit/route-test.js new file mode 100644 index 0000000..31b91c6 --- /dev/null +++ b/tests/unit/pods/search-schema/edit/route-test.js @@ -0,0 +1,11 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('route:search-schema/edit', 'Unit | Route | search schema/edit', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it exists', function(assert) { + var route = this.subject(); + assert.ok(route); +}); diff --git a/tests/unit/serializers/search-index-test.js b/tests/unit/serializers/search-index-test.js index 8850c4a..58730e3 100644 --- a/tests/unit/serializers/search-index-test.js +++ b/tests/unit/serializers/search-index-test.js @@ -2,7 +2,7 @@ import { moduleForModel, test } from 'ember-qunit'; moduleForModel('search-index', 'Unit | Serializer | search index', { // Specify the other units that are required for this test. - needs: ['serializer:search-index'] + needs: ['serializer:search-index', 'model:search-schema'] }); // Replace this with your real tests.