superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ccwilli...@apache.org
Subject [incubator-superset] branch master updated: Add data structures for chart plugin system (#6028)
Date Tue, 09 Oct 2018 18:47:22 GMT
This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new cd2c46a  Add data structures for chart plugin system (#6028)
cd2c46a is described below

commit cd2c46a5ed5a3d3df48b2d7cbf22521cbaa965b3
Author: Krist Wongsuphasawat <krist.wongz@gmail.com>
AuthorDate: Tue Oct 9 11:47:16 2018 -0700

    Add data structures for chart plugin system (#6028)
    
    * add unit tests
    
    * add test structure
    
    * add unit tests for Registry
    
    * add LoaderRegistry unit test
    
    * add unit test for makeSingleton
    
    * add type check
    
    * add plugin data structures
    
    * simplify API
    
    * add preset tests
    
    * update test message
    
    * fix lint
    
    * update makeSingleton
    
    * update Plugin, Preset and unit test
    
    * revise Registry code
    
    * update unit test, add remove function
    
    * update test
    
    * update unit test
    
    * update plugin unit test
    
    * add .keys(), .entries() and .entriesAsPromise()
    
    * update test description
---
 .../spec/javascripts/modules/Registry_spec.js      | 175 +++++++++++++++++++++
 .../spec/javascripts/utils/isRequired_spec.js      |   9 ++
 .../spec/javascripts/utils/makeSingleton_spec.js   |  38 +++++
 .../visualizations/models/ChartPlugin_spec.js      |  42 +++++
 .../visualizations/models/Plugin_spec.js           |  48 ++++++
 .../visualizations/models/Preset_spec.js           |  65 ++++++++
 superset/assets/src/modules/Registry.js            |  72 +++++++++
 superset/assets/src/utils/isRequired.js            |   3 +
 superset/assets/src/utils/makeSingleton.js         |  10 ++
 .../visualizations/core/models/ChartMetadata.js    |  13 ++
 .../src/visualizations/core/models/ChartPlugin.js  |  43 +++++
 .../src/visualizations/core/models/Plugin.js       |  25 +++
 .../src/visualizations/core/models/Preset.js       |  23 +++
 .../registries/ChartComponentRegistrySingleton.js  |  12 ++
 .../registries/ChartMetadataRegistrySingleton.js   |  12 ++
 .../ChartTransformPropsRegistrySingleton.js        |  12 ++
 16 files changed, 602 insertions(+)

diff --git a/superset/assets/spec/javascripts/modules/Registry_spec.js b/superset/assets/spec/javascripts/modules/Registry_spec.js
new file mode 100644
index 0000000..47cdb21
--- /dev/null
+++ b/superset/assets/spec/javascripts/modules/Registry_spec.js
@@ -0,0 +1,175 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import Registry from '../../../src/modules/Registry';
+
+describe('Registry', () => {
+  it('exists', () => {
+    expect(Registry !== undefined).to.equal(true);
+  });
+
+  describe('new Registry(name)', () => {
+    it('can create a new registry when name is not given', () => {
+      const registry = new Registry();
+      expect(registry).to.be.instanceOf(Registry);
+    });
+    it('can create a new registry when name is given', () => {
+      const registry = new Registry('abc');
+      expect(registry).to.be.instanceOf(Registry);
+      expect(registry.name).to.equal('abc');
+    });
+  });
+
+  describe('.has(key)', () => {
+    it('returns true if an item with the given key exists', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      expect(registry.has('a')).to.equal(true);
+      registry.registerLoader('b', () => 'testValue2');
+      expect(registry.has('b')).to.equal(true);
+    });
+    it('returns false if an item with the given key does not exist', () => {
+      const registry = new Registry();
+      expect(registry.has('a')).to.equal(false);
+    });
+  });
+
+  describe('.registerValue(key, value)', () => {
+    it('registers the given value with the given key', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      expect(registry.has('a')).to.equal(true);
+      expect(registry.get('a')).to.equal('testValue');
+    });
+    it('returns the registry itself', () => {
+      const registry = new Registry();
+      expect(registry.registerValue('a', 'testValue')).to.equal(registry);
+    });
+  });
+
+  describe('.registerLoader(key, loader)', () => {
+    it('registers the given loader with the given key', () => {
+      const registry = new Registry();
+      registry.registerLoader('a', () => 'testValue');
+      expect(registry.has('a')).to.equal(true);
+      expect(registry.get('a')).to.equal('testValue');
+    });
+    it('returns the registry itself', () => {
+      const registry = new Registry();
+      expect(registry.registerLoader('a', () => 'testValue')).to.equal(registry);
+    });
+  });
+
+  describe('.get(key)', () => {
+    it('given the key, returns the value if the item is a value', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      expect(registry.get('a')).to.equal('testValue');
+    });
+    it('given the key, returns the result of the loader function if the item is a loader',
() => {
+      const registry = new Registry();
+      registry.registerLoader('b', () => 'testValue2');
+      expect(registry.get('b')).to.equal('testValue2');
+    });
+    it('returns null if the item with specified key does not exist', () => {
+      const registry = new Registry();
+      expect(registry.get('a')).to.equal(null);
+    });
+    it('If the key was registered multiple times, returns the most recent item.', () =>
{
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      expect(registry.get('a')).to.equal('testValue');
+      registry.registerLoader('a', () => 'newValue');
+      expect(registry.get('a')).to.equal('newValue');
+    });
+  });
+
+  describe('.getAsPromise(key)', () => {
+    it('given the key, returns a promise of item value if the item is a value', () =>
{
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      return registry.getAsPromise('a').then((value) => {
+        expect(value).to.equal('testValue');
+      });
+    });
+    it('given the key, returns a promise of result of the loader function if the item is
a loader ', () => {
+      const registry = new Registry();
+      registry.registerLoader('a', () => 'testValue');
+      return registry.getAsPromise('a').then((value) => {
+        expect(value).to.equal('testValue');
+      });
+    });
+    it('returns a rejected promise if the item with specified key does not exist', () =>
{
+      const registry = new Registry();
+      return registry.getAsPromise('a').then(null, (err) => {
+        expect(err).to.equal('Item with key "a" is not registered.');
+      });
+    });
+    it('If the key was registered multiple times, returns a promise of the most recent item.',
() => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      const promise1 = registry.getAsPromise('a').then((value) => {
+        expect(value).to.equal('testValue');
+      });
+      registry.registerLoader('a', () => 'newValue');
+      const promise2 = registry.getAsPromise('a').then((value) => {
+        expect(value).to.equal('newValue');
+      });
+      return Promise.all([promise1, promise2]);
+    });
+  });
+
+  describe('.keys()', () => {
+    it('returns an array of keys', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      registry.registerLoader('b', () => 'test2');
+      expect(registry.keys()).to.deep.equal(['a', 'b']);
+    });
+  });
+
+  describe('.entries()', () => {
+    it('returns an array of { key, value }', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'test1');
+      registry.registerLoader('b', () => 'test2');
+      expect(registry.entries()).to.deep.equal([
+        { key: 'a', value: 'test1' },
+        { key: 'b', value: 'test2' },
+      ]);
+    });
+  });
+
+  describe('.entriesAsPromise()', () => {
+    it('returns a Promise of an array { key, value }', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'test1');
+      registry.registerLoader('b', () => 'test2');
+      registry.registerLoader('c', () => Promise.resolve('test3'));
+      return registry.entriesAsPromise().then((entries) => {
+        expect(entries).to.deep.equal([
+          { key: 'a', value: 'test1' },
+          { key: 'b', value: 'test2' },
+          { key: 'c', value: 'test3' },
+        ]);
+      });
+    });
+  });
+
+  describe('.remove(key)', () => {
+    it('removes the item with given key', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      registry.remove('a');
+      expect(registry.get('a')).to.equal(null);
+    });
+    it('does not throw error if the key does not exist', () => {
+      const registry = new Registry();
+      expect(() => registry.remove('a')).to.not.throw();
+    });
+    it('returns itself', () => {
+      const registry = new Registry();
+      registry.registerValue('a', 'testValue');
+      expect(registry.remove('a')).to.equal(registry);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/utils/isRequired_spec.js b/superset/assets/spec/javascripts/utils/isRequired_spec.js
new file mode 100644
index 0000000..9a41663
--- /dev/null
+++ b/superset/assets/spec/javascripts/utils/isRequired_spec.js
@@ -0,0 +1,9 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+import isRequired from '../../../src/utils/isRequired';
+
+describe('isRequired(field)', () => {
+  it('should throw error with the given field in the message', () => {
+    expect(() => isRequired('myField')).to.throw(Error, 'myField is required.');
+  });
+});
diff --git a/superset/assets/spec/javascripts/utils/makeSingleton_spec.js b/superset/assets/spec/javascripts/utils/makeSingleton_spec.js
new file mode 100644
index 0000000..686a89a
--- /dev/null
+++ b/superset/assets/spec/javascripts/utils/makeSingleton_spec.js
@@ -0,0 +1,38 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import makeSingleton from '../../../src/utils/makeSingleton';
+
+describe('makeSingleton()', () => {
+  class Dog {
+    constructor(name) {
+      this.name = name;
+    }
+    sit() {
+      this.isSitting = true;
+    }
+  }
+  describe('makeSingleton(BaseClass)', () => {
+    const getInstance = makeSingleton(Dog);
+
+    it('returns a function for getting singleton instance of a given base class', () =>
{
+      expect(getInstance).to.be.a('Function');
+      expect(getInstance()).to.be.instanceOf(Dog);
+    });
+    it('returned function returns same instance across all calls', () => {
+      expect(getInstance()).to.equal(getInstance());
+    });
+  });
+  describe('makeSingleton(BaseClass, ...args)', () => {
+    const getInstance = makeSingleton(Dog, 'Doug');
+
+    it('returns a function for getting singleton instance of a given base class constructed
with the given arguments', () => {
+      expect(getInstance).to.be.a('Function');
+      expect(getInstance()).to.be.instanceOf(Dog);
+      expect(getInstance().name).to.equal('Doug');
+    });
+    it('returned function returns same instance across all calls', () => {
+      expect(getInstance()).to.equal(getInstance());
+    });
+  });
+
+});
diff --git a/superset/assets/spec/javascripts/visualizations/models/ChartPlugin_spec.js b/superset/assets/spec/javascripts/visualizations/models/ChartPlugin_spec.js
new file mode 100644
index 0000000..fb1c35f
--- /dev/null
+++ b/superset/assets/spec/javascripts/visualizations/models/ChartPlugin_spec.js
@@ -0,0 +1,42 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import ChartPlugin from '../../../../src/visualizations/core/models/ChartPlugin';
+import ChartMetadata from '../../../../src/visualizations/core/models/ChartMetadata';
+
+describe('ChartPlugin', () => {
+  const metadata = new ChartMetadata({});
+
+  it('exists', () => {
+    expect(ChartPlugin).to.not.equal(undefined);
+  });
+
+  describe('new ChartPlugin()', () => {
+    it('creates a new plugin', () => {
+      const plugin = new ChartPlugin({
+        metadata,
+        Chart() {},
+      });
+      expect(plugin).to.be.instanceof(ChartPlugin);
+    });
+    it('throws an error if metadata is not specified', () => {
+      expect(() => new ChartPlugin()).to.throw(Error);
+    });
+    it('throws an error if none of Chart or loadChart is specified', () => {
+      expect(() => new ChartPlugin({ metadata })).to.throw(Error);
+    });
+  });
+
+  describe('.register(key)', () => {
+    const plugin = new ChartPlugin({
+      metadata,
+      Chart() {},
+    });
+    it('throws an error if key is not provided', () => {
+      expect(() => plugin.register()).to.throw(Error);
+      expect(() => plugin.configure({ key: 'abc' }).register()).to.not.throw(Error);
+    });
+    it('returns itself', () => {
+      expect(plugin.configure({ key: 'abc' }).register()).to.equal(plugin);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/visualizations/models/Plugin_spec.js b/superset/assets/spec/javascripts/visualizations/models/Plugin_spec.js
new file mode 100644
index 0000000..c40aeb6
--- /dev/null
+++ b/superset/assets/spec/javascripts/visualizations/models/Plugin_spec.js
@@ -0,0 +1,48 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import Plugin from '../../../../src/visualizations/core/models/Plugin';
+
+describe('Plugin', () => {
+  it('exists', () => {
+    expect(Plugin).to.not.equal(undefined);
+  });
+
+  describe('new Plugin()', () => {
+    it('creates a new plugin', () => {
+      const plugin = new Plugin();
+      expect(plugin).to.be.instanceof(Plugin);
+    });
+  });
+
+  describe('.configure(config, replace)', () => {
+    it('extends the default config with given config when replace is not set or false', ()
=> {
+      const plugin = new Plugin();
+      plugin.configure({ key: 'abc', foo: 'bar' });
+      plugin.configure({ key: 'def' });
+      expect(plugin.config).to.deep.equal({ key: 'def', foo: 'bar' });
+    });
+    it('replaces the default config with given config when replace is true', () => {
+      const plugin = new Plugin();
+      plugin.configure({ key: 'abc', foo: 'bar' });
+      plugin.configure({ key: 'def' }, true);
+      expect(plugin.config).to.deep.equal({ key: 'def' });
+    });
+    it('returns the plugin itself', () => {
+      const plugin = new Plugin();
+      expect(plugin.configure({ key: 'abc' })).to.equal(plugin);
+    });
+  });
+
+  describe('.resetConfig()', () => {
+    it('resets config back to default', () => {
+      const plugin = new Plugin();
+      plugin.configure({ key: 'abc', foo: 'bar' });
+      plugin.resetConfig();
+      expect(plugin.config).to.deep.equal({});
+    });
+    it('returns the plugin itself', () => {
+      const plugin = new Plugin();
+      expect(plugin.resetConfig()).to.equal(plugin);
+    });
+  });
+});
diff --git a/superset/assets/spec/javascripts/visualizations/models/Preset_spec.js b/superset/assets/spec/javascripts/visualizations/models/Preset_spec.js
new file mode 100644
index 0000000..4e5f7d2
--- /dev/null
+++ b/superset/assets/spec/javascripts/visualizations/models/Preset_spec.js
@@ -0,0 +1,65 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+import Preset from '../../../../src/visualizations/core/models/Preset';
+import Plugin from '../../../../src/visualizations/core/models/Plugin';
+
+describe('Preset', () => {
+  it('exists', () => {
+    expect(Preset).to.not.equal(undefined);
+  });
+
+  describe('new Preset()', () => {
+    it('creates new preset', () => {
+      const preset = new Preset();
+      expect(preset).to.be.instanceOf(Preset);
+    });
+  });
+
+  describe('.register()', () => {
+    it('register all listed presets then plugins', () => {
+      const values = [];
+      class Plugin1 extends Plugin {
+        register() {
+          values.push(1);
+        }
+      }
+      class Plugin2 extends Plugin {
+        register() {
+          values.push(2);
+        }
+      }
+      class Plugin3 extends Plugin {
+        register() {
+          values.push(3);
+        }
+      }
+      class Plugin4 extends Plugin {
+        register() {
+          const { key } = this.config;
+          values.push(key);
+        }
+      }
+
+      const preset1 = new Preset({
+        plugins: [new Plugin1()],
+      });
+      const preset2 = new Preset({
+        plugins: [new Plugin2()],
+      });
+      const preset3 = new Preset({
+        presets: [preset1, preset2],
+        plugins: [
+          new Plugin3(),
+          new Plugin4().configure({ key: 'abc' }),
+        ],
+      });
+      preset3.register();
+      expect(values).to.deep.equal([1, 2, 3, 'abc']);
+    });
+
+    it('returns itself', () => {
+      const preset = new Preset();
+      expect(preset.register()).to.equal(preset);
+    });
+  });
+});
diff --git a/superset/assets/src/modules/Registry.js b/superset/assets/src/modules/Registry.js
new file mode 100644
index 0000000..f39a0c5
--- /dev/null
+++ b/superset/assets/src/modules/Registry.js
@@ -0,0 +1,72 @@
+export default class Registry {
+  constructor(name = '') {
+    this.name = name;
+    this.items = {};
+    this.promises = {};
+  }
+
+  has(key) {
+    const item = this.items[key];
+    return item !== null && item !== undefined;
+  }
+
+  registerValue(key, value) {
+    this.items[key] = { value };
+    delete this.promises[key];
+    return this;
+  }
+
+  registerLoader(key, loader) {
+    this.items[key] = { loader };
+    delete this.promises[key];
+    return this;
+  }
+
+  get(key) {
+    const item = this.items[key];
+    if (item) {
+      return item.loader ? item.loader() : item.value;
+    }
+    return null;
+  }
+
+  getAsPromise(key) {
+    const promise = this.promises[key];
+    if (promise) {
+      return promise;
+    }
+    const item = this.get(key);
+    if (item) {
+      const newPromise = Promise.resolve(item);
+      this.promises[key] = newPromise;
+      return newPromise;
+    }
+    return Promise.reject(`Item with key "${key}" is not registered.`);
+  }
+
+  keys() {
+    return Object.keys(this.items);
+  }
+
+  entries() {
+    return this.keys().map(key => ({
+      key,
+      value: this.get(key),
+    }));
+  }
+
+  entriesAsPromise() {
+    const keys = this.keys();
+    return Promise.all(keys.map(key => this.getAsPromise(key)))
+      .then(values => values.map((value, i) => ({
+        key: keys[i],
+        value,
+      })));
+  }
+
+  remove(key) {
+    delete this.items[key];
+    delete this.promises[key];
+    return this;
+  }
+}
diff --git a/superset/assets/src/utils/isRequired.js b/superset/assets/src/utils/isRequired.js
new file mode 100644
index 0000000..988c8ce
--- /dev/null
+++ b/superset/assets/src/utils/isRequired.js
@@ -0,0 +1,3 @@
+export default function isRequired(field) {
+  throw new Error(`${field} is required.`);
+}
diff --git a/superset/assets/src/utils/makeSingleton.js b/superset/assets/src/utils/makeSingleton.js
new file mode 100644
index 0000000..3eee475
--- /dev/null
+++ b/superset/assets/src/utils/makeSingleton.js
@@ -0,0 +1,10 @@
+export default function makeSingleton(BaseClass, ...args) {
+  let singleton;
+
+  return function getInstance() {
+    if (!singleton) {
+      singleton = new BaseClass(...args);
+    }
+    return singleton;
+  };
+}
diff --git a/superset/assets/src/visualizations/core/models/ChartMetadata.js b/superset/assets/src/visualizations/core/models/ChartMetadata.js
new file mode 100644
index 0000000..3d528b8
--- /dev/null
+++ b/superset/assets/src/visualizations/core/models/ChartMetadata.js
@@ -0,0 +1,13 @@
+export default class ChartMetadata {
+  constructor({
+    name,
+    description,
+    thumbnail,
+    show = true,
+  }) {
+    this.name = name;
+    this.description = description;
+    this.thumbnail = thumbnail;
+    this.show = show;
+  }
+}
diff --git a/superset/assets/src/visualizations/core/models/ChartPlugin.js b/superset/assets/src/visualizations/core/models/ChartPlugin.js
new file mode 100644
index 0000000..a910185
--- /dev/null
+++ b/superset/assets/src/visualizations/core/models/ChartPlugin.js
@@ -0,0 +1,43 @@
+import Plugin from './Plugin';
+import isRequired from '../../../utils/isRequired';
+import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
+import getChartComponentRegistry from '../registries/ChartComponentRegistrySingleton';
+import getChartTransformPropsRegistry from '../registries/ChartTransformPropsRegistrySingleton';
+
+const IDENTITY = x => x;
+
+export default class ChartPlugin extends Plugin {
+  constructor({
+    metadata = isRequired('metadata'),
+
+    // use transformProps for immediate value
+    transformProps = IDENTITY,
+    // use loadTransformProps for dynamic import (lazy-loading)
+    loadTransformProps,
+
+    // use Chart for immediate value
+    Chart,
+    // use loadChart for dynamic import (lazy-loading)
+    loadChart,
+  } = {}) {
+    super();
+    this.metadata = metadata;
+    this.loadTransformProps = loadTransformProps || (() => transformProps);
+
+    if (loadChart) {
+      this.loadChart = loadChart;
+    } else if (Chart) {
+      this.loadChart = () => Chart;
+    } else {
+      throw new Error('Chart or loadChart is required');
+    }
+  }
+
+  register() {
+    const { key = isRequired('config.key') } = this.config;
+    getChartMetadataRegistry().registerValue(key, this.metadata);
+    getChartComponentRegistry().registerLoader(key, this.loadChart);
+    getChartTransformPropsRegistry().registerLoader(key, this.loadTransformProps);
+    return this;
+  }
+}
diff --git a/superset/assets/src/visualizations/core/models/Plugin.js b/superset/assets/src/visualizations/core/models/Plugin.js
new file mode 100644
index 0000000..33609f6
--- /dev/null
+++ b/superset/assets/src/visualizations/core/models/Plugin.js
@@ -0,0 +1,25 @@
+export default class Plugin {
+  constructor() {
+    this.resetConfig();
+  }
+
+  resetConfig() {
+    // The child class can set default config
+    // by overriding this function.
+    this.config = {};
+    return this;
+  }
+
+  configure(config, replace = false) {
+    if (replace) {
+      this.config = config;
+    } else {
+      this.config = { ...this.config, ...config };
+    }
+    return this;
+  }
+
+  register() {
+    return this;
+  }
+}
diff --git a/superset/assets/src/visualizations/core/models/Preset.js b/superset/assets/src/visualizations/core/models/Preset.js
new file mode 100644
index 0000000..557351d
--- /dev/null
+++ b/superset/assets/src/visualizations/core/models/Preset.js
@@ -0,0 +1,23 @@
+export default class Preset {
+  constructor({
+    name = '',
+    description = '',
+    presets = [],
+    plugins = [],
+  } = {}) {
+    this.name = name;
+    this.description = description;
+    this.presets = presets;
+    this.plugins = plugins;
+  }
+
+  register() {
+    this.presets.forEach((preset) => {
+      preset.register();
+    });
+    this.plugins.forEach((plugin) => {
+      plugin.register();
+    });
+    return this;
+  }
+}
diff --git a/superset/assets/src/visualizations/core/registries/ChartComponentRegistrySingleton.js
b/superset/assets/src/visualizations/core/registries/ChartComponentRegistrySingleton.js
new file mode 100644
index 0000000..df7d743
--- /dev/null
+++ b/superset/assets/src/visualizations/core/registries/ChartComponentRegistrySingleton.js
@@ -0,0 +1,12 @@
+import Registry from '../../../modules/Registry';
+import makeSingleton from '../../../utils/makeSingleton';
+
+class ChartComponentRegistry extends Registry {
+  constructor() {
+    super('ChartComponent');
+  }
+}
+
+const getInstance = makeSingleton(ChartComponentRegistry);
+
+export default getInstance;
diff --git a/superset/assets/src/visualizations/core/registries/ChartMetadataRegistrySingleton.js
b/superset/assets/src/visualizations/core/registries/ChartMetadataRegistrySingleton.js
new file mode 100644
index 0000000..e1c569b
--- /dev/null
+++ b/superset/assets/src/visualizations/core/registries/ChartMetadataRegistrySingleton.js
@@ -0,0 +1,12 @@
+import Registry from '../../../modules/Registry';
+import makeSingleton from '../../../utils/makeSingleton';
+
+class ChartMetadataRegistry extends Registry {
+  constructor() {
+    super('ChartMetadata');
+  }
+}
+
+const getInstance = makeSingleton(ChartMetadataRegistry);
+
+export default getInstance;
diff --git a/superset/assets/src/visualizations/core/registries/ChartTransformPropsRegistrySingleton.js
b/superset/assets/src/visualizations/core/registries/ChartTransformPropsRegistrySingleton.js
new file mode 100644
index 0000000..a26fab5
--- /dev/null
+++ b/superset/assets/src/visualizations/core/registries/ChartTransformPropsRegistrySingleton.js
@@ -0,0 +1,12 @@
+import Registry from '../../../modules/Registry';
+import makeSingleton from '../../../utils/makeSingleton';
+
+class ChartTransformPropsRegistry extends Registry {
+  constructor() {
+    super('ChartTransformProps');
+  }
+}
+
+const getInstance = makeSingleton(ChartTransformPropsRegistry);
+
+export default getInstance;


Mime
View raw message