diff --git a/app/helpers/reactjs_helper.rb b/app/helpers/reactjs_helper.rb index 96269aa6bb3..f18ccc64812 100644 --- a/app/helpers/reactjs_helper.rb +++ b/app/helpers/reactjs_helper.rb @@ -1,6 +1,6 @@ module ReactjsHelper def react(name, data = {}, attributes = {}, element = 'div') - uid = unique_html_id('react') + uid = attributes[:id] || unique_html_id('react') capture do concat(content_tag(element, nil, attributes.merge(:id => uid))) concat(javascript_tag("ManageIQ.component.componentFactory('#{j(name)}', '##{j(uid)}', #{data.to_json})")) diff --git a/app/javascript/components/breadcrumbs/index.jsx b/app/javascript/components/breadcrumbs/index.jsx index 89804c1c17b..150a1aa762e 100644 --- a/app/javascript/components/breadcrumbs/index.jsx +++ b/app/javascript/components/breadcrumbs/index.jsx @@ -1,81 +1,69 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { Breadcrumb } from 'patternfly-react'; import { unescape } from 'lodash'; import { onClickTree, onClick, onClickToExplorer } from './on-click-functions'; +// FIXME: don't parse html here const parsedText = text => unescape(text).replace(/<[/]{0,1}strong>/g, ''); -class Breadcrumbs extends Component { - renderItems = () => { - const { items, controllerName } = this.props; - return items.filter((_item, index) => index !== (items.length - 1)).map((item, index) => { +const renderItems = ({ items, controllerName }) => { + return items + .filter((_item, index) => index !== (items.length - 1)) + .map((item, index) => { const text = parsedText(item.title); - if ((item.url || item.key || item.to_explorer) && !item.action) { - if (item.key || item.to_explorer) { - return ( - - (item.to_explorer - ? onClickToExplorer(e, controllerName, item.to_explorer) - : onClickTree(e, controllerName, item)) - } - > - {text} - - ); - } + if (item.action || (!item.url && !item.key && !item.to_explorer)) { + return
  • {text}
  • ; // eslint-disable-line react/no-array-index-key + } + + if (item.key || item.to_explorer) { return ( onClick(e, item.url)} + key={`${item.key}-${index}`} // eslint-disable-line react/no-array-index-key + onClick={e => + (item.to_explorer + ? onClickToExplorer(e, controllerName, item.to_explorer) + : onClickTree(e, controllerName, item)) + } > {text} ); } - return
  • {text}
  • ; // eslint-disable-line react/no-array-index-key - }); - }; - render() { - const { - items, title, controllerName, ...rest // eslint-disable-line no-unused-vars - } = this.props; - - return ( - - {items && this.renderItems()} - - - {items && items.length > 0 ? parsedText(items[items.length - 1].title) : parsedText(title)} - + return ( + onClick(e, item.url)} + > + {text} - - ); - } -} + ); + }); +}; + +const Breadcrumbs = ({ items, title, controllerName }) => ( + + {items && renderItems({ items, controllerName })} + + + {items && items.length > 0 ? parsedText(items[items.length - 1].title) : parsedText(title)} + + + +); Breadcrumbs.propTypes = { - items: PropTypes.arrayOf(PropTypes.shape( - { - title: PropTypes.string.isRequired, - url: PropTypes.string, - action: PropTypes.string, - key: PropTypes.string, - }, - )), - title: PropTypes.string, controllerName: PropTypes.string, -}; - -Breadcrumbs.defaultProps = { - items: undefined, - title: undefined, - controllerName: undefined, + items: PropTypes.arrayOf(PropTypes.shape({ + action: PropTypes.string, + key: PropTypes.string, + title: PropTypes.string.isRequired, + url: PropTypes.string, + })), + title: PropTypes.string, }; export default Breadcrumbs; diff --git a/app/javascript/miq-component/helpers.js b/app/javascript/miq-component/helpers.js index df1db0697b1..98c8f30b0a2 100644 --- a/app/javascript/miq-component/helpers.js +++ b/app/javascript/miq-component/helpers.js @@ -1,8 +1,8 @@ import * as registry from './registry.js'; import reactBlueprint from './react-blueprint.jsx'; -export function addReact(name, component) { - return registry.define(name, reactBlueprint(component)); +export function addReact(name, component, options = {}) { + return registry.define(name, reactBlueprint(component), options); } export function componentFactory(blueprintName, selector, props) { diff --git a/app/javascript/miq-component/registry.js b/app/javascript/miq-component/registry.js index 5b89db14ece..e9e35b5c2a3 100644 --- a/app/javascript/miq-component/registry.js +++ b/app/javascript/miq-component/registry.js @@ -1,29 +1,27 @@ import { writeProxy, lockInstanceProperties } from './utils'; import { cleanVirtualDom } from './helpers'; -const registry = new Map(); // Map> +const registry = {}; // Map /** * Get definition of a component with the given `name`. */ export function getDefinition(name) { - return Array.from(registry.keys()).find(definition => definition.name === name); + return registry[name]; } /** * Make sure the instance `id` is sane and cannot be changed. */ export function sanitizeAndFreezeInstanceId(instance, definition) { - const id = typeof instance.id === 'string' - ? instance.id - : `${definition.name}-${registry.get(definition).size}`; + const id = instance.id || `${definition.name}-${definition.instances.size}`; Object.defineProperty(instance, 'id', { get() { return id; }, set() { - throw new Error(`Attempt to modify id of instance ${instance.id}`); + throw new Error(`Attempt to modify id of instance ${id}`); }, enumerable: true, }); @@ -35,7 +33,7 @@ export function sanitizeAndFreezeInstanceId(instance, definition) { * - the given instance `id` isn't already taken */ export function validateInstance(instance, definition) { - if (Array.from(registry.get(definition)).find(existingInstance => existingInstance === instance)) { + if (Array.from(definition.instances).find(existingInstance => existingInstance === instance)) { throw new Error('Instance already present, check your blueprint.create implementation'); } if (getInstance(definition.name, instance.id)) { @@ -46,23 +44,28 @@ export function validateInstance(instance, definition) { /** * Implementation of the `ComponentApi.define` method. */ -export function define(name, blueprint = {}, instances = null) { +export function define(name, blueprint = {}, options = {}) { // validate inputs - if (typeof name !== 'string' || isDefined(name)) { - return; + if (typeof name !== 'string') { + throw new Error(`Registry.define: non-string name: ${name}`); + } + if (isDefined(name) && !options.override) { + throw new Error(`Registry.define: component already exists: ${name} (use { override: true } ?)`); } // add new definition to the registry - const newDefinition = { name, blueprint }; - registry.set(newDefinition, new Set()); + const instances = new Set(); + const newDefinition = { name, blueprint, instances }; + registry[name] = newDefinition; // add existing instances to the registry - if (Array.isArray(instances)) { - instances.filter((instance) => !!instance) + if (Array.isArray(options.instances)) { + options.instances.filter((instance) => !!instance) .forEach((instance) => { sanitizeAndFreezeInstanceId(instance, newDefinition); validateInstance(instance, newDefinition); - registry.get(newDefinition).add(instance); + + newDefinition.instances.add(instance); }); } } @@ -147,7 +150,7 @@ export function newInstance(name, initialProps = {}, mountTo = undefined) { } // remove instance from the registry - registry.get(definition).delete(newInstance); + definition.instances.delete(newInstance); // prevent access to existing instance properties except for id lockInstanceProperties(newInstance); @@ -157,7 +160,7 @@ export function newInstance(name, initialProps = {}, mountTo = undefined) { }; // add instance to the registry - registry.get(definition).add(newInstance); + definition.instances.add(newInstance); return newInstance; } @@ -167,7 +170,7 @@ export function newInstance(name, initialProps = {}, mountTo = undefined) { */ export function getInstance(name, id) { const definition = getDefinition(name); - return definition && Array.from(registry.get(definition)).find(instance => instance.id === id); + return definition && Array.from(definition.instances).find(instance => instance.id === id); } /** @@ -181,7 +184,7 @@ export function isDefined(name) { * Test helper: get names of all components. */ export function getComponentNames() { - return Array.from(registry.keys()).map(definition => definition.name); + return Object.keys(registry); } /** @@ -189,12 +192,12 @@ export function getComponentNames() { */ export function getComponentInstances(name) { const definition = getDefinition(name); - return definition ? Array.from(registry.get(definition).values()) : []; + return definition ? Array.from(definition.instances) : []; } /** * Test helper: remove all component data. */ export function clearRegistry() { - registry.clear(); + Object.keys(registry).forEach((k) => (delete registry[k])); } diff --git a/app/javascript/spec/miq-component/helpers.spec.js b/app/javascript/spec/miq-component/helpers.spec.js index 4036ec7ba5b..76b4a011a3b 100644 --- a/app/javascript/spec/miq-component/helpers.spec.js +++ b/app/javascript/spec/miq-component/helpers.spec.js @@ -24,7 +24,7 @@ describe('Helpers', () => { }, ]; - define('FooComponent', {}, testInstances); + define('FooComponent', {}, { instances: testInstances }); cleanVirtualDom(); expect(destroy1).not.toHaveBeenCalled(); diff --git a/app/javascript/spec/miq-component/miq-component.spec.js b/app/javascript/spec/miq-component/miq-component.spec.js index b7dd60b407d..065d94bd2d9 100644 --- a/app/javascript/spec/miq-component/miq-component.spec.js +++ b/app/javascript/spec/miq-component/miq-component.spec.js @@ -43,14 +43,24 @@ describe('Component API', () => { expect(getComponentNames()).toEqual(['FooComponent', 'BarComponent']); }); - it('define method does nothing if the component name is already taken', () => { + it('define method throws if the component name is already taken', () => { define('FooComponent', {}); + expect(() => { + define('FooComponent', {}); + }).toThrow(); + expect(getComponentNames()).toEqual(['FooComponent']); + }); + + it('define method passes twice with override option', () => { define('FooComponent', {}); + define('FooComponent', {}, { override: true }); expect(getComponentNames()).toEqual(['FooComponent']); }); it('define method does nothing if the component name is not a string', () => { - define(123, {}); + expect(() => { + define(123, {}); + }).toThrow(); expect(isDefined(123)).toBe(false); expect(getComponentNames()).toEqual([]); }); @@ -60,17 +70,17 @@ describe('Component API', () => { { id: 'first' }, { id: 'second' }, ]; - define('FooComponent', {}, testInstances); + define('FooComponent', {}, { instances: testInstances }); expect(getInstance('FooComponent', 'first')).toBe(testInstances[0]); expect(getInstance('FooComponent', 'second')).toBe(testInstances[1]); }); it('when passing existing instances, define method ensures a sane instance id', () => { const testInstances = [ - { id: 'first' }, { id: 123 }, {}, + { id: 'first' }, {}, {}, ]; - define('FooComponent', {}, testInstances); + define('FooComponent', {}, { instances: testInstances }); const registeredInstances = getComponentInstances('FooComponent'); expect(registeredInstances).toHaveLength(3); @@ -84,7 +94,7 @@ describe('Component API', () => { { id: 'first' }, ]; - define('FooComponent', {}, testInstances); + define('FooComponent', {}, { instances: testInstances }); expect(() => { testInstances[0].id = 'second'; }).toThrow(); @@ -95,7 +105,7 @@ describe('Component API', () => { false, '', null, undefined, {}, ]; - define('FooComponent', {}, testInstances); + define('FooComponent', {}, { instances: testInstances }); const registeredInstances = getComponentInstances('FooComponent'); expect(registeredInstances).toHaveLength(1); @@ -106,7 +116,7 @@ describe('Component API', () => { const testInstance = { id: 'first' }; expect(() => { - define('FooComponent', {}, [testInstance, testInstance]); + define('FooComponent', {}, { instances: [testInstance, testInstance] }); }).toThrow(); }); @@ -116,7 +126,7 @@ describe('Component API', () => { ]; expect(() => { - define('FooComponent', {}, testInstances); + define('FooComponent', {}, { instances: testInstances }); }).toThrow(); }); @@ -193,7 +203,7 @@ describe('Component API', () => { define('FooComponent', { create: jest.fn().mockName('testBlueprint.create') .mockImplementationOnce(() => ({ id: 'first', elementId: mountId })) - .mockImplementationOnce(() => ({ id: 123, elementId: mountId })) + .mockImplementationOnce(() => ({ elementId: mountId })) .mockImplementationOnce(() => ({ elementId: mountId })), }); @@ -424,7 +434,7 @@ describe('Component API', () => { it('sanitizeAndFreezeInstanceId ensures the instance id is sane and cannot be changed', () => { const testInstances = [ - { id: 'first' }, { id: 123 }, {}, + { id: 'first' }, {}, {}, ]; define('FooComponent', {}); diff --git a/config/webpack/shared.js b/config/webpack/shared.js index 2273d9025ca..565c7fee818 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -62,6 +62,16 @@ let plugins = [ publicPath: output.publicPath, writeToFileEmit: true, }), + + // plugin to output timestamp after compilation (useful for --watch) + { + apply(compiler) { + compiler.hooks.done.tap('done timestamp', () => { + // setTimeout to append instead of prepend the date + setTimeout(() => console.log('webpack: done', new Date())); + }); + }, + }, ]; if (env.WEBPACK_VERBOSE) {