[FIX JENKINS-35860] - extension point component type filter (#308)
* Add component type filter for extensionPoints * Bump js-extensions beta version * EXTENSIONS -> README * more documentation for dataType vs. componentType * modify usage of 'type' to 'dataType' for consistency * Split ClassMetadataStore, modify extension filtering to a common method * Separate componentType to its own file
This commit is contained in:
parent
84692913ad
commit
7eaf71b391
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "0.0.63",
|
||||
"@jenkins-cd/js-extensions": "0.0.17-beta-1",
|
||||
"@jenkins-cd/js-extensions": "0.0.19",
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"@jenkins-cd/sse-gateway": "0.0.6",
|
||||
"immutable": "3.8.1",
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react';
|
|||
import { EmptyStateView } from '@jenkins-cd/design-language';
|
||||
import { actions as selectorActions, testResults as testResultsSelector,
|
||||
connect, createSelector } from '../redux';
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
import Extensions, { dataType } from '@jenkins-cd/js-extensions';
|
||||
|
||||
const EmptyState = () => (
|
||||
<EmptyStateView tightSpacing>
|
||||
|
@ -58,7 +58,7 @@ export class RunDetailsTests extends Component {
|
|||
<div className="test-result-duration">Duration {testResults.duration}</div>
|
||||
</div>
|
||||
|
||||
<Extensions.Renderer extensionPoint="jenkins.test.result" dataType={testResults} testResults={testResults} />
|
||||
<Extensions.Renderer extensionPoint="jenkins.test.result" filter={dataType(testResults)} testResults={testResults} />
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,4 +12,4 @@ extensions:
|
|||
extensionPoint: jenkins.pipeline.run.result
|
||||
- component: components/testing/TestResults
|
||||
extensionPoint: jenkins.test.result
|
||||
type: hudson.tasks.test.TestResult
|
||||
dataType: hudson.tasks.test.TestResult
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/design-language": "0.0.63",
|
||||
"@jenkins-cd/js-extensions": "0.0.17-beta-1",
|
||||
"@jenkins-cd/js-extensions": "0.0.19",
|
||||
"@jenkins-cd/js-modules": "0.0.5",
|
||||
"history": "2.0.2",
|
||||
"immutable": "3.8.1",
|
||||
|
|
|
@ -57,9 +57,9 @@ exports.initialize = function (oncomplete) {
|
|||
// Get the extension list metadata from Jenkins.
|
||||
// Might want to do some flux fancy-pants stuff for this.
|
||||
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
|
||||
Extensions.store.init({
|
||||
Extensions.init({
|
||||
extensionDataProvider: cb => getURL(`${appRoot}/js-extensions`, rsp => cb(rsp.data)),
|
||||
typeInfoProvider: (type, cb) => getURL(`${appRoot}/rest/classes/${type}`, cb)
|
||||
classMetadataProvider: (type, cb) => getURL(`${appRoot}/rest/classes/${type}`, cb)
|
||||
});
|
||||
oncomplete();
|
||||
};
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
# Jenkins JavaScript Extensions
|
||||
|
||||
Jenkins JavasScript Extensions (JSe) in BlueOcean are handled in the UI by the `@jenkins-cd/js-extensions` module.
|
||||
|
||||
JSe is based on the extensibility model already established by Jenkins, based on data and views, with the ability to inherit views based on parent data types.
|
||||
|
||||
JSe `@jenkins-cd/js-extensions` module exports 2 things:
|
||||
- `store` - the `ExtensionStore` instance, which must be initialized
|
||||
- `Renderer` - a React component to conveniently render extensions
|
||||
|
||||
### Store API
|
||||
|
||||
The `ExtensionStore` API is very simple, all public methods are asynchronous:
|
||||
|
||||
- `getExtensions(extensionPointName, [type,] onload)`
|
||||
This method will async load data and type information as needed, and call the onload handler with a list of extension exports, e.g. the React classes or otherwise exported references.
|
||||
|
||||
- `getTypeInfo(type, onload)`
|
||||
This will return a list of type information, from the [classes API](../blueocean-rest/README.md#classes_API), this method also handles caching results locally.
|
||||
|
||||
- `init()`
|
||||
Required to be called with `{ extensionDataProvider: ..., typeInfoProvider: }` see: [ExtensionStore.js](src/ExtensionStore.js#init) for details. This is currently done in [init.jsx](../blueocean-web/src/main/js/init.jsx), with methods to fetch extension data from `<jenkins-url>/blue/js-extensions/` and type information from `<jenkins-url>/blue/rest/classes/<class-name>`.
|
||||
|
||||
### Rendering extension points
|
||||
|
||||
The most common usage pattern is to use the exported `Renderer`, specifying the extension point name, any necessary contextual data, and optionally specifying a data type.
|
||||
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
...
|
||||
<Extensions.Renderer extensionPoint="jenkins.navigation.top.menu" />
|
||||
|
||||
For example, rendering the test results for a build may be scoped to the specific type of test results in this manner:
|
||||
|
||||
<Extensions.Renderer extensionPoint="test-results-view" dataType={data._class} testResults={data} />
|
||||
|
||||
The `ExtensionRenderer` component optionally uses the [classes API](../blueocean-rest/README.md#classes_API) to look up an appropriate, specific set of views for the data being displayed. This should works seamlessly with other [capabilities](../blueocean-rest/README.md#capabilities).
|
||||
|
||||
|
||||
### Defining extension points
|
||||
|
||||
Extensions are defined in a `jenkins-js-extensions.yaml` file in the javascript source directory of a plugin by defining a list of extensions similar to this:
|
||||
|
||||
# Extensions in this plugin
|
||||
extensions:
|
||||
- component: AboutNavLink
|
||||
extensionPoint: jenkins.topNavigation.menu
|
||||
- component: components/tests/AbstractTestResult
|
||||
extensionPoint: jenkins.test.result
|
||||
type: hudson.tasks.test.AbstractTestResultAction
|
||||
|
||||
Properties are:
|
||||
- `component`: a module from which the default export will be used
|
||||
- `extensionPoint`: the extension point name
|
||||
- `type`: an optional data type this extension handles
|
||||
|
||||
For example, the `AboutNavLink` might be defined as a default export:
|
||||
|
||||
export default class NavLink extends React.Component {
|
||||
...
|
||||
}
|
||||
|
||||
Although extensions are not limited to React components, this is the typical usage so far.
|
|
@ -1,4 +1,80 @@
|
|||
# ExtensionPoint npm module
|
||||
# Jenkins JavaScript Extensions
|
||||
|
||||
This module is published via npm so that other plugins, external to the blueocean project, can make use of it.
|
||||
Plugins can themselves make use of extension points.
|
||||
Jenkins JavaScript Extensions are extensions which contribute to the UI in Jenkins BlueOcean.
|
||||
This module is used to define extension points - locations where the application accepts plugin-provided implementations.
|
||||
This module is also used to process the plugin extension point implementations to provide to BlueOcean.
|
||||
|
||||
This module is published via npm as `@jenkins-cd/js-extensions` so that other plugins, external to the blueocean project, can make use of it.
|
||||
Plugins can themselves make use of extension points.
|
||||
|
||||
Jenkins JavaScript Extensions are based on the extensibility model already established by Jenkins, based on data and views, with the ability to inherit views based on parent data types.
|
||||
|
||||
Jenkins JavaScript Extensions: `@jenkins-cd/js-extensions` module exports:
|
||||
- `Renderer` - a React component to conveniently render extensions
|
||||
- `store` - the `ExtensionStore` instance (which must be initialized before it can be used)
|
||||
- `classMetadataStore` - class/capability metadata store
|
||||
- `dataType()` - function for filtering extensions based on the data type
|
||||
- `componentType()` - function for filtering extensions based on the required component type (e.g. React class)
|
||||
|
||||
### ExtensionStore API
|
||||
|
||||
The `ExtensionStore` API is very simple, all public methods are asynchronous:
|
||||
|
||||
- `getExtensions(extensionPointName, [filter,] onload)`
|
||||
This method will async load data, filter the extensions based on the provided `filter`s, and call the onload handler with a list of extension exports, e.g. the React classes or otherwise exported references.
|
||||
`filter` - a filter function currently the module exports the following functions - see the exported functions for the commonly used filters
|
||||
|
||||
### ClassMetadataStore API
|
||||
|
||||
- `getClassMetadata(dataType, onload)`
|
||||
This will return a list of type information, from the [classes API](../blueocean-rest/README.md#classes_API), this method also handles caching results locally.
|
||||
|
||||
### Rendering extension points
|
||||
|
||||
The most common usage pattern is to use the exported `Renderer`, specifying the extension point name, any necessary contextual data, and optionally specifying a data type.
|
||||
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
...
|
||||
<Extensions.Renderer extensionPoint="jenkins.navigation.top.menu" />
|
||||
|
||||
For example, rendering the test results for a build may be scoped to the specific type of test results in this manner:
|
||||
|
||||
<Extensions.Renderer extensionPoint="test-results-view" filter={dataType(data)} testResults={data} />
|
||||
|
||||
The `ExtensionRenderer` component optionally uses the [classes API](../blueocean-rest/README.md#classes_API) to look up an appropriate, specific set of views for the data being displayed.
|
||||
This should works seamlessly with other [capabilities](../blueocean-rest/README.md#capabilities).
|
||||
|
||||
|
||||
### Defining extension point implementations
|
||||
|
||||
Extensions are defined in a `jenkins-js-extensions.yaml` file in the javascript source directory of a plugin by defining a list of extensions similar to this:
|
||||
|
||||
# Extensions in this plugin
|
||||
extensions:
|
||||
- component: AboutNavLink
|
||||
extensionPoint: jenkins.topNavigation.menu
|
||||
- component: components/tests/AbstractTestResult
|
||||
extensionPoint: jenkins.test.result
|
||||
dataType: hudson.tasks.test.AbstractTestResultAction
|
||||
|
||||
Properties are:
|
||||
- `component`: a module from which the default export will be used
|
||||
- `extensionPoint`: the extension point name
|
||||
- `dataType`: an optional Java data type this extension handles
|
||||
|
||||
For example, the `AboutNavLink` might be defined as a default export:
|
||||
|
||||
export default class NavLink extends React.Component {
|
||||
...
|
||||
}
|
||||
|
||||
#### Enforcing specific component types
|
||||
|
||||
In order to ensure a specific component is returned, an extension point may also use the `componentType` filter - it accepts an object prototype (e.g. an ES6 class), e.g.:
|
||||
|
||||
import TestResults from './base-components/TestResults';
|
||||
...
|
||||
<Extensions.Renderer extensionPoint="test-view" filter={componentType(TestResults)} ... />
|
||||
|
||||
Extensions are not limited to React components.
|
||||
The `componentType` filter will attempt to match returned components by a series of prototype and typeof checks to appropriately filter returned types including ES6 classes.
|
||||
|
|
|
@ -1,3 +1,22 @@
|
|||
// Provide an ExtensionStore & ExtensionRenderer react component
|
||||
exports.store = require('./dist/ExtensionStore.js').instance;
|
||||
|
||||
exports.Renderer = require('./dist/ExtensionRenderer.js').ExtensionRenderer;
|
||||
|
||||
exports.classMetadataStore = require('./dist/ClassMetadataStore.js').instance;
|
||||
|
||||
exports.dataType = function dataType(dataType) { return exports.classMetadataStore.dataType(dataType); };
|
||||
|
||||
exports.untyped = function untyped() { return exports.classMetadataStore.untyped(); };
|
||||
|
||||
exports.isType = require('./dist/ComponentTypeFilter.js').isType;
|
||||
|
||||
exports.componentType = require('./dist/ComponentTypeFilter.js').componentType;
|
||||
|
||||
exports.init = function init(args) {
|
||||
exports.classMetadataStore.init(args.classMetadataProvider);
|
||||
exports.store.init({
|
||||
extensionDataProvider: args.extensionDataProvider,
|
||||
classMetadataStore: exports.classMetadataStore,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@jenkins-cd/js-extensions",
|
||||
"version": "0.0.17-beta-1",
|
||||
"version": "0.0.19",
|
||||
"description": "Jenkins Extension Store",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
var jsTest = require('@jenkins-cd/js-test');
|
||||
var expect = require('chai').expect;
|
||||
var ExtensionStore = require('../dist/ExtensionStore').ExtensionStore;
|
||||
var ClassMetadataStore = require('../dist/ClassMetadataStore').instance;
|
||||
var componentType = require('../dist/ComponentTypeFilter').componentType;
|
||||
var javaScriptExtensionInfo = require('./javaScriptExtensionInfo-01.json');
|
||||
|
||||
// js modules calling console.debug
|
||||
console.debug = function(msg) { console.log('DEBUG: ' + msg); };
|
||||
|
||||
var mockDataLoad = function(extensionStore, out) {
|
||||
var jsModules = require('@jenkins-cd/js-modules');
|
||||
// Mock the calls to import
|
||||
var jsModules = require('@jenkins-cd/js-modules');
|
||||
var theRealImport = jsModules.import;
|
||||
|
||||
var makeClassMetadataStore = function(fn) {
|
||||
ClassMetadataStore.init(fn);
|
||||
return ClassMetadataStore;
|
||||
};
|
||||
|
||||
var mockDataLoad = function(extensionStore, out, componentMap) {
|
||||
out.plugins = {};
|
||||
out.loadCount = 0;
|
||||
|
||||
// Mock the calls to import
|
||||
var theRealImport = jsModules.import;
|
||||
|
||||
jsModules.import = function(bundleId) {
|
||||
var internal = require('@jenkins-cd/js-modules/js/internal');
|
||||
var bundleModuleSpec = internal.parseResourceQName(bundleId);
|
||||
|
@ -25,7 +32,11 @@ var mockDataLoad = function(extensionStore, out) {
|
|||
if (pluginMetadata.hpiPluginId === pluginId) {
|
||||
var extensions = pluginMetadata.extensions;
|
||||
for(var i2 = 0; i2 < extensions.length; i2++) {
|
||||
extensionStore._registerComponentInstance(extensions[i2].extensionPoint, pluginMetadata.hpiPluginId, extensions[i2].component, extensions[i2].component);
|
||||
var component = extensions[i2].component;
|
||||
if (componentMap && component in componentMap) {
|
||||
component = componentMap[component];
|
||||
}
|
||||
extensionStore._registerComponentInstance(extensions[i2].extensionPoint, pluginMetadata.hpiPluginId, extensions[i2].component, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +79,7 @@ describe("ExtensionStore.js", function () {
|
|||
|
||||
extensionStore.init({
|
||||
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
|
||||
typeInfoProvider: function(type, cb) { cb({}); }
|
||||
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ep-1', function(extensions) {
|
||||
|
@ -89,7 +100,7 @@ describe("ExtensionStore.js", function () {
|
|||
// this info will be loaded from <jenkins>/blue/js-extensions/
|
||||
extensionStore.init({
|
||||
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
|
||||
typeInfoProvider: function(type, cb) { cb({}); }
|
||||
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
|
||||
});
|
||||
|
||||
// Call load for ExtensionPoint impls 'ep-1'. This should mimic
|
||||
|
@ -146,21 +157,21 @@ describe("ExtensionStore.js", function () {
|
|||
|
||||
extensionStore.init({
|
||||
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
|
||||
typeInfoProvider: function(type, cb) { cb(typeData[type]); }
|
||||
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb(typeData[type]); })
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ept-1', 'type-1', function(extensions) {
|
||||
expect(extensionStore.typeInfo).to.not.be.undefined;
|
||||
extensionStore.getExtensions('ept-1', [ClassMetadataStore.dataType('type-1')], function(extensions) {
|
||||
expect(extensions.length).to.equal(1);
|
||||
|
||||
expect(extensions[0]).to.equal('typed-component-1.1');
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ept-2', 'type-2', function(extensions) {
|
||||
extensionStore.getExtensions('ept-2', [ClassMetadataStore.dataType('type-2')], function(extensions) {
|
||||
expect(extensions.length).to.equal(1);
|
||||
expect(extensions).to.include.members(["typed-component-1.2"]);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("- handles untyped extension points", function(done) {
|
||||
|
@ -191,20 +202,20 @@ describe("ExtensionStore.js", function () {
|
|||
|
||||
extensionStore.init({
|
||||
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
|
||||
typeInfoProvider: function(type, cb) { cb(typeData[type]); }
|
||||
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb(typeData[type]); })
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ep-1', function(extensions) {
|
||||
extensionStore.getExtensions('ep-1', [ClassMetadataStore.untyped()], function(extensions) {
|
||||
expect(extensions.length).to.equal(3);
|
||||
expect(extensions).to.include.members(["component-1.1","component-1.2","component-2.1"]);
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ept-2', function(extensions) {
|
||||
extensionStore.getExtensions('ept-2', [ClassMetadataStore.untyped()], function(extensions) {
|
||||
expect(extensions.length).to.equal(0);
|
||||
expect(extensions).to.include.members([]);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("- handles multi-key requests", function(done) {
|
||||
|
@ -215,7 +226,7 @@ describe("ExtensionStore.js", function () {
|
|||
|
||||
extensionStore.init({
|
||||
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
|
||||
typeInfoProvider: function(type, cb) { cb({}); }
|
||||
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
|
||||
});
|
||||
|
||||
extensionStore.getExtensions(['ep-1','ep-2'], function(ep1,ep2) {
|
||||
|
@ -228,4 +239,50 @@ describe("ExtensionStore.js", function () {
|
|||
|
||||
done();
|
||||
});
|
||||
|
||||
it("- handles componentType", function(done) {
|
||||
var extensionStore = new ExtensionStore();
|
||||
|
||||
class PretendReactClass {
|
||||
}
|
||||
|
||||
class PretendComponent1 extends PretendReactClass {
|
||||
}
|
||||
|
||||
class PretendComponent2 extends PretendReactClass {
|
||||
}
|
||||
|
||||
var plugins = {};
|
||||
mockDataLoad(extensionStore, plugins, {
|
||||
'component-1.3.1': PretendComponent1,
|
||||
'component-2.3.1': PretendComponent2,
|
||||
});
|
||||
|
||||
extensionStore.init({
|
||||
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
|
||||
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ep-3', [componentType(PretendComponent1)], function(extensions) {
|
||||
expect(extensions.length).to.equal(1);
|
||||
expect(extensions).to.include.members([PretendComponent1]);
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ep-3', [componentType(PretendComponent2)], function(extensions) {
|
||||
expect(extensions.length).to.equal(1);
|
||||
expect(extensions).to.include.members([PretendComponent2]);
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ep-3', [componentType(PretendReactClass)], function(extensions) {
|
||||
expect(extensions.length).to.equal(2);
|
||||
expect(extensions).to.include.members([PretendComponent1, PretendComponent2]);
|
||||
});
|
||||
|
||||
extensionStore.getExtensions('ep-3', [componentType(PretendReactClass), componentType(PretendComponent1)], function(extensions) {
|
||||
expect(extensions.length).to.equal(1);
|
||||
expect(extensions).to.include.members([PretendComponent1]);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
{
|
||||
"component": "component-1.3",
|
||||
"extensionPoint": "ep-2"
|
||||
},
|
||||
{
|
||||
"component": "component-1.3.1",
|
||||
"extensionPoint": "ep-3"
|
||||
}
|
||||
],
|
||||
"hpiPluginId": "plugin-1"
|
||||
|
@ -29,6 +33,10 @@
|
|||
{
|
||||
"component": "component-2.3",
|
||||
"extensionPoint": "ep-2"
|
||||
},
|
||||
{
|
||||
"component": "component-2.3.1",
|
||||
"extensionPoint": "ep-3"
|
||||
}
|
||||
],
|
||||
"hpiPluginId": "plugin-2"
|
||||
|
@ -38,22 +46,22 @@
|
|||
{
|
||||
"component": "typed-component-1.1",
|
||||
"extensionPoint": "ept-1",
|
||||
"type": "supertype-1"
|
||||
"dataType": "supertype-1"
|
||||
},
|
||||
{
|
||||
"component": "typed-component-1.2",
|
||||
"extensionPoint": "ept-2",
|
||||
"type": "type-2"
|
||||
"dataType": "type-2"
|
||||
},
|
||||
{
|
||||
"component": "typed-component-1.2.1",
|
||||
"extensionPoint": "ept-2",
|
||||
"type": "subtype-2"
|
||||
"dataType": "subtype-2"
|
||||
},
|
||||
{
|
||||
"component": "typed-component-1.2.2",
|
||||
"extensionPoint": "ept-2",
|
||||
"type": "supertype-2"
|
||||
"dataType": "supertype-2"
|
||||
}
|
||||
],
|
||||
"hpiPluginId": "plugin-3",
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* ClassMetadataStore is responsible for maintaining extension metadata
|
||||
* including type/capability info
|
||||
*/
|
||||
export class ClassMetadataStore {
|
||||
init(classMetadataProvider) {
|
||||
/**
|
||||
* Type info cache
|
||||
*/
|
||||
this.classMetadata = {};
|
||||
|
||||
/**
|
||||
* Fetch function for the classMetadata
|
||||
*/
|
||||
this.classMetadataProvider = classMetadataProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type/capability info for the given data type
|
||||
*/
|
||||
getClassMetadata(type, onload) {
|
||||
var classMeta = this.classMetadata[type];
|
||||
if (classMeta) {
|
||||
return onload(classMeta);
|
||||
}
|
||||
this.classMetadataProvider(type, (data) => {
|
||||
classMeta = this.classMetadata[type] = JSON.parse(JSON.stringify(data));
|
||||
classMeta.classes = classMeta.classes || [];
|
||||
// Make sure the type itself is in the list
|
||||
if (classMeta.classes.indexOf(type) < 0) {
|
||||
classMeta.classes = [type, ...classMeta.classes];
|
||||
}
|
||||
onload(classMeta);
|
||||
});
|
||||
}
|
||||
|
||||
dataType(dataType) {
|
||||
return (extensions, onload) => {
|
||||
if (dataType && typeof(dataType) === 'object'
|
||||
&& '_class' in dataType) { // handle the common API incoming data
|
||||
dataType = dataType._class;
|
||||
}
|
||||
|
||||
this.getClassMetadata(dataType, (currentTypeInfo) => {
|
||||
// prevent returning extensions for the given type
|
||||
// when a more specific extension is found
|
||||
var matchingExtensions = [];
|
||||
eachType: for (var typeIndex = 0; typeIndex < currentTypeInfo.classes.length; typeIndex++) {
|
||||
// currentTypeInfo.classes is ordered by java hierarchy, including
|
||||
// and beginning with the current data type
|
||||
var type = currentTypeInfo.classes[typeIndex];
|
||||
for (var i = 0; i < extensions.length; i++) {
|
||||
var extension = extensions[i];
|
||||
if (type === extension.dataType) {
|
||||
matchingExtensions.push(extension);
|
||||
}
|
||||
}
|
||||
// if we have this specific type handled, don't
|
||||
// proceed to parent types
|
||||
if (matchingExtensions.length > 0) {
|
||||
break eachType;
|
||||
}
|
||||
}
|
||||
onload(matchingExtensions);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtering function to only return untyped extensions
|
||||
*/
|
||||
untyped() {
|
||||
return (extensions, onload) => {
|
||||
// exclude typed extensions when types not requested
|
||||
extensions = extensions.filter(m => !('dataType' in m));
|
||||
onload(extensions);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const instance = new ClassMetadataStore();
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* Tries to determine if the objectToTest is of the given type.
|
||||
* Will normalize things like String/'string' inconsistencies
|
||||
* as well as ES6 class & traditional prototype inheritance.
|
||||
* NOTE: This ALSO tests the prototype hierarchy if objectToTest
|
||||
* is a Function.
|
||||
*/
|
||||
export function isType(objectToTest, type) {
|
||||
var o = objectToTest;
|
||||
if (typeof o === type) {
|
||||
return true;
|
||||
}
|
||||
if (type === String || type === 'string') {
|
||||
return o instanceof String;
|
||||
}
|
||||
if (type === Function || type === 'function') {
|
||||
return o instanceof Function;
|
||||
}
|
||||
if (type === Object || type === 'object') {
|
||||
return o instanceof Object;
|
||||
}
|
||||
if (objectToTest instanceof Function) {
|
||||
var proto = objectToTest;
|
||||
while (proto) {
|
||||
if (proto === type) {
|
||||
return true;
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
}
|
||||
return objectToTest instanceof type;
|
||||
}
|
||||
|
||||
export function componentType(componentType) {
|
||||
return (extensions, onload) => {
|
||||
extensions = extensions.filter(e => isType(e.instance, componentType));
|
||||
onload(extensions);
|
||||
};
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var ExtensionStore = require('./ExtensionStore.js');
|
||||
var ExtensionStore = require('./ExtensionStore.js').instance;
|
||||
var ResourceLoadTracker = require('./ResourceLoadTracker').instance;
|
||||
|
||||
/**
|
||||
|
@ -62,7 +62,7 @@ export class ExtensionRenderer extends React.Component {
|
|||
}
|
||||
|
||||
_setExtensions() {
|
||||
ExtensionStore.instance.getExtensions(this.props.extensionPoint, this.props.dataType,
|
||||
ExtensionStore.getExtensions(this.props.extensionPoint, this.props.filter,
|
||||
extensions => this.setState({extensions: extensions})
|
||||
);
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ ExtensionRenderer.defaultProps = {
|
|||
|
||||
ExtensionRenderer.propTypes = {
|
||||
extensionPoint: React.PropTypes.string.isRequired,
|
||||
dataType: React.PropTypes.any,
|
||||
filter: React.PropTypes.any,
|
||||
wrappingElement: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@ export class ExtensionStore {
|
|||
* FIXME this is NOT a constructor, as there's no common way to
|
||||
* pass around a DI singleton at the moment across everything
|
||||
* that needs it (e.g. redux works for the app, not for other
|
||||
* things in this module)
|
||||
* things in this module).
|
||||
*
|
||||
* NOTE: this is currently called from `blueocean-web/src/main/js/init.jsx`
|
||||
*
|
||||
* Needs:
|
||||
* args = {
|
||||
|
@ -15,9 +17,11 @@ export class ExtensionStore {
|
|||
* ... // get the data
|
||||
* callback(extensionData); // array of extensions
|
||||
* },
|
||||
* typeInfoProvider: (type, callback) => {
|
||||
* ... // get the data based on 'type'
|
||||
* callback(typeInfo);
|
||||
* classMetadataStore: {
|
||||
* getClassMetadata(dataType, callback) => {
|
||||
* ... // get the data based on 'dataType'
|
||||
* callback(typeInfo);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
@ -29,14 +33,10 @@ export class ExtensionStore {
|
|||
* The registered ExtensionPoint metadata + instance refs
|
||||
*/
|
||||
this.extensionPoints = {};
|
||||
/**
|
||||
* Type info cache
|
||||
*/
|
||||
this.typeInfo = {};
|
||||
/**
|
||||
* Used to fetch type information
|
||||
*/
|
||||
this.typeInfoProvider = args.typeInfoProvider;
|
||||
this.classMetadataStore = args.classMetadataStore;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,7 +53,7 @@ export class ExtensionStore {
|
|||
extension.instance = instance;
|
||||
return;
|
||||
}
|
||||
throw `Unable to locate plugin for ${extensionPointId} / ${pluginId} / ${component}`;
|
||||
throw new Error(`Unable to locate plugin for ${extensionPointId} / ${pluginId} / ${component}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,102 +76,69 @@ export class ExtensionStore {
|
|||
* will call the onload callback with a list of exported extension
|
||||
* objects (e.g. React classes or otherwise).
|
||||
*/
|
||||
getExtensions(key, type, onload) {
|
||||
if (!this.extensionDataProvider) {
|
||||
throw "Must call ExtensionStore.init({ extensionDataProvider: (cb) => ..., typeInfoProvider: (type, cb) => ... }) first";
|
||||
}
|
||||
getExtensions(extensionPoint, filter, onload) {
|
||||
// Allow calls like: getExtensions('something', a => ...)
|
||||
if (arguments.length === 2 && typeof(type) === 'function') {
|
||||
onload = type;
|
||||
type = undefined;
|
||||
if (arguments.length === 2 && typeof(filter) === 'function') {
|
||||
onload = filter;
|
||||
filter = undefined;
|
||||
}
|
||||
|
||||
// And calls like: getExtensions(['a','b'], (a,b) => ...)
|
||||
if (key instanceof Array) {
|
||||
var keys = key;
|
||||
if (extensionPoint instanceof Array) {
|
||||
var args = [];
|
||||
var nextArg = ext => {
|
||||
if(ext) args.push(ext);
|
||||
if (keys.length === 0) {
|
||||
if (extensionPoint.length === 0) {
|
||||
onload(...args);
|
||||
} else {
|
||||
var arg = keys[0];
|
||||
keys = keys.slice(1);
|
||||
this.getExtensions(arg, null, nextArg);
|
||||
var arg = extensionPoint[0];
|
||||
extensionPoint = extensionPoint.slice(1);
|
||||
this.getExtensions(arg, filter, nextArg);
|
||||
}
|
||||
};
|
||||
nextArg();
|
||||
return;
|
||||
}
|
||||
|
||||
this._loadBundles(key, extensions => this._filterExtensions(extensions, key, type, onload));
|
||||
this._loadBundles(extensionPoint, extensions => this._filterExtensions(extensions, filter, onload));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type/capability info for the given data type
|
||||
*/
|
||||
getTypeInfo(type, onload) {
|
||||
var ti = this.typeInfo[type];
|
||||
if (ti) {
|
||||
return onload(ti);
|
||||
}
|
||||
this.typeInfoProvider(type, (data) => {
|
||||
ti = this.typeInfo[type] = JSON.parse(JSON.stringify(data));
|
||||
ti.classes = ti.classes || [];
|
||||
if (ti.classes.indexOf(type) < 0) {
|
||||
ti.classes = [type, ...ti.classes];
|
||||
}
|
||||
onload(ti);
|
||||
});
|
||||
}
|
||||
|
||||
_filterExtensions(extensions, key, currentDataType, onload) {
|
||||
if (currentDataType && typeof(currentDataType) === 'object'
|
||||
&& '_class' in currentDataType) { // handle the common API incoming data
|
||||
currentDataType = currentDataType._class;
|
||||
}
|
||||
_filterExtensions(extensions, filters, onload) {
|
||||
if (extensions.length === 0) {
|
||||
onload(extensions); // no extensions for the given key
|
||||
onload(extensions); // no extensions to filter
|
||||
return;
|
||||
}
|
||||
if (currentDataType) {
|
||||
var currentTypeInfo = this.typeInfo[currentDataType];
|
||||
if (!currentTypeInfo) {
|
||||
this.getTypeInfo(currentDataType, () => {
|
||||
this._filterExtensions(extensions, key, currentDataType, onload);
|
||||
});
|
||||
return;
|
||||
|
||||
if (filters) {
|
||||
// allow calls like: getExtensions('abcd', dataType(something), ext => ...)
|
||||
if (!filters.length) {
|
||||
filters = [ filters ];
|
||||
}
|
||||
// prevent returning extensions for the given type
|
||||
// when a more specific extension is found
|
||||
var matchingExtensions = [];
|
||||
eachType: for (var typeIndex = 0; typeIndex < currentTypeInfo.classes.length; typeIndex++) {
|
||||
// currentTypeInfo.classes is ordered by java hierarchy, including
|
||||
// and beginning with the current data type
|
||||
var type = currentTypeInfo.classes[typeIndex];
|
||||
for (var i = 0; i < extensions.length; i++) {
|
||||
var extension = extensions[i];
|
||||
if (type === extension.type) {
|
||||
matchingExtensions.push(extension);
|
||||
}
|
||||
var remaining = [].concat(filters);
|
||||
var nextFilter = extensions => {
|
||||
if (remaining.length === 0) {
|
||||
// Map to instances and proceed
|
||||
onload(extensions.map(m => m.instance));
|
||||
} else {
|
||||
var filter = remaining[0];
|
||||
remaining = remaining.slice(1);
|
||||
filter(extensions, nextFilter);
|
||||
}
|
||||
// if we have this specific type handled, don't
|
||||
// proceed to parent types
|
||||
if (matchingExtensions.length > 0) {
|
||||
break eachType;
|
||||
}
|
||||
}
|
||||
extensions = matchingExtensions;
|
||||
};
|
||||
nextFilter(extensions);
|
||||
} else {
|
||||
// exclude typed extensions when types not requested
|
||||
extensions = extensions.filter(m => !('type' in m));
|
||||
// Map to instances and proceed
|
||||
onload(extensions.map(m => m.instance));
|
||||
}
|
||||
onload(extensions.map(m => m.instance));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the extension data
|
||||
*/
|
||||
_loadExtensionData(oncomplete) {
|
||||
if (!this.extensionDataProvider) {
|
||||
throw new Error("Must call ExtensionStore.init({ extensionDataProvider: (cb) => ..., typeInfoProvider: (type, cb) => ... }) first");
|
||||
}
|
||||
if (this.extensionPointList) {
|
||||
onconplete(this.extensionPointList);
|
||||
return;
|
||||
|
|
Loading…
Reference in New Issue