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

Fixes #20469 - Register react components from plugins #4733

Merged
merged 1 commit into from
Oct 4, 2017

Conversation

xprazak2
Copy link
Contributor

@xprazak2 xprazak2 commented Aug 9, 2017

Plugins can register their own react components so that MountingService is aware of them.

//foreman_myplugin/webpack/index.js

import componentRegistry from 'foremanReact/components/componentRegistry';
import MyComponent from './components/MyComponent';
import MyOtherComponent from './components/MyOtherComponent';

componentRegistry.register({ name: 'MyComponent', type: MyComponent });
// store and data attributes are true by default
componentRegistry.register({ name: 'MyOtherComponent', type: MyOtherComponent, store: false, data: false });

// or to register multiple components:
componentRegistry.registerMultiple([
  { name: 'MyComponent', type: MyComponent },
  { name: 'MyOtherComponent', type: MyOtherComponent, store: false, data: false }
]);

components from plugins can be imported using an alias, so plugins can use components from other plugins:

// somewhere in foreman_somename/webpack/
import OtherComponent from 'myplugin/OtherComponent';

If you are not sure about alias for plugin, run script/plugin_webpack_directories.rb

We do not load all js on each page, plugins must make sure their js files are present.
The arguments of helper are ids that plugins use when registering:

# my_view.erb
<%= webpacked_plugins_js_for :foreman_ansible, :foreman_openscap %>

TODO:

  • Stylesheets in components are ignored for unknown reason, so Statistics page looks strange
  • register multiple components with one call

@theforeman-bot
Copy link
Member

@xprazak2, the Redmine ticket used is for a different project than the one associated with this GitHub repository. Please either:

If changing the ticket number used, remember to update the PR title and the commit message (using git commit --amend).


This message was auto-generated by Foreman's prprocessor

@theforeman-bot
Copy link
Member

Issues: #20429

throw `Component not found: ${name} among ${registeredComponents()}`
}
let ComponentName = currentComponent.type;
return <ComponentName data={ currentComponent.data ? data : undefined } store={ currentComponent.store ? store : undefined } />;

Choose a reason for hiding this comment

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

Line 66 exceeds the maximum line length of 100 max-len

if (!currentComponent) {
throw `Component not found: ${name} among ${registeredComponents()}`
}
let ComponentName = currentComponent.type;

Choose a reason for hiding this comment

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

Expected blank line after variable declarations newline-after-var

let markup = (name, data, store) => {
let currentComponent = getComponent(name);
if (!currentComponent) {
throw `Component not found: ${name} among ${registeredComponents()}`

Choose a reason for hiding this comment

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

Expected an object to be thrown no-throw-literal
Missing semicolon semi

};

let markup = (name, data, store) => {
let currentComponent = getComponent(name);

Choose a reason for hiding this comment

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

Expected blank line after variable declarations newline-after-var

}

registry[name] = component;
console.log('Component registered with name ' + name);

Choose a reason for hiding this comment

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

Unexpected console statement no-console


let register = (name, component) => {
if (registry[name]) {
throw `Component name already taken: ${name}`;

Choose a reason for hiding this comment

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

Expected an object to be thrown no-throw-literal

import PowerStatus from '../components/hosts/powerStatus/';
import NotificationContainer from '../components/notifications/';
import ToastsList from '../components/toastNotifications/';
import StorageContainer from '../components/hosts/storage/vmware/';

Choose a reason for hiding this comment

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

'StorageContainer' is defined but never used no-unused-vars

@@ -0,0 +1,72 @@
import React from 'react';
import store from '../redux';

Choose a reason for hiding this comment

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

'store' is defined but never used no-unused-vars

@theforeman-bot
Copy link
Member

@xprazak2, the Redmine ticket used is for a different project than the one associated with this GitHub repository. Please either:

If changing the ticket number used, remember to update the PR title and the commit message (using git commit --amend).


This message was auto-generated by Foreman's prprocessor

@ohadlevy
Copy link
Member

ohadlevy commented Aug 9, 2017

Looking good, I hope to try it soon.
For the js code style I suggest running lint or using something like prettier (with eslint)

@xprazak2 xprazak2 force-pushed the register-components branch from a372615 to b3988c4 Compare August 14, 2017 13:37
@@ -0,0 +1,85 @@
import React from 'react';
import store from '../redux';

Choose a reason for hiding this comment

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

'store' is defined but never used no-unused-vars

@xprazak2 xprazak2 force-pushed the register-components branch from b3988c4 to 493a67c Compare August 14, 2017 13:50
foremanReactService:
path.join(__dirname,
'../webpack/assets/javascripts/services'),
foremanNodeModules:
Copy link
Contributor Author

@xprazak2 xprazak2 Aug 14, 2017

Choose a reason for hiding this comment

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

This is here because of packaging-related changes. Plugins need to find foreman's node_modules somehow.

import React from 'foremanNodeModules/react';

Copy link
Member

@ohadlevy ohadlevy Aug 14, 2017

Choose a reason for hiding this comment

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

does it mean that all plugins need to prefix their requirements to foreman node module? that would explain why in my tests i had a very large vendor file and a very large plugin specific file...I could see this being fairly annoying + hard to use npm packages that include common other libs (e.g. a react-date picker or something like that)

Copy link
Contributor Author

@xprazak2 xprazak2 Aug 18, 2017

Choose a reason for hiding this comment

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

I agree it is a nuisance, but import React from 'react'; in plugins does not work and I haven't found a better solution yet. I am not sure what you mean about packages that include common other libs, could you explain please?

Copy link
Member

Choose a reason for hiding this comment

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

for example, if I import a 3rd party react component (lets say a date picker) then it code will include

import React from 'react';

This will:
a. make us pull another copy of react into the plugin node modules
b. will include another copy of react in the plugin bundle.

Copy link
Member

Choose a reason for hiding this comment

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

/cc @dLobatog - this is probably a side effect we did not expect.

@xprazak2 xprazak2 changed the title Fixes #20429 - [wip] Register react components from plugins Fixes #20429 - Register react components from plugins Aug 14, 2017
Copy link
Member

@ohadlevy ohadlevy left a comment

Choose a reason for hiding this comment

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

looks like a great start, I've left a few comments inline.

Other things to consider for this pr:

  • tests
  • ability for a plugin to add customer actions/reducers.
  • reuse the same eslint configuration as core by default.

does a factory (such as described at https://medium.com/@SntsDev/the-factory-pattern-in-js-es6-78f0afad17e9) make any sense in this context?

thanks @xprazak2 !

@@ -17,6 +17,9 @@
<%= csrf_meta_tags %>
<%= javascript_include_tag *webpack_asset_paths('vendor', :extension => 'js'), "data-turbolinks-track" => true %>
<%= javascript_include_tag *webpack_asset_paths('bundle', :extension => 'js'), "data-turbolinks-track" => true %>
<% webpacked_plugins_js_tags.each do |item| %>
<%= javascript_include_tag *item, "data-turbolinks-track" => true %>
Copy link
Member

Choose a reason for hiding this comment

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

should this be optional? e.g. maybe not load all js on all pages?

foremanReactService:
path.join(__dirname,
'../webpack/assets/javascripts/services'),
foremanNodeModules:
Copy link
Member

@ohadlevy ohadlevy Aug 14, 2017

Choose a reason for hiding this comment

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

does it mean that all plugins need to prefix their requirements to foreman node module? that would explain why in my tests i had a very large vendor file and a very large plugin specific file...I could see this being fairly annoying + hard to use npm packages that include common other libs (e.g. a react-date picker or something like that)

let registry = {
PieChart: {
type: PieChart,
store: true,
Copy link
Member

Choose a reason for hiding this comment

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

can store and data default to true (so you dry)?

}
};

let register = (name, component) => {
Copy link
Member

Choose a reason for hiding this comment

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

why let vs const?

let currentComponent = getComponent(name);

if (!currentComponent) {
throw new Error(`Component not found: ${name} among ${registeredComponents()}`);
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: extra whitespace between found: and ${name}

const currentComponent = this.getComponent(name);

if (!currentComponent) {
throw new Error(`Component not found: ${name} among ${registeredComponents()}`);

Choose a reason for hiding this comment

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

'registeredComponents' is not defined no-undef

return splitDirs.length > 2 ? splitDirs[1] : pluginDirs;
};

var webpackDirs = execSync(path.join(__dirname, './plugin_webpack_directories.rb'), {

Choose a reason for hiding this comment

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

Unexpected var, use let or const instead no-var

// If we get multiple lines, then the plugin_webpack_directories.rb script
// has on the stdout more that just the JSON we want, so we use newline to split and check.
var sanitizeWebpackDirs = function (pluginDirs) {
var splitDirs = pluginDirs.toString().split("\n").reverse();

Choose a reason for hiding this comment

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

Unexpected var, use let or const instead no-var
Strings must use singlequote quotes


// If we get multiple lines, then the plugin_webpack_directories.rb script
// has on the stdout more that just the JSON we want, so we use newline to split and check.
var sanitizeWebpackDirs = function (pluginDirs) {

Choose a reason for hiding this comment

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

Unexpected var, use let or const instead no-var

'use strict';

var execSync = require('child_process').execSync;
var path = require('path');

Choose a reason for hiding this comment

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

Unexpected var, use let or const instead no-var

@@ -0,0 +1,18 @@
'use strict';

var execSync = require('child_process').execSync;

Choose a reason for hiding this comment

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

Unexpected var, use let or const instead no-var


bundle_names.map do |name|
javascript_include_tag *webpack_asset_paths(name, :extension => 'js'), "data-turbolinks-track" => true
end.reduce(&:concat).html_safe

Choose a reason for hiding this comment

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

Prefer inject over reduce.

bundle_names = requested_plugins.map { |plugin| Foreman::Plugin.bundle_name plugin }

bundle_names.map do |name|
javascript_include_tag *webpack_asset_paths(name, :extension => 'js'), "data-turbolinks-track" => true

Choose a reason for hiding this comment

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

Ambiguous splat operator. Parenthesize the method arguments if it's surely a splat operator, or add a whitespace to the right of the * if it should be a multiplication.

@xprazak2 xprazak2 force-pushed the register-components branch from 3cb8db2 to bc326dd Compare August 18, 2017 14:44
@xprazak2
Copy link
Contributor Author

Changes:

  • I turned the registry into a factory as described in the blog post, data and store attributes are true by default, which made code a bit DRYer.
import MyComponent from './MyComponent';
componentRegistry.register({ name: 'MyComponent', type: MyComponent });

registerMultiple accepts an array of objects with the same format.

  • npm run lint lints all the webpacked plugins now as well.
  • Including everything into all pages seems a bit too much. It will be therefore responsibility of a plugin to request the js files to be included:
# my_view.erb
<%= webpacked_plugins_js_for :foreman_ansible, :foreman_openscap %>

The arguments of helper are ids that plugins use when registering.

  • still missing tests, adding actions and better path resolution

@ohadlevy
Copy link
Member

@xprazak2 interesting to follow ManageIQ/manageiq-ui-classic#1944 as well, as they implement plugability in the redux concept, i think this two (this pr and manageiq) will complete each other.

@timogoebel
Copy link
Member

We gave this a shot. Works nicely.
@ohadlevy: Anything missing?

@@ -77,7 +77,7 @@
"webpack-stats-plugin": "^0.1.5"
},
"scripts": {
"lint": "./node_modules/.bin/eslint -c .eslintrc webpack/ || exit 0",
"lint": "./node_modules/.bin/eslint -c .eslintrc webpack/ $(./script/foreman_plugins_eslint.js) || exit 0",
Copy link
Member

@ohadlevy ohadlevy Aug 31, 2017

Choose a reason for hiding this comment

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

nice :) we should do the same for rubocop

@xprazak2 xprazak2 force-pushed the register-components branch 2 times, most recently from c83509c to 9e25134 Compare October 3, 2017 07:07
@xprazak2
Copy link
Contributor Author

xprazak2 commented Oct 3, 2017

I fixed the code climate error, is there anything else I should fix? Do you want me to incorporate the change that prevents node modules to appear twice?

@xprazak2
Copy link
Contributor Author

xprazak2 commented Oct 3, 2017

[test katello]

},

registerMultiple(componentObjs) {
return _.forEach(componentObjs, (obj) => {
Copy link
Member

Choose a reason for hiding this comment

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

maybe just import foreach instead of the entire lodash, e.g.:

import foreach from 'lodash/foreach'

return foreach(componentObjs, (obj) => {)

@xprazak2 xprazak2 force-pushed the register-components branch from 9e25134 to 24b1175 Compare October 3, 2017 10:28
@xprazak2
Copy link
Contributor Author

xprazak2 commented Oct 3, 2017

I now import just the functions instead of the whole lodash

@@ -4,4 +4,31 @@ def mount_react_component(name, selector, data = [])
"$(tfm.reactMounter.mount('#{name}', '#{selector}', #{data}));".html_safe
end
end

def webpacked_plugins_js_for(*plugin_names)
Copy link
Member

Choose a reason for hiding this comment

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

@xprazak2 would you mind adding tests for these helpers?

@xprazak2 xprazak2 force-pushed the register-components branch from 24b1175 to 911ba90 Compare October 3, 2017 13:11
@xprazak2
Copy link
Contributor Author

xprazak2 commented Oct 3, 2017

I added a couple of tests to cover new helpers.

Foreman::Plugin.register(:foreman_angular) {}

def webpack_asset_paths(bundle_name, opts)
["<script src=\"https://foreman.example.com:3808/webpack/#{bundle_name}.js\"></script>"]
Copy link
Member

Choose a reason for hiding this comment

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

shouldnt this be on the same url as the server in prod/test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does it matter? It is just a stub for test, the method comes from webpack-rails and I cannot use it directly in test because it complains about missing manifest.

@ohadlevy
Copy link
Member

ohadlevy commented Oct 3, 2017 via email

@xprazak2 xprazak2 force-pushed the register-components branch from 911ba90 to 6b49bc8 Compare October 3, 2017 14:26
@mmoll
Copy link
Contributor

mmoll commented Oct 3, 2017

[test katello]

Copy link

@waldenraines waldenraines left a comment

Choose a reason for hiding this comment

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

What is here worked well for us after @ohadlevy's change in #4733 (comment) but I don't see that included in the webpack config yet. Will approve after that is added.

@@ -52,11 +50,16 @@ var config = {
path.join(__dirname, '..', 'webpack'),

Choose a reason for hiding this comment

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

Do you mind making the change @ohadlevy mentions in #4733 (comment)?

Copy link
Member

Choose a reason for hiding this comment

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

in that case 👍 from my side to add those changes. /cc @dLobatog as it seems we would need to find a different approach to package this.

Copy link
Member

Choose a reason for hiding this comment

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

It did indeed broke packaging which means our nightlies are now broken. Given the current load on the packaging and release team it might be wise to slow down these changes, at least until 1.16 is out.

@xprazak2
Copy link
Contributor Author

xprazak2 commented Oct 4, 2017

I replaced 'node_modules/' with path.join(__dirname, '..', 'node_modules'). I also removed the alias for node_modules as it is no longer needed, now we can use

import React from 'react'

in plugins as well.

@ohadlevy ohadlevy merged commit 8ec9fb0 into theforeman:develop Oct 4, 2017
@ohadlevy
Copy link
Member

ohadlevy commented Oct 4, 2017

Thanks @xprazak2!

@dLobatog
Copy link
Member

dLobatog commented Oct 6, 2017

This PR removed the fix in http://projects.theforeman.org/issues/20511, which in turn makes Foreman nightlies unable to build again http://koji.katello.org/kojifiles/work/tasks/5312/35312/build.log 'ModuleNotFoundError: Module not found: Error: Can't resolve 'object-assign' in '/usr/lib/node_modules/react/lib''. I'll fix that on another PR.

@ohadlevy
Copy link
Member

ohadlevy commented Oct 6, 2017

@dLobatog as mentioned earlier in this thread, the packaging solution conflict with our plugin assets requirement (as it duplicate the libraries in the bundle across plugins).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.