From f9060a485d0f44bca521f4ae7948962a6ea2740b Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Fri, 9 Mar 2018 12:39:25 -0800 Subject: [PATCH] [Plugin] Add imported root plugin (#1784) * [Plugin] Add static root plugin Add StaticRootPlugin, which allows a file exported with the ImportExport plugin to be mounted as a static root in Open MCT. Allows deployers to configure standard displays for deployments by exporting displays they have already created. * Include all src files --- karma.conf.js | 3 +- src/plugins/plugins.js | 8 +- .../staticRootPlugin/StaticModelProvider.js | 78 ++++++++++ .../StaticModelProviderSpec.js | 133 ++++++++++++++++++ src/plugins/staticRootPlugin/plugin.js | 51 +++++++ .../static-provider-test.json | 1 + 6 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 src/plugins/staticRootPlugin/StaticModelProvider.js create mode 100644 src/plugins/staticRootPlugin/StaticModelProviderSpec.js create mode 100644 src/plugins/staticRootPlugin/plugin.js create mode 100644 src/plugins/staticRootPlugin/static-provider-test.json diff --git a/karma.conf.js b/karma.conf.js index 486ee66010..2c8c27f93f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -37,14 +37,13 @@ module.exports = function(config) { {pattern: 'bower_components/**/*.js', included: false}, {pattern: 'node_modules/d3-*/**/*.js', included: false}, {pattern: 'node_modules/vue/**/*.js', included: false}, - {pattern: 'src/**/*.js', included: false}, + {pattern: 'src/**/*', included: false}, {pattern: 'example/**/*.html', included: false}, {pattern: 'example/**/*.js', included: false}, {pattern: 'example/**/*.json', included: false}, {pattern: 'platform/**/*.js', included: false}, {pattern: 'warp/**/*.js', included: false}, {pattern: 'platform/**/*.html', included: false}, - {pattern: 'src/**/*.html', included: false}, 'test-main.js' ], diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 0deb2f5497..87159d9637 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -31,7 +31,8 @@ define([ './summaryWidget/plugin', './URLIndicatorPlugin/URLIndicatorPlugin', './telemetryMean/plugin', - './plot/plugin' + './plot/plugin', + './staticRootPlugin/plugin' ], function ( _, UTCTimeSystem, @@ -43,7 +44,8 @@ define([ SummaryWidget, URLIndicatorPlugin, TelemetryMean, - PlotPlugin + PlotPlugin, + StaticRootPlugin ) { var bundleMap = { CouchDB: 'platform/persistence/couch', @@ -66,6 +68,8 @@ define([ plugins.ImportExport = ImportExport; + plugins.StaticRootPlugin = StaticRootPlugin; + /** * A tabular view showing the latest values of multiple telemetry points at * once. Formatted so that labels and values are aligned. diff --git a/src/plugins/staticRootPlugin/StaticModelProvider.js b/src/plugins/staticRootPlugin/StaticModelProvider.js new file mode 100644 index 0000000000..956df5a58e --- /dev/null +++ b/src/plugins/staticRootPlugin/StaticModelProvider.js @@ -0,0 +1,78 @@ +define([ + '../../api/objects/object-utils' +], function ( + objectUtils +) { + /** + * Transforms an import json blob into a object map that can be used to + * provide objects. Rewrites root identifier in import data with provided + * rootIdentifier, and rewrites all child object identifiers so that they + * exist in the same namespace as the rootIdentifier. + */ + function rewriteObjectIdentifiers(importData, rootIdentifier) { + var rootId = importData.rootId; + var objectString = JSON.stringify(importData.openmct); + + Object.keys(importData.openmct).forEach(function (originalId, i) { + var newId; + if (originalId === rootId) { + newId = objectUtils.makeKeyString(rootIdentifier); + } else { + newId = objectUtils.makeKeyString({ + namespace: rootIdentifier.namespace, + key: i + }); + } + while (objectString.indexOf(originalId) !== -1) { + objectString = objectString.replace( + '"' + originalId + '"', + '"' + newId + '"' + ); + } + }); + + return JSON.parse(objectString); + } + + /** + * Convets all objects in an object make from old format objects to new + * format objects. + */ + function convertToNewObjects(oldObjectMap) { + return Object.keys(oldObjectMap) + .reduce(function (newObjectMap, key) { + newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key); + return newObjectMap; + }, {}); + } + + /* Set the root location correctly for a top-level object */ + function setRootLocation(objectMap, rootIdentifier) { + objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT'; + return objectMap; + } + + /** + * Takes importData (as provided by the ImportExport plugin) and exposes + * an object provider to fetch those objects. + */ + function StaticModelProvider(importData, rootIdentifier) { + var oldFormatObjectMap = rewriteObjectIdentifiers(importData, rootIdentifier); + var newFormatObjectMap = convertToNewObjects(oldFormatObjectMap); + this.objectMap = setRootLocation(newFormatObjectMap, rootIdentifier); + } + + /** + * Standard "Get". + */ + StaticModelProvider.prototype.get = function (identifier) { + var keyString = objectUtils.makeKeyString(identifier); + if (this.objectMap[keyString]) { + return this.objectMap[keyString]; + } + throw new Error(keyString + ' not found in import models.'); + }; + + return StaticModelProvider; + +}); diff --git a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js new file mode 100644 index 0000000000..80a3592263 --- /dev/null +++ b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js @@ -0,0 +1,133 @@ +define([ + './StaticModelProvider', + 'text!./static-provider-test.json' +], function ( + StaticModelProvider, + testStaticDataText +) { + + describe('StaticModelProvider', function () { + + var staticProvider; + + beforeEach(function () { + var staticData = JSON.parse(testStaticDataText); + staticProvider = new StaticModelProvider(staticData, { + namespace: 'my-import', + key: 'root' + }); + }); + + describe('rootObject', function () { + var rootModel; + + beforeEach(function () { + rootModel = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('is located at top level', function () { + expect(rootModel.location).toBe('ROOT'); + }); + + it('has new-format identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('has new-format composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + }); + }); + + describe('childObjects', function () { + var swg; + var layout; + var fixed; + + beforeEach(function () { + swg = staticProvider.get({ + namespace: 'my-import', + key: '1' + }); + layout = staticProvider.get({ + namespace: 'my-import', + key: '2' + }); + fixed = staticProvider.get({ + namespace: 'my-import', + key: '3' + }); + }); + + it('match expected ordering', function () { + // this is a sanity check to make sure the identifiers map in + // the correct order. + expect(swg.type).toBe('generator'); + expect(layout.type).toBe('layout'); + expect(fixed.type).toBe('telemetry.fixed'); + }); + + it('have new-style identifiers', function () { + expect(swg.identifier).toEqual({ + namespace: 'my-import', + key: '1' + }); + expect(layout.identifier).toEqual({ + namespace: 'my-import', + key: '2' + }); + expect(fixed.identifier).toEqual({ + namespace: 'my-import', + key: '3' + }); + }); + + it('have new-style composition', function () { + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '3' + }); + expect(fixed.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + }); + + it('rewrites locations', function () { + expect(swg.location).toBe('my-import:root'); + expect(layout.location).toBe('my-import:root'); + expect(fixed.location).toBe('my-import:2'); + }); + + it('rewrites matched identifiers in objects', function () { + expect(layout.configuration.layout.panels['my-import:1']) + .toBeDefined(); + expect(layout.configuration.layout.panels['my-import:3']) + .toBeDefined(); + expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0']) + .not.toBeDefined(); + expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d']) + .not.toBeDefined(); + expect(fixed.configuration['fixed-display'].elements[0].id) + .toBe('my-import:1'); + }); + + }); + }); +}); diff --git a/src/plugins/staticRootPlugin/plugin.js b/src/plugins/staticRootPlugin/plugin.js new file mode 100644 index 0000000000..7ef980bc82 --- /dev/null +++ b/src/plugins/staticRootPlugin/plugin.js @@ -0,0 +1,51 @@ +define([ + './StaticModelProvider' +], function ( + StaticModelProvider +) { + /** + * Static Root Plugin: takes an export file and exposes it as a new root + * object. + */ + function StaticRootPlugin(namespace, exportUrl) { + + var rootIdentifier = { + namespace: namespace, + key: 'root' + }; + + var cachedProvider; + + var loadProvider = function () { + return fetch(exportUrl) + .then(function (response) { + return response.json(); + }) + .then(function (importData) { + cachedProvider = new StaticModelProvider(importData, rootIdentifier); + return cachedProvider; + }); + + }; + + var getProvider = function () { + if (!cachedProvider) { + cachedProvider = loadProvider(); + } + return Promise.resolve(cachedProvider); + }; + + return function install(openmct) { + openmct.objects.addRoot(rootIdentifier); + openmct.objects.addProvider(namespace, { + get: function (identifier) { + return getProvider().then(function (provider) { + return provider.get(identifier); + }); + } + }); + }; + } + + return StaticRootPlugin; +}); diff --git a/src/plugins/staticRootPlugin/static-provider-test.json b/src/plugins/staticRootPlugin/static-provider-test.json new file mode 100644 index 0000000000..541f8efbc5 --- /dev/null +++ b/src/plugins/staticRootPlugin/static-provider-test.json @@ -0,0 +1 @@ +{"openmct":{"a9122832-4b6e-43ea-8219-5359c14c5de8":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","d2ac3ae4-0af2-49fe-81af-adac09936215"],"name":"import-provider-test","type":"folder","notes":"test data for import provider.","modified":1508522673278,"location":"mine","persisted":1508522673278},"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"telemetry":{"period":10,"amplitude":1,"offset":0,"dataRateInHz":1,"values":[{"key":"utc","name":"Time","format":"utc","hints":{"domain":1,"priority":0},"source":"utc"},{"key":"yesterday","name":"Yesterday","format":"utc","hints":{"domain":2,"priority":1},"source":"yesterday"},{"key":"sin","name":"Sine","hints":{"range":1,"priority":2},"source":"sin"},{"key":"cos","name":"Cosine","hints":{"range":2,"priority":3},"source":"cos"}]},"name":"SWG-10","type":"generator","modified":1508522652874,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522652874},"d2ac3ae4-0af2-49fe-81af-adac09936215":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","20273193-f069-49e9-b4f7-b97a87ed755d"],"name":"Layout","type":"layout","configuration":{"layout":{"panels":{"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"position":[0,0],"dimensions":[17,8]},"20273193-f069-49e9-b4f7-b97a87ed755d":{"position":[0,8],"dimensions":[17,1],"hasFrame":false}}}},"modified":1508522745580,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522745580},"20273193-f069-49e9-b4f7-b97a87ed755d":{"layoutGrid":[64,16],"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0"],"name":"FP Test","type":"telemetry.fixed","configuration":{"fixed-display":{"elements":[{"type":"fixed.telemetry","x":0,"y":0,"id":"483c00d4-bb1d-4b42-b29a-c58e06b322a0","stroke":"transparent","color":"","titled":true,"width":8,"height":2,"useGrid":true,"size":"24px"}]}},"modified":1508522717619,"location":"d2ac3ae4-0af2-49fe-81af-adac09936215","persisted":1508522717619}},"rootId":"a9122832-4b6e-43ea-8219-5359c14c5de8"} \ No newline at end of file