From 6d7d3e226881c500f133f5e1b29e087e3aa00471 Mon Sep 17 00:00:00 2001 From: Karel Hala Date: Thu, 31 Aug 2017 10:48:00 +0200 Subject: [PATCH 1/5] Add extensible components API to create components which can expose API and render endpoints --- app/javascript/custom-typings.ts | 2 + app/javascript/extensible-components/index.ts | 50 +++++++++++++++++++ app/javascript/extensible-components/lib.ts | 28 +++++++++++ .../packs/extensible-componenets-common.js | 1 + 4 files changed, 81 insertions(+) create mode 100644 app/javascript/custom-typings.ts create mode 100644 app/javascript/extensible-components/index.ts create mode 100644 app/javascript/extensible-components/lib.ts create mode 100644 app/javascript/packs/extensible-componenets-common.js diff --git a/app/javascript/custom-typings.ts b/app/javascript/custom-typings.ts new file mode 100644 index 00000000000..1ba5fa88554 --- /dev/null +++ b/app/javascript/custom-typings.ts @@ -0,0 +1,2 @@ +declare var ManageIQ: any; +declare var Rx: any; \ No newline at end of file diff --git a/app/javascript/extensible-components/index.ts b/app/javascript/extensible-components/index.ts new file mode 100644 index 00000000000..37afb5fdf29 --- /dev/null +++ b/app/javascript/extensible-components/index.ts @@ -0,0 +1,50 @@ +import { IExtensionComponent, IMiQApiCallback } from './lib'; + +/** + * Class for easy creation of extensible component. + */ +export class ExtensibleComponent { + public unsubscribe: Function; + constructor(public name: string, public api: IMiQApiCallback, public render: IMiQApiCallback){} +} + +/** + * Create new object which will hold extension components on MiQ main object. + */ +ManageIQ.extensionComponents = ManageIQ.extensionComponents || {}; + +/** + * Subject from from Rxjs to send message that we want to register new component. + */ +ManageIQ.extensionComponents.source = new Rx.Subject(); + +/** + * Components will be saved in items which will be set. To easy remove of components which no longer exists. + */ +ManageIQ.extensionComponents.items = new Set(); + +/** + * Helper function to create new component. + * @param name string name of new component. + * @param api callback functions to change inner logic of component. + * @param render callback function to apply render functions. + */ +ManageIQ.extensionComponents.newComponent = function(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { + let newCmp = new ExtensibleComponent(name, api, render); + ManageIQ.extensionComponents.source.onNext({action: 'add', payload: newCmp}); + return newCmp; +} + +/** + * Subscribe to extensionComponents source to add new components to items object. + */ +ManageIQ.extensionComponents.source.subscribe((event: IExtensionComponent) => { + if (event.action === 'add' && event.hasOwnProperty('payload')) { + event.payload.unsubscribe = () => { + ManageIQ.extensionComponents.items.delete(event.payload); + } + ManageIQ.extensionComponents.items.add(event.payload); + } else { + throw new Error('Unsupported action with extension components.'); + } +}); \ No newline at end of file diff --git a/app/javascript/extensible-components/lib.ts b/app/javascript/extensible-components/lib.ts new file mode 100644 index 00000000000..d98cd336d67 --- /dev/null +++ b/app/javascript/extensible-components/lib.ts @@ -0,0 +1,28 @@ +export interface IExtensionComponent { + action: string; + payload: any; +} + +export interface IMiQApiCallback { + [propName: string]: Function; +} + +export interface IExtensibleComponent { + extensibleComponent: any; + apiCallbacks: () => IMiQApiCallback; + renderCallbacks: () => IMiQApiCallback; +} + +export const extensionSource = ManageIQ.extensionComponents.source; + +export const extensionItems = ManageIQ.extensionComponents.items; + +/** + * Helper function to create new component. + * @param name string name of new component. + * @param api callback functions to change inner logic of component. + * @param render callback function to apply render functions. + */ +export function newComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { + return ManageIQ.extensionComponents.newComponent(name, api, render); +} \ No newline at end of file diff --git a/app/javascript/packs/extensible-componenets-common.js b/app/javascript/packs/extensible-componenets-common.js new file mode 100644 index 00000000000..52db0e70974 --- /dev/null +++ b/app/javascript/packs/extensible-componenets-common.js @@ -0,0 +1 @@ +require('../extensible-components'); \ No newline at end of file From 3ddc11158f3dc23419b6827cd8aa609b5d0c83cb Mon Sep 17 00:00:00 2001 From: Karel Hala Date: Thu, 31 Aug 2017 15:18:33 +0200 Subject: [PATCH 2/5] Add registrator function for extensible components --- app/javascript/custom-typings.ts | 2 +- app/javascript/extensible-components/index.ts | 19 ++++++++++++++++++- app/javascript/extensible-components/lib.ts | 2 +- .../packs/extensible-componenets-common.js | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/javascript/custom-typings.ts b/app/javascript/custom-typings.ts index 1ba5fa88554..f0b394b20aa 100644 --- a/app/javascript/custom-typings.ts +++ b/app/javascript/custom-typings.ts @@ -1,2 +1,2 @@ declare var ManageIQ: any; -declare var Rx: any; \ No newline at end of file +declare var Rx: any; diff --git a/app/javascript/extensible-components/index.ts b/app/javascript/extensible-components/index.ts index 37afb5fdf29..697de49fb42 100644 --- a/app/javascript/extensible-components/index.ts +++ b/app/javascript/extensible-components/index.ts @@ -47,4 +47,21 @@ ManageIQ.extensionComponents.source.subscribe((event: IExtensionComponent) => { } else { throw new Error('Unsupported action with extension components.'); } -}); \ No newline at end of file +}); + +ManageIQ.extensionComponents.subscribe = function(cmpName: string) { + let unsubscribe; + return { + with: (callback) => { + unsubscribe = ManageIQ.extensionComponents.source + .map(sourceAction => sourceAction.action === 'add' ? sourceAction.payload : {}) + .filter(component => component && component.name === cmpName) + .subscribe(cmp => cmp && callback(cmp)); + + ManageIQ.extensionComponents.items.forEach((component) => { + component.name === cmpName && callback(component); + }); + }, + delete: () => unsubscribe && unsubscribe() + } +} diff --git a/app/javascript/extensible-components/lib.ts b/app/javascript/extensible-components/lib.ts index d98cd336d67..356d95fb90b 100644 --- a/app/javascript/extensible-components/lib.ts +++ b/app/javascript/extensible-components/lib.ts @@ -25,4 +25,4 @@ export const extensionItems = ManageIQ.extensionComponents.items; */ export function newComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { return ManageIQ.extensionComponents.newComponent(name, api, render); -} \ No newline at end of file +} diff --git a/app/javascript/packs/extensible-componenets-common.js b/app/javascript/packs/extensible-componenets-common.js index 52db0e70974..6cabd5ac4c4 100644 --- a/app/javascript/packs/extensible-componenets-common.js +++ b/app/javascript/packs/extensible-componenets-common.js @@ -1 +1 @@ -require('../extensible-components'); \ No newline at end of file +require('../extensible-components'); From 400e0a79f5c7987f6ec5b298d6a655b1717e561e Mon Sep 17 00:00:00 2001 From: Karel Hala Date: Fri, 1 Sep 2017 17:01:19 +0200 Subject: [PATCH 3/5] Use better styling and rename some variables in extensible component --- app/javascript/extensible-components/index.ts | 66 +++++++++---------- app/javascript/extensible-components/lib.ts | 12 ++-- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/app/javascript/extensible-components/index.ts b/app/javascript/extensible-components/index.ts index 697de49fb42..d3ac3a3f63c 100644 --- a/app/javascript/extensible-components/index.ts +++ b/app/javascript/extensible-components/index.ts @@ -1,67 +1,67 @@ import { IExtensionComponent, IMiQApiCallback } from './lib'; +const source = new Rx.Subject(); +const items = new Set(); + /** * Class for easy creation of extensible component. */ export class ExtensibleComponent { - public unsubscribe: Function; + public delete: () => void; constructor(public name: string, public api: IMiQApiCallback, public render: IMiQApiCallback){} } -/** - * Create new object which will hold extension components on MiQ main object. - */ -ManageIQ.extensionComponents = ManageIQ.extensionComponents || {}; - -/** - * Subject from from Rxjs to send message that we want to register new component. - */ -ManageIQ.extensionComponents.source = new Rx.Subject(); - -/** - * Components will be saved in items which will be set. To easy remove of components which no longer exists. - */ -ManageIQ.extensionComponents.items = new Set(); - /** * Helper function to create new component. * @param name string name of new component. * @param api callback functions to change inner logic of component. * @param render callback function to apply render functions. */ -ManageIQ.extensionComponents.newComponent = function(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { +function addComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { let newCmp = new ExtensibleComponent(name, api, render); - ManageIQ.extensionComponents.source.onNext({action: 'add', payload: newCmp}); + source.onNext({action: 'add', payload: newCmp}); return newCmp; } /** - * Subscribe to extensionComponents source to add new components to items object. + * Helper function to subscribe to extension items based on component's name. + * @param cmpName name of component we want to subscribe to. + * @return object which has with and unsubscribe property. With is for callback to use found component and delete to + * unsubscribe from rxjs subject. */ -ManageIQ.extensionComponents.source.subscribe((event: IExtensionComponent) => { - if (event.action === 'add' && event.hasOwnProperty('payload')) { - event.payload.unsubscribe = () => { - ManageIQ.extensionComponents.items.delete(event.payload); - } - ManageIQ.extensionComponents.items.add(event.payload); - } else { - throw new Error('Unsupported action with extension components.'); - } -}); - -ManageIQ.extensionComponents.subscribe = function(cmpName: string) { +function subscribe(cmpName: string) { let unsubscribe; return { with: (callback) => { - unsubscribe = ManageIQ.extensionComponents.source + unsubscribe = source .map(sourceAction => sourceAction.action === 'add' ? sourceAction.payload : {}) .filter(component => component && component.name === cmpName) .subscribe(cmp => cmp && callback(cmp)); - ManageIQ.extensionComponents.items.forEach((component) => { + ManageIQ.extensions.items.forEach((component) => { component.name === cmpName && callback(component); }); }, delete: () => unsubscribe && unsubscribe() } } + +const extensions = { + addComponent, + subscribe, + get items() { return items; } +} + +ManageIQ.extensions = ManageIQ.extensions || extensions; + +/** + * Subscribe to extensions source to add new components to items object. + */ +source.subscribe((component: IExtensionComponent) => { + if (component.action === 'add' && component.hasOwnProperty('payload')) { + component.payload.delete = () => ManageIQ.extensions.items.delete(component.payload); + ManageIQ.extensions.items.add(component.payload); + } else { + console.error('Unsupported action with extension components.'); + } +}); diff --git a/app/javascript/extensible-components/lib.ts b/app/javascript/extensible-components/lib.ts index 356d95fb90b..99f0fb4f239 100644 --- a/app/javascript/extensible-components/lib.ts +++ b/app/javascript/extensible-components/lib.ts @@ -13,9 +13,13 @@ export interface IExtensibleComponent { renderCallbacks: () => IMiQApiCallback; } -export const extensionSource = ManageIQ.extensionComponents.source; +export function getItems() { + return ManageIQ.extensions.getItems(); +} -export const extensionItems = ManageIQ.extensionComponents.items; +export function subscribe(cmpName: string) { + return ManageIQ.extensions.subscribe(cmpName); +} /** * Helper function to create new component. @@ -23,6 +27,6 @@ export const extensionItems = ManageIQ.extensionComponents.items; * @param api callback functions to change inner logic of component. * @param render callback function to apply render functions. */ -export function newComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { - return ManageIQ.extensionComponents.newComponent(name, api, render); +export function addComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { + return ManageIQ.extensions.addComponent(name, api, render); } From 0bc16759bd1bf7ab10630cdc1ae8dda663cee462 Mon Sep 17 00:00:00 2001 From: Karel Hala Date: Wed, 6 Sep 2017 15:10:56 +0200 Subject: [PATCH 4/5] Add tests to extensible component --- app/javascript/extensible-components/index.ts | 2 +- app/javascript/extensible-components/lib.ts | 2 +- .../packs/extensible-componenets.spec.js | 75 +++++++++++++++++++ spec/javascripts/support/jasmine.yml | 2 +- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 spec/javascripts/packs/extensible-componenets.spec.js diff --git a/app/javascript/extensible-components/index.ts b/app/javascript/extensible-components/index.ts index d3ac3a3f63c..e10bc4a1360 100644 --- a/app/javascript/extensible-components/index.ts +++ b/app/javascript/extensible-components/index.ts @@ -42,7 +42,7 @@ function subscribe(cmpName: string) { component.name === cmpName && callback(component); }); }, - delete: () => unsubscribe && unsubscribe() + delete: () => unsubscribe && unsubscribe.dispose() } } diff --git a/app/javascript/extensible-components/lib.ts b/app/javascript/extensible-components/lib.ts index 99f0fb4f239..8bfd071f7f7 100644 --- a/app/javascript/extensible-components/lib.ts +++ b/app/javascript/extensible-components/lib.ts @@ -14,7 +14,7 @@ export interface IExtensibleComponent { } export function getItems() { - return ManageIQ.extensions.getItems(); + return ManageIQ.extensions.items; } export function subscribe(cmpName: string) { diff --git a/spec/javascripts/packs/extensible-componenets.spec.js b/spec/javascripts/packs/extensible-componenets.spec.js new file mode 100644 index 00000000000..3cb7124ec75 --- /dev/null +++ b/spec/javascripts/packs/extensible-componenets.spec.js @@ -0,0 +1,75 @@ +describe('Extensible components', function() { + var cmp; + var mockApi = { + onSomeAction: jasmine.createSpy('onSomeAction', function() {}), + onSomeActionWithParams: jasmine.createSpy('onSomeActionWithParams', function(param) {}), + }; + + var mockRender = { + addButtonHtml: jasmine.createSpy('addButtonHtml', function(htmlElem) {}) + }; + + it('should be defined with empty items', function() { + expect(ManageIQ.extensions).toBeDefined(); + expect(ManageIQ.extensions.items.size).toBe(0); + }); + + it('should create one item', function() { + cmp = ManageIQ.extensions.addComponent('testCmp', mockApi, mockRender); + expect(ManageIQ.extensions.items.size).toBe(1); + }); + + it('should remove newly created item', function() { + cmp.delete(); + expect(ManageIQ.extensions.items.size).toBe(0); + }); + + describe('default actions', function() { + var subscription; + var someParam = 'something'; + + beforeEach(function() { + cmp = ManageIQ.extensions.addComponent('testCmp', mockApi, mockRender); + }); + + describe('subscription', function() { + it('should subscribe based on name', function() { + subscription = ManageIQ.extensions.subscribe('testCmp'); + expect(subscription.delete).toBeDefined(); + expect(subscription.with).toBeDefined(); + }); + + it('should react to API', function() { + subscription.with(function(component) { + component.api.onSomeAction(); + component.api.onSomeActionWithParams(someParam); + }); + expect(mockApi.onSomeAction).toHaveBeenCalled(); + expect(mockApi.onSomeActionWithParams).toHaveBeenCalledWith(someParam); + }); + + it('should react to render', function() { + var someHTML = '
something
'; + subscription.with(function(component) { + component.render.addButtonHtml(someHTML); + }); + expect(mockRender.addButtonHtml).toHaveBeenCalledWith(someHTML); + }); + }); + + it('should not subscribe', function() { + subscription = ManageIQ.extensions.subscribe('somethingBad'); + subscription.with(function(component) { + //callback should not be called! + expect(false).toBe(true); + }); + expect(subscription.with).toBeDefined(); + expect(subscription.delete).toBeDefined(); + }); + + afterEach(function() { + cmp.delete(); + subscription && subscription.delete(); + }); + }); +}); diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 97242487bf7..b7bba30c038 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -18,6 +18,7 @@ src_files: - assets/angular-mocks - __spec__/helpers/fixtures-fix.js - packs/manageiq-ui-classic/application-common.js + - packs/manageiq-ui-classic/extensible-componenets-common.js # stylesheets # @@ -126,4 +127,3 @@ boot_files: # # rack_options: # server: 'thin' - From 1aaa1e34c08fb3e7d0cc732372ddb24b72d5a1c6 Mon Sep 17 00:00:00 2001 From: Karel Hala Date: Wed, 6 Sep 2017 17:23:24 +0200 Subject: [PATCH 5/5] Update render description to accept function callback with element in extensible component --- app/javascript/extensible-components/index.ts | 6 +++--- app/javascript/extensible-components/lib.ts | 8 +++++++- .../packs/extensible-componenets.spec.js | 14 +++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/javascript/extensible-components/index.ts b/app/javascript/extensible-components/index.ts index e10bc4a1360..412e9d96e97 100644 --- a/app/javascript/extensible-components/index.ts +++ b/app/javascript/extensible-components/index.ts @@ -1,4 +1,4 @@ -import { IExtensionComponent, IMiQApiCallback } from './lib'; +import { IExtensionComponent, IMiQApiCallback, IMiQRenderCallback } from './lib'; const source = new Rx.Subject(); const items = new Set(); @@ -8,7 +8,7 @@ const items = new Set(); */ export class ExtensibleComponent { public delete: () => void; - constructor(public name: string, public api: IMiQApiCallback, public render: IMiQApiCallback){} + constructor(public name: string, public api: IMiQApiCallback, public render: IMiQRenderCallback){} } /** @@ -17,7 +17,7 @@ export class ExtensibleComponent { * @param api callback functions to change inner logic of component. * @param render callback function to apply render functions. */ -function addComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) { +function addComponent(name: string, api?: IMiQApiCallback, render?: IMiQRenderCallback) { let newCmp = new ExtensibleComponent(name, api, render); source.onNext({action: 'add', payload: newCmp}); return newCmp; diff --git a/app/javascript/extensible-components/lib.ts b/app/javascript/extensible-components/lib.ts index 8bfd071f7f7..2393749911f 100644 --- a/app/javascript/extensible-components/lib.ts +++ b/app/javascript/extensible-components/lib.ts @@ -1,3 +1,5 @@ +export type RenderCallback = (element: HTMLElement) => void; + export interface IExtensionComponent { action: string; payload: any; @@ -7,10 +9,14 @@ export interface IMiQApiCallback { [propName: string]: Function; } +export interface IMiQRenderCallback { + [propName: string]: (renderCallback: RenderCallback) => void; +} + export interface IExtensibleComponent { extensibleComponent: any; apiCallbacks: () => IMiQApiCallback; - renderCallbacks: () => IMiQApiCallback; + renderCallbacks: () => IMiQRenderCallback; } export function getItems() { diff --git a/spec/javascripts/packs/extensible-componenets.spec.js b/spec/javascripts/packs/extensible-componenets.spec.js index 3cb7124ec75..6bfc2b2944f 100644 --- a/spec/javascripts/packs/extensible-componenets.spec.js +++ b/spec/javascripts/packs/extensible-componenets.spec.js @@ -1,12 +1,14 @@ describe('Extensible components', function() { + var htmlPartial = '
something
'; var cmp; var mockApi = { onSomeAction: jasmine.createSpy('onSomeAction', function() {}), - onSomeActionWithParams: jasmine.createSpy('onSomeActionWithParams', function(param) {}), + onSomeActionWithParams: jasmine.createSpy('onSomeActionWithParams', function(someParam){}), }; var mockRender = { - addButtonHtml: jasmine.createSpy('addButtonHtml', function(htmlElem) {}) + addButton: jasmine.createSpy('addButton', function(callback) {}), + addButton2: function(callback) { callback(htmlPartial); } }; it('should be defined with empty items', function() { @@ -49,11 +51,13 @@ describe('Extensible components', function() { }); it('should react to render', function() { - var someHTML = '
something
'; + var someCallback = jasmine.createSpy('someCallback', function(element) {}); subscription.with(function(component) { - component.render.addButtonHtml(someHTML); + component.render.addButton(someCallback); + component.render.addButton2(someCallback); }); - expect(mockRender.addButtonHtml).toHaveBeenCalledWith(someHTML); + expect(mockRender.addButton).toHaveBeenCalledWith(someCallback); + expect(someCallback).toHaveBeenCalledWith(htmlPartial); }); });