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 <khalidadil29@gmail.com>
This commit is contained in:
Jesse Mazzella 2023-04-14 16:31:04 -07:00 committed by GitHub
parent a9a98380f2
commit dbdc9bb4e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 282 additions and 126 deletions

View File

@ -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;
}

View File

@ -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');
});
});
});

View File

@ -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"}