diff --git a/platform/commonUI/edit/src/actions/SaveAsAction.js b/platform/commonUI/edit/src/actions/SaveAsAction.js index 2972944804..aa59f12340 100644 --- a/platform/commonUI/edit/src/actions/SaveAsAction.js +++ b/platform/commonUI/edit/src/actions/SaveAsAction.js @@ -79,12 +79,6 @@ function ( return this.objectService; }; - function resolveWith(object) { - return function () { - return object; - }; - } - /** * Save changes and conclude editing. * @@ -102,7 +96,6 @@ function ( SaveAsAction.prototype.save = function () { var self = this, domainObject = this.domainObject, - copyService = this.copyService, dialog = new SaveInProgressDialog(this.dialogService), toUndirty = []; @@ -139,19 +132,22 @@ function ( return fetchObject(object.getModel().location); } - function allowClone(objectToClone) { - var allowed = - (objectToClone.getId() === domainObject.getId()) - || objectToClone.getCapability('location').isOriginal(); - if (allowed) { - toUndirty.push(objectToClone); - } - - return allowed; + function saveObject(parent) { + return self.openmct.editor.save().then(() => { + // Force mutation for search indexing + return parent; + }); } - function cloneIntoParent(parent) { - return copyService.perform(domainObject, parent, allowClone); + function addSavedObjectToParent(parent) { + return parent.getCapability("composition") + .add(domainObject) + .then(function (addedObject) { + return parent.getCapability("persistence").persist() + .then(function () { + return addedObject; + }); + }); } function undirty(object) { @@ -160,26 +156,17 @@ function ( function undirtyOriginals(object) { return Promise.all(toUndirty.map(undirty)) - .then(resolveWith(object)); + .then(() => { + return object; + }); } - function saveAfterClone(clonedObject) { - return self.openmct.editor.save().then(() => { - // Force mutation for search indexing - return clonedObject; - }); - } - - function finishEditing(clonedObject) { - return fetchObject(clonedObject.getId()); - } - - function indexForSearch(savedObject) { - savedObject.useCapability('mutation', (model) => { + function indexForSearch(addedObject) { + addedObject.useCapability('mutation', (model) => { return model; }); - return savedObject; + return addedObject; } function onSuccess(object) { @@ -201,10 +188,12 @@ function ( .then(doWizardSave) .then(showBlockingDialog) .then(getParent) - .then(cloneIntoParent) + .then(saveObject) + .then(addSavedObjectToParent) .then(undirtyOriginals) - .then(saveAfterClone) - .then(finishEditing) + .then((addedObject) => { + return fetchObject(addedObject.getId()); + }) .then(indexForSearch) .then(hideBlockingDialog) .then(onSuccess) diff --git a/src/adapter/bundle.js b/src/adapter/bundle.js index eba3bc07cd..2c659e33e5 100644 --- a/src/adapter/bundle.js +++ b/src/adapter/bundle.js @@ -35,7 +35,8 @@ define([ './services/LegacyObjectAPIInterceptor', './views/installLegacyViews', './policies/LegacyCompositionPolicyAdapter', - './actions/LegacyActionAdapter' + './actions/LegacyActionAdapter', + './services/LegacyPersistenceAdapter' ], function ( ActionDialogDecorator, AdapterCapability, @@ -51,7 +52,8 @@ define([ LegacyObjectAPIInterceptor, installLegacyViews, legacyCompositionPolicyAdapter, - LegacyActionAdapter + LegacyActionAdapter, + LegacyPersistenceAdapter ) { return { name: 'src/adapter', @@ -114,6 +116,15 @@ define([ "instantiate", "topic" ] + }, + { + provides: "persistenceService", + type: "provider", + priority: "fallback", + implementation: function legacyPersistenceProvider(openmct) { + return new LegacyPersistenceAdapter.default(openmct); + }, + depends: ["openmct"] } ], policies: [ diff --git a/src/adapter/services/LegacyPersistenceAdapter.js b/src/adapter/services/LegacyPersistenceAdapter.js new file mode 100644 index 0000000000..fdca7c3d94 --- /dev/null +++ b/src/adapter/services/LegacyPersistenceAdapter.js @@ -0,0 +1,47 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import objectUtils from 'objectUtils'; + +export default class LegacyPersistenceAdapter { + constructor(openmct) { + this.openmct = openmct; + } + + listObjects() { + return Promise.resolve([]); + } + + listSpaces() { + return Promise.resolve(Object.keys(this.openmct.objects.providers)); + } + + updateObject(legacyDomainObject) { + return this.openmct.objects.save(legacyDomainObject.useCapability('adapter')); + } + + readObject(keystring) { + let identifier = objectUtils.parseKeyString(keystring); + + return this.openmct.legacyObject(this.openmct.objects.get(identifier)); + } +} diff --git a/src/api/objects/RootObjectProvider.js b/src/api/objects/RootObjectProvider.js index 3e0d999573..7874d05f66 100644 --- a/src/api/objects/RootObjectProvider.js +++ b/src/api/objects/RootObjectProvider.js @@ -29,7 +29,7 @@ class RootObjectProvider { key: "ROOT", namespace: "" }, - name: 'The root object', + name: 'Open MCT', type: 'root', composition: [] }; diff --git a/src/api/objects/test/RootObjectProviderSpec.js b/src/api/objects/test/RootObjectProviderSpec.js index 9536f845d5..74c51fcdea 100644 --- a/src/api/objects/test/RootObjectProviderSpec.js +++ b/src/api/objects/test/RootObjectProviderSpec.js @@ -22,7 +22,7 @@ import RootObjectProvider from '../RootObjectProvider'; describe('RootObjectProvider', function () { - // let rootRegistry; + const ROOT_NAME = 'Open MCT'; let rootObjectProvider; let roots = ['some root']; let rootRegistry = { @@ -43,7 +43,7 @@ describe('RootObjectProvider', function () { key: "ROOT", namespace: "" }, - name: 'The root object', + name: ROOT_NAME, type: 'root', composition: ['some root'] }); diff --git a/src/plugins/ISOTimeFormat/ISOTimeFormat.js b/src/plugins/ISOTimeFormat/ISOTimeFormat.js new file mode 100644 index 0000000000..ebc47586b0 --- /dev/null +++ b/src/plugins/ISOTimeFormat/ISOTimeFormat.js @@ -0,0 +1,47 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +export default class ISOTimeFormat { + constructor() { + this.key = 'iso'; + } + + format(value) { + if (value !== undefined) { + return new Date(value).toISOString(); + } else { + return value; + } + } + + parse(text) { + if (typeof text === 'number' || text === undefined) { + return text; + } + + return Date.parse(text); + } + + validate(text) { + return !isNaN(Date.parse(text)); + } +} diff --git a/src/plugins/ISOTimeFormat/plugin.js b/src/plugins/ISOTimeFormat/plugin.js new file mode 100644 index 0000000000..100dd1c076 --- /dev/null +++ b/src/plugins/ISOTimeFormat/plugin.js @@ -0,0 +1,29 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import ISOTimeFormat from './ISOTimeFormat'; + +export default function () { + return function install(openmct) { + openmct.telemetry.addFormat(new ISOTimeFormat()); + }; +} diff --git a/src/plugins/ISOTimeFormat/pluginSpec.js b/src/plugins/ISOTimeFormat/pluginSpec.js new file mode 100644 index 0000000000..ad35bb4c81 --- /dev/null +++ b/src/plugins/ISOTimeFormat/pluginSpec.js @@ -0,0 +1,55 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import ISOTimeFormat from './ISOTimeFormat.js'; + +describe("the plugin", () => { + const ISO_KEY = 'iso'; + const JUNK = "junk"; + const MOON_LANDING_TIMESTAMP = -14256000000; + const MOON_LANDING_DATESTRING = '1969-07-20T00:00:00.000Z'; + let isoFormatter; + + beforeEach(() => { + isoFormatter = new ISOTimeFormat(); + }); + + describe("creates a new ISO based formatter", function () { + + it("with the key 'iso'", () => { + expect(isoFormatter.key).toBe(ISO_KEY); + }); + + it("that will format a timestamp in ISO standard format", () => { + expect(isoFormatter.format(MOON_LANDING_TIMESTAMP)).toBe(MOON_LANDING_DATESTRING); + }); + + it("that will parse an ISO Date String into milliseconds", () => { + expect(isoFormatter.parse(MOON_LANDING_DATESTRING)).toBe(MOON_LANDING_TIMESTAMP); + }); + + it("that will validate correctly", () => { + expect(isoFormatter.validate(MOON_LANDING_DATESTRING)).toBe(true); + expect(isoFormatter.validate(JUNK)).toBe(false); + }); + }); +}); diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js index fffd2b8881..de63b19d02 100644 --- a/src/plugins/condition/ConditionManager.js +++ b/src/plugins/condition/ConditionManager.js @@ -125,11 +125,17 @@ export default class ConditionManager extends EventEmitter { } } - updateCondition(conditionConfiguration, index) { - let condition = this.conditions[index]; - this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration; - condition.update(conditionConfiguration); - this.persistConditions(); + updateCondition(conditionConfiguration) { + let condition = this.findConditionById(conditionConfiguration.id); + if (condition) { + condition.update(conditionConfiguration); + } + + let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id); + if (index > -1) { + this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration; + this.persistConditions(); + } } updateConditionDescription(condition) { @@ -202,12 +208,18 @@ export default class ConditionManager extends EventEmitter { this.persistConditions(); } - removeCondition(index) { - let condition = this.conditions[index]; - condition.destroy(); - this.conditions.splice(index, 1); - this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1); - this.persistConditions(); + removeCondition(id) { + let index = this.conditions.findIndex(item => item.id === id); + if (index > -1) { + this.conditions[index].destroy(); + this.conditions.splice(index, 1); + } + + let conditionCollectionIndex = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === id); + if (conditionCollectionIndex > -1) { + this.conditionSetDomainObject.configuration.conditionCollection.splice(conditionCollectionIndex, 1); + this.persistConditions(); + } } findConditionById(id) { @@ -220,8 +232,8 @@ export default class ConditionManager extends EventEmitter { reorderPlan.forEach((reorderEvent) => { let item = oldConditions[reorderEvent.oldIndex]; newCollection.push(item); - this.conditionSetDomainObject.configuration.conditionCollection = newCollection; }); + this.conditionSetDomainObject.configuration.conditionCollection = newCollection; this.persistConditions(); } diff --git a/src/plugins/condition/ConditionManagerSpec.js b/src/plugins/condition/ConditionManagerSpec.js index 77b303d37b..2e52f06b59 100644 --- a/src/plugins/condition/ConditionManagerSpec.js +++ b/src/plugins/condition/ConditionManagerSpec.js @@ -27,13 +27,32 @@ describe('ConditionManager', () => { let conditionMgr; let mockListener; let openmct = {}; - let mockCondition = { + let mockDefaultCondition = { isDefault: true, id: '1234-5678', configuration: { criteria: [] } }; + let mockCondition1 = { + id: '2345-6789', + configuration: { + criteria: [] + } + }; + let updatedMockCondition1 = { + id: '2345-6789', + configuration: { + trigger: 'xor', + criteria: [] + } + }; + let mockCondition2 = { + id: '3456-7890', + configuration: { + criteria: [] + } + }; let conditionSetDomainObject = { identifier: { namespace: "", @@ -43,7 +62,9 @@ describe('ConditionManager', () => { location: "mine", configuration: { conditionCollection: [ - mockCondition + mockCondition1, + mockCondition2, + mockDefaultCondition ] } }; @@ -59,7 +80,7 @@ describe('ConditionManager', () => { let mockDomainObject = { useCapability: function () { - return mockCondition; + return mockDefaultCondition; } }; mockInstantiate.and.callFake(function () { @@ -107,7 +128,11 @@ describe('ConditionManager', () => { openmct.objects.get.and.returnValues(new Promise(function (resolve, reject) { resolve(conditionSetDomainObject); }), new Promise(function (resolve, reject) { - resolve(mockCondition); + resolve(mockCondition1); + }), new Promise(function (resolve, reject) { + resolve(mockCondition2); + }), new Promise(function (resolve, reject) { + resolve(mockDefaultCondition); })); openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key); openmct.objects.observe.and.returnValue(function () {}); @@ -126,9 +151,65 @@ describe('ConditionManager', () => { }); it('creates a conditionCollection with a default condition', function () { - expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(1); - let defaultConditionId = conditionMgr.conditions[0].id; - expect(defaultConditionId).toEqual(mockCondition.id); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(3); + let defaultConditionId = conditionMgr.conditions[2].id; + expect(defaultConditionId).toEqual(mockDefaultCondition.id); + }); + + it('reorders a conditionCollection', function () { + let reorderPlan = [{ + oldIndex: 1, + newIndex: 0 + }, + { + oldIndex: 0, + newIndex: 1 + }, + { + oldIndex: 2, + newIndex: 2 + }]; + conditionMgr.reorderConditions(reorderPlan); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(3); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual(mockCondition2.id); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[1].id).toEqual(mockCondition1.id); + }); + + it('updates the right condition after reorder', function () { + let reorderPlan = [{ + oldIndex: 1, + newIndex: 0 + }, + { + oldIndex: 0, + newIndex: 1 + }, + { + oldIndex: 2, + newIndex: 2 + }]; + conditionMgr.reorderConditions(reorderPlan); + conditionMgr.updateCondition(updatedMockCondition1); + expect(conditionMgr.conditions[1].trigger).toEqual(updatedMockCondition1.configuration.trigger); + }); + + it('removes the right condition after reorder', function () { + let reorderPlan = [{ + oldIndex: 1, + newIndex: 0 + }, + { + oldIndex: 0, + newIndex: 1 + }, + { + oldIndex: 2, + newIndex: 2 + }]; + conditionMgr.reorderConditions(reorderPlan); + conditionMgr.removeCondition(mockCondition1.id); + expect(conditionMgr.conditions.length).toEqual(2); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual(mockCondition2.id); }); }); diff --git a/src/plugins/condition/components/Condition.vue b/src/plugins/condition/components/Condition.vue index f8186c4533..3d37fff5b5 100644 --- a/src/plugins/condition/components/Condition.vue +++ b/src/plugins/condition/components/Condition.vue @@ -308,15 +308,15 @@ export default { }; this.condition.configuration.criteria.push(criteriaObject); }, - dragStart(e) { - e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag - e.dataTransfer.effectAllowed = "copyMove"; - e.dataTransfer.setDragImage(e.target.closest('.c-condition-h'), 0, 0); + dragStart(event) { + event.dataTransfer.clearData(); + event.dataTransfer.setData('dragging', event.target); // required for FF to initiate drag + event.dataTransfer.effectAllowed = "copyMove"; + event.dataTransfer.setDragImage(event.target.closest('.c-condition-h'), 0, 0); this.$emit('setMoveIndex', this.conditionIndex); }, - dragEnd(event) { + dragEnd() { this.dragStarted = false; - event.dataTransfer.clearData(); this.$emit('dragComplete'); }, dropCondition(event, targetIndex) { @@ -359,10 +359,10 @@ export default { }, destroy() { }, - removeCondition(ev) { - this.$emit('removeCondition', this.conditionIndex); + removeCondition() { + this.$emit('removeCondition', this.condition.id); }, - cloneCondition(ev) { + cloneCondition() { this.$emit('cloneCondition', { condition: this.condition, index: this.conditionIndex @@ -380,8 +380,7 @@ export default { }, persist() { this.$emit('updateCondition', { - condition: this.condition, - index: this.conditionIndex + condition: this.condition }); }, initCap(str) { diff --git a/src/plugins/condition/components/ConditionCollection.vue b/src/plugins/condition/components/ConditionCollection.vue index 0b6147814e..012c686273 100644 --- a/src/plugins/condition/components/ConditionCollection.vue +++ b/src/plugins/condition/components/ConditionCollection.vue @@ -223,10 +223,10 @@ export default { this.conditionManager.addCondition(); }, updateCondition(data) { - this.conditionManager.updateCondition(data.condition, data.index); + this.conditionManager.updateCondition(data.condition); }, - removeCondition(index) { - this.conditionManager.removeCondition(index); + removeCondition(id) { + this.conditionManager.removeCondition(id); }, reorder(reorderPlan) { this.conditionManager.reorderConditions(reorderPlan); diff --git a/src/plugins/persistence/couch/CouchDocument.js b/src/plugins/persistence/couch/CouchDocument.js new file mode 100644 index 0000000000..72d6655ba2 --- /dev/null +++ b/src/plugins/persistence/couch/CouchDocument.js @@ -0,0 +1,53 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * A CouchDocument describes domain object model in a format + * which is easily read-written to CouchDB. This includes + * Couch's _id and _rev fields, as well as a separate + * metadata field which contains a subset of information found + * in the model itself (to support search optimization with + * CouchDB views.) + * @memberof platform/persistence/couch + * @constructor + * @param {string} id the id under which to store this mode + * @param {object} model the model to store + * @param {string} rev the revision to include (or undefined, + * if no revision should be noted for couch) + * @param {boolean} whether or not to mark this document as + * deleted (see CouchDB docs for _deleted) + */ +export default function CouchDocument(id, model, rev, markDeleted) { + return { + "_id": id, + "_rev": rev, + "_deleted": markDeleted, + "metadata": { + "category": "domain object", + "type": model.type, + "owner": "admin", + "name": model.name, + "created": Date.now() + }, + "model": model + }; +} diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js new file mode 100644 index 0000000000..64dfe88c7e --- /dev/null +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -0,0 +1,156 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import CouchDocument from "./CouchDocument"; +import CouchObjectQueue from "./CouchObjectQueue"; + +const REV = "_rev"; +const ID = "_id"; + +export default class CouchObjectProvider { + constructor(openmct, url, namespace) { + this.openmct = openmct; + this.url = url; + this.namespace = namespace; + this.objectQueue = {}; + } + + request(subPath, method, value) { + return fetch(this.url + '/' + subPath, { + method: method, + body: JSON.stringify(value) + }).then(response => response.json()) + .then(function (response) { + return response; + }, function () { + return undefined; + }); + } + + // Check the response to a create/update/delete request; + // track the rev if it's valid, otherwise return false to + // indicate that the request failed. + // persist any queued objects + checkResponse(response, intermediateResponse) { + let requestSuccess = false; + const id = response ? response.id : undefined; + let rev; + if (response && response.ok) { + rev = response.rev; + requestSuccess = true; + } + + intermediateResponse.resolve(requestSuccess); + + if (id) { + if (!this.objectQueue[id]) { + this.objectQueue[id] = new CouchObjectQueue(undefined, rev); + } + + this.objectQueue[id].updateRevision(rev); + this.objectQueue[id].pending = false; + if (this.objectQueue[id].hasNext()) { + this.updateQueued(id); + } + } + } + + getModel(response) { + if (response && response.model) { + let key = response[ID]; + let object = response.model; + object.identifier = { + namespace: this.namespace, + key: key + }; + if (!this.objectQueue[key]) { + this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); + } + + this.objectQueue[key].updateRevision(response[REV]); + + return object; + } else { + return undefined; + } + } + + get(identifier) { + return this.request(identifier.key, "GET").then(this.getModel.bind(this)); + } + + getIntermediateResponse() { + let intermediateResponse = {}; + intermediateResponse.promise = new Promise(function (resolve, reject) { + intermediateResponse.resolve = resolve; + intermediateResponse.reject = reject; + }); + + return intermediateResponse; + } + + enqueueObject(key, model, intermediateResponse) { + if (this.objectQueue[key]) { + this.objectQueue[key].enqueue({ + model, + intermediateResponse + }); + } else { + this.objectQueue[key] = new CouchObjectQueue({ + model, + intermediateResponse + }); + } + } + + create(model) { + let intermediateResponse = this.getIntermediateResponse(); + const key = model.identifier.key; + this.enqueueObject(key, model, intermediateResponse); + this.objectQueue[key].pending = true; + const queued = this.objectQueue[key].dequeue(); + this.request(key, "PUT", new CouchDocument(key, queued.model)).then((response) => { + this.checkResponse(response, queued.intermediateResponse); + }); + + return intermediateResponse.promise; + } + + updateQueued(key) { + if (!this.objectQueue[key].pending) { + this.objectQueue[key].pending = true; + const queued = this.objectQueue[key].dequeue(); + this.request(key, "PUT", new CouchDocument(key, queued.model, this.objectQueue[key].rev)).then((response) => { + this.checkResponse(response, queued.intermediateResponse); + }); + } + } + + update(model) { + let intermediateResponse = this.getIntermediateResponse(); + const key = model.identifier.key; + this.enqueueObject(key, model, intermediateResponse); + this.updateQueued(key); + + return intermediateResponse.promise; + } +} diff --git a/src/plugins/persistence/couch/CouchObjectQueue.js b/src/plugins/persistence/couch/CouchObjectQueue.js new file mode 100644 index 0000000000..33708330e1 --- /dev/null +++ b/src/plugins/persistence/couch/CouchObjectQueue.js @@ -0,0 +1,51 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +export default class CouchObjectQueue { + constructor(object, rev) { + this.rev = rev; + this.objects = object ? [object] : []; + this.pending = false; + } + + updateRevision(rev) { + this.rev = rev; + } + + hasNext() { + return this.objects.length; + } + + enqueue(item) { + this.objects.push(item); + } + + dequeue() { + return this.objects.shift(); + } + + clear() { + this.rev = undefined; + this.objects = []; + } + +} diff --git a/src/plugins/persistence/couch/plugin.js b/src/plugins/persistence/couch/plugin.js new file mode 100644 index 0000000000..7692e3ab67 --- /dev/null +++ b/src/plugins/persistence/couch/plugin.js @@ -0,0 +1,30 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import CouchObjectProvider from './CouchObjectProvider'; +const NAMESPACE = ''; + +export default function CouchPlugin(url) { + return function install(openmct) { + openmct.objects.addProvider(NAMESPACE, new CouchObjectProvider(openmct, url, NAMESPACE)); + }; +} diff --git a/src/plugins/persistence/couch/pluginSpec.js b/src/plugins/persistence/couch/pluginSpec.js new file mode 100644 index 0000000000..43c06678ec --- /dev/null +++ b/src/plugins/persistence/couch/pluginSpec.js @@ -0,0 +1,116 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import CouchPlugin from './plugin.js'; +import { + createOpenMct, + resetApplicationState, spyOnBuiltins +} from 'utils/testing'; +import CouchObjectProvider from './CouchObjectProvider'; + +describe('the plugin', () => { + let openmct; + let element; + let child; + let provider; + let testSpace = 'testSpace'; + let testPath = '/test/db'; + let mockDomainObject = { + identifier: { + namespace: '', + key: 'some-value' + } + }; + + beforeEach((done) => { + openmct = createOpenMct(false); + openmct.install(new CouchPlugin(testSpace, testPath)); + + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + + openmct.on('start', done); + openmct.startHeadless(); + + provider = openmct.objects.getProvider(mockDomainObject.identifier); + spyOn(provider, 'get').and.callThrough(); + spyOn(provider, 'create').and.callThrough(); + spyOn(provider, 'update').and.callThrough(); + + spyOnBuiltins(['fetch'], window); + fetch.and.returnValue(Promise.resolve({ + json: () => { + return { + ok: true, + _id: 'some-value', + _rev: 1, + model: {} + }; + } + })); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('gets an object', () => { + openmct.objects.get(mockDomainObject.identifier).then((result) => { + expect(result.identifier.key).toEqual(mockDomainObject.identifier.key); + }); + }); + + it('creates an object', () => { + openmct.objects.save(mockDomainObject).then((result) => { + expect(provider.create).toHaveBeenCalled(); + expect(result).toBeTrue(); + }); + }); + + it('updates an object', () => { + openmct.objects.save(mockDomainObject).then((result) => { + expect(result).toBeTrue(); + expect(provider.create).toHaveBeenCalled(); + openmct.objects.save(mockDomainObject).then((updatedResult) => { + expect(updatedResult).toBeTrue(); + expect(provider.update).toHaveBeenCalled(); + }); + }); + }); + + it('updates queued objects', () => { + let couchProvider = new CouchObjectProvider(openmct, 'http://localhost', ''); + let intermediateResponse = couchProvider.getIntermediateResponse(); + spyOn(couchProvider, 'updateQueued'); + couchProvider.enqueueObject(mockDomainObject.identifier.key, mockDomainObject, intermediateResponse); + couchProvider.objectQueue[mockDomainObject.identifier.key].updateRevision(1); + couchProvider.update(mockDomainObject); + expect(couchProvider.objectQueue[mockDomainObject.identifier.key].hasNext()).toBe(2); + couchProvider.checkResponse({ + ok: true, + rev: 2, + id: mockDomainObject.identifier.key + }, intermediateResponse); + + expect(couchProvider.updateQueued).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index d03612e2c4..6c3f7b2401 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -24,6 +24,7 @@ define([ 'lodash', './utcTimeSystem/plugin', './localTimeSystem/plugin', + './ISOTimeFormat/plugin', '../../example/generator/plugin', './autoflow/AutoflowTabularPlugin', './timeConductor/plugin', @@ -55,11 +56,13 @@ define([ './URLTimeSettingsSynchronizer/plugin', './notificationIndicator/plugin', './newFolderAction/plugin', + './persistence/couch/plugin', './defaultRootName/plugin' ], function ( _, UTCTimeSystem, LocalTimeSystem, + ISOTimeFormat, GeneratorPlugin, AutoflowPlugin, TimeConductorPlugin, @@ -91,12 +94,12 @@ define([ URLTimeSettingsSynchronizer, NotificationIndicator, NewFolderAction, + CouchDBPlugin, DefaultRootName ) { const bundleMap = { LocalStorage: 'platform/persistence/local', MyItems: 'platform/features/my-items', - CouchDB: 'platform/persistence/couch', Elasticsearch: 'platform/persistence/elastic' }; @@ -128,27 +131,7 @@ define([ plugins.Conductor = TimeConductorPlugin.default; - plugins.CouchDB = function (url) { - return function (openmct) { - if (url) { - const bundleName = "config/couch"; - openmct.legacyRegistry.register(bundleName, { - "extensions": { - "constants": [ - { - "key": "COUCHDB_PATH", - "value": url, - "priority": "mandatory" - } - ] - } - }); - openmct.legacyRegistry.enable(bundleName); - } - - openmct.legacyRegistry.enable(bundleMap.CouchDB); - }; - }; + plugins.CouchDB = CouchDBPlugin.default; plugins.Elasticsearch = function (url) { return function (openmct) { @@ -203,6 +186,7 @@ define([ plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; plugins.NotificationIndicator = NotificationIndicator.default; plugins.NewFolderAction = NewFolderAction.default; + plugins.ISOTimeFormat = ISOTimeFormat.default; plugins.DefaultRootName = DefaultRootName.default; return plugins; diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue index ef7607c689..5da77f3373 100644 --- a/src/plugins/timeConductor/Conductor.vue +++ b/src/plugins/timeConductor/Conductor.vue @@ -260,7 +260,7 @@ export default { this.isZooming = false; if (bounds) { - this.handleNewBounds(bounds); + this.openmct.time.bounds(bounds); } else { this.setViewFromBounds(this.bounds); } diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss index b0fb8ec02c..967ec2d853 100644 --- a/src/ui/layout/mct-tree.scss +++ b/src/ui/layout/mct-tree.scss @@ -2,8 +2,6 @@ display: flex; flex-direction: column; flex: 1 1 auto; - //TODO: Do we need this??? - //padding-right: $interiorMarginSm; overflow: auto; > * + * { margin-top: $interiorMargin; } @@ -245,6 +243,7 @@ border: 1px solid $colorInteriorBorder; border-radius: $controlCr; padding: $interiorMargin; + overflow: auto; } } diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 51fb54112e..80aea7f582 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -76,7 +76,7 @@ @expanded="handleExpanded" />