From dbdc9bb4e2eaba5164a5a145383e9afebca58ef4 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Fri, 14 Apr 2023 16:31:04 -0700 Subject: [PATCH] fix(#6549): [StaticRootPlugin] Remap non-empty root namespaces and non-root namespaces correctly (#6583) * fix: preserve truthy namespaces and unmapped values - Fixes the issue of the Static Root provider overriding existing namespaces, such as those from a telemetry dictionary - Fixes the issue of keys of child objects NOT present in the idMapping (such as those from a telemetry dictionary) being overwritten as undefined - TODO: This will not work for objects exported from an environment that has the "MyItems" namespace defined to anything other than an empty string. Need to figure out how to handle this. * fix: handle the case of rootId having a namespace !== "" * refactor: use `parseKeyString` * fix: StaticRootPlugin object mapping for non-empty namespaces * fix: use index, fix location identifiers * tets: add non-empty namespace tests (wip) * refactor: rename and move test files * test: update StaticModelProvider tests * fix: remap to identifiers for config, not keystring --------- Co-authored-by: Khalid Adil --- .../staticRootPlugin/StaticModelProvider.js | 82 +++-- .../StaticModelProviderSpec.js | 325 +++++++++++++----- ...static-provider-test-empty-namespace.json} | 0 .../static-provider-test-foo-namespace.json | 1 + 4 files changed, 282 insertions(+), 126 deletions(-) rename src/plugins/staticRootPlugin/{static-provider-test.json => test-data/static-provider-test-empty-namespace.json} (100%) create mode 100644 src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json diff --git a/src/plugins/staticRootPlugin/StaticModelProvider.js b/src/plugins/staticRootPlugin/StaticModelProvider.js index 3b495e431f..4bf9ffbca3 100644 --- a/src/plugins/staticRootPlugin/StaticModelProvider.js +++ b/src/plugins/staticRootPlugin/StaticModelProvider.js @@ -46,76 +46,96 @@ class StaticModelProvider { throw new Error(keyString + ' not found in import models.'); } - parseObjectLeaf(objectLeaf, idMap, namespace) { + parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) { Object.keys(objectLeaf).forEach((nodeKey) => { if (idMap.get(nodeKey)) { const newIdentifier = objectUtils.makeKeyString({ - namespace, + namespace: newRootNamespace, key: idMap.get(nodeKey) }); objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] }; delete objectLeaf[nodeKey]; - objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, namespace); + objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, newRootNamespace, oldRootNamespace); } else { - objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, namespace); + objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, newRootNamespace, oldRootNamespace); } }); return objectLeaf; } - parseArrayLeaf(arrayLeaf, idMap, namespace) { + parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) { return arrayLeaf.map((leafValue, index) => this.parseTreeLeaf( - null, leafValue, idMap, namespace)); + null, leafValue, idMap, newRootNamespace, oldRootNamespace)); } - parseBranchedLeaf(branchedLeafValue, idMap, namespace) { + parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) { if (Array.isArray(branchedLeafValue)) { - return this.parseArrayLeaf(branchedLeafValue, idMap, namespace); + return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); } else { - return this.parseObjectLeaf(branchedLeafValue, idMap, namespace); + return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); } } - parseTreeLeaf(leafKey, leafValue, idMap, namespace) { + parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) { if (leafValue === null || leafValue === undefined) { return leafValue; } const hasChild = typeof leafValue === 'object'; if (hasChild) { - return this.parseBranchedLeaf(leafValue, idMap, namespace); + return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace); } if (leafKey === 'key') { - return idMap.get(leafValue); - } else if (leafKey === 'namespace') { - return namespace; - } else if (leafKey === 'location') { - if (idMap.get(leafValue)) { - const newLocationIdentifier = objectUtils.makeKeyString({ - namespace, - key: idMap.get(leafValue) - }); - - return newLocationIdentifier; + let mappedLeafValue; + if (oldRootNamespace) { + mappedLeafValue = idMap.get(objectUtils.makeKeyString({ + namespace: oldRootNamespace, + key: leafValue + })); + } else { + mappedLeafValue = idMap.get(leafValue); } - return null; - } else if (idMap.get(leafValue)) { - const newIdentifier = objectUtils.makeKeyString({ - namespace, - key: idMap.get(leafValue) + return mappedLeafValue ?? leafValue; + } else if (leafKey === 'namespace') { + // Only rewrite the namespace if it matches the old root namespace. + // This is to prevent rewriting namespaces of objects that are not + // children of the root object (e.g.: objects from a telemetry dictionary) + return leafValue === oldRootNamespace + ? newRootNamespace + : leafValue; + } else if (leafKey === 'location') { + const mappedLeafValue = idMap.get(leafValue); + if (!mappedLeafValue) { + return null; + } + + const newLocationIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: mappedLeafValue }); - return newIdentifier; + return newLocationIdentifier; } else { - return leafValue; + const mappedLeafValue = idMap.get(leafValue); + if (mappedLeafValue) { + const newIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: mappedLeafValue + }); + + return newIdentifier; + } else { + return leafValue; + } } } rewriteObjectIdentifiers(importData, rootIdentifier) { - const namespace = rootIdentifier.namespace; + const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId); + const { namespace: newRootNamespace } = rootIdentifier; const idMap = new Map(); const objectTree = importData.openmct; @@ -128,7 +148,7 @@ class StaticModelProvider { idMap.set(originalId, newId); }); - const newTree = this.parseTreeLeaf(null, objectTree, idMap, namespace); + const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace); return newTree; } diff --git a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js index 51481f4e6a..347a160dd9 100644 --- a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js +++ b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js @@ -20,130 +20,265 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import testStaticData from './static-provider-test.json'; +import testStaticDataEmptyNamespace from './test-data/static-provider-test-empty-namespace.json'; +import testStaticDataFooNamespace from './test-data/static-provider-test-foo-namespace.json'; import StaticModelProvider from './StaticModelProvider'; describe('StaticModelProvider', function () { + describe('with empty namespace', function () { - let staticProvider; - - beforeEach(function () { - const staticData = JSON.parse(JSON.stringify(testStaticData)); - staticProvider = new StaticModelProvider(staticData, { - namespace: 'my-import', - key: 'root' - }); - }); - - describe('rootObject', function () { - let rootModel; + let staticProvider; beforeEach(function () { - rootModel = staticProvider.get({ + const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace)); + staticProvider = new StaticModelProvider(staticData, { namespace: 'my-import', key: 'root' }); }); - it('is located at top level', function () { - expect(rootModel.location).toBe('ROOT'); + describe('rootObject', function () { + let 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 remapped identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('has remapped identifiers in composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + }); }); - it('has new-format identifier', function () { - expect(rootModel.identifier).toEqual({ + describe('childObjects', function () { + let swg; + let layout; + let 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 remapped 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 remapped 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'); + }); + + }); + }); + describe('with namespace "foo"', function () { + + let staticProvider; + + beforeEach(function () { + const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace)); + staticProvider = new StaticModelProvider(staticData, { 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('rootObject', function () { + let rootModel; - describe('childObjects', function () { - let swg; - let layout; - let fixed; + beforeEach(function () { + rootModel = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + }); - beforeEach(function () { - swg = staticProvider.get({ - namespace: 'my-import', - key: '1' + it('is located at top level', function () { + expect(rootModel.location).toBe('ROOT'); }); - layout = staticProvider.get({ - namespace: 'my-import', - key: '2' + + it('has remapped identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); }); - fixed = staticProvider.get({ - namespace: 'my-import', - key: '3' + + it('has remapped composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); }); }); - 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'); - }); + describe('childObjects', function () { + let clock; + let layout; + let swg; + let folder; - it('have new-style identifiers', function () { - expect(swg.identifier).toEqual({ - namespace: 'my-import', - key: '1' + beforeEach(function () { + folder = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + layout = staticProvider.get({ + namespace: 'my-import', + key: '1' + }); + swg = staticProvider.get({ + namespace: 'my-import', + key: '2' + }); + clock = staticProvider.get({ + namespace: 'my-import', + key: '3' + }); }); - expect(layout.identifier).toEqual({ - namespace: 'my-import', - key: '2' + + it('match expected ordering', function () { + // this is a sanity check to make sure the identifiers map in + // the correct order. + expect(folder.type).toBe('folder'); + expect(swg.type).toBe('generator'); + expect(layout.type).toBe('layout'); + expect(clock.type).toBe('clock'); }); - expect(fixed.identifier).toEqual({ - namespace: 'my-import', - key: '3' + + it('have remapped identifiers', function () { + expect(folder.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + expect(layout.identifier).toEqual({ + namespace: 'my-import', + key: '1' + }); + expect(swg.identifier).toEqual({ + namespace: 'my-import', + key: '2' + }); + expect(clock.identifier).toEqual({ + namespace: 'my-import', + key: '3' + }); + }); + + it('have remapped identifiers in composition', function () { + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '3' + }); + }); + + it('layout has remapped identifiers in configuration', function () { + const identifiers = layout.configuration.items + .map(item => item.identifier) + .filter(identifier => identifier !== undefined); + expect(identifiers).toContain({ + namespace: 'my-import', + key: '2' + }); + expect(identifiers).toContain({ + namespace: 'my-import', + key: '3' + }); + }); + + it('rewrites locations', function () { + expect(folder.location).toBe('ROOT'); + expect(swg.location).toBe('my-import:root'); + expect(layout.location).toBe('my-import:root'); + expect(clock.location).toBe('my-import:root'); }); }); - - 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/static-provider-test.json b/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json similarity index 100% rename from src/plugins/staticRootPlugin/static-provider-test.json rename to src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json diff --git a/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json b/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json new file mode 100644 index 0000000000..49dd9d5926 --- /dev/null +++ b/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json @@ -0,0 +1 @@ +{"openmct":{"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1":{"identifier":{"key":"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","namespace":"foo"},"name":"Folder Foo","type":"folder","composition":[{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"modified":1681164966705,"location":"foo:mine","created":1681164829371,"persisted":1681164966706},"foo:95729018-86ed-4484-867d-10c63c41c5a1":{"identifier":{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},"name":"Display Layout Bar","type":"layout","composition":[{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"configuration":{"items":[{"fill":"#666666","stroke":"","x":42,"y":42,"width":20,"height":4,"type":"box-view","id":"14505a5d-b846-4504-961f-8c9bcdf19f39"},{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"x":0,"y":0,"width":40,"height":15,"displayMode":"all","value":"sin","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"05baa95f-2064-4cb0-ad9f-575758491220"},{"width":40,"height":15,"x":0,"y":15,"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5"}],"layoutGrid":[10,10],"objectStyles":{"05baa95f-2064-4cb0-ad9f-575758491220":{"staticStyle":{"style":{"border":"1px solid #00ff00","backgroundColor":"#0000ff","color":"#ff00ff"}}}}},"modified":1681165037189,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164838178,"persisted":1681165037190},"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c":{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"name":"SWG Baz","type":"generator","telemetry":{"period":"20","amplitude":"2","offset":"5","dataRateInHz":1,"phase":0,"randomness":0,"loadDelay":0,"infinityValues":false,"staleness":false},"modified":1681164910719,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164903684,"persisted":1681164910719},"foo:3545554b-53c8-467d-a70d-e90d1a120e4a":{"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"name":"Clock Qux","type":"clock","configuration":{"baseFormat":"YYYY/MM/DD hh:mm:ss","use24":"clock12","timezone":"UTC"},"modified":1681164989837,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164966702,"persisted":1681164989837}},"rootId":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1"} \ No newline at end of file