Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data structures for chart plugin system #6028

Merged
merged 20 commits into from
Oct 9, 2018
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions superset/assets/spec/javascripts/modules/Registry_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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('.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);
});
});
});
9 changes: 9 additions & 0 deletions superset/assets/spec/javascripts/utils/isRequired_spec.js
Original file line number Diff line number Diff line change
@@ -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.');
});
});
38 changes: 38 additions & 0 deletions superset/assets/spec/javascripts/utils/makeSingleton_spec.js
Original file line number Diff line number Diff line change
@@ -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());
});
});

});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
52 changes: 52 additions & 0 deletions superset/assets/src/modules/Registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my only comment at this point is whether we should throw an error if a value already exists, that would require people to consciously call .remove() and possibly raise this to their attn if they weren't aware of it?

or we could just log a warning 🤔 wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may happen a lot though for the presets. For example, registering a preset that contains all common charts then override bar chart with a new plugin. Some presets may reference another preset and override certain charts.

I think throwing an error would be too harsh.
Can add warning but then that will mean for every plugin we have to add .remove() call before .register() to avoid the warning. If every plugin does that, would a warning still be useful?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's a fair point, sgtm 👍

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.`);
}

remove(key) {
delete this.items[key];
delete this.promises[key];
return this;
}
}
3 changes: 3 additions & 0 deletions superset/assets/src/utils/isRequired.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function isRequired(field) {
throw new Error(`${field} is required.`);
}
Loading