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 #29298 - Add new Content View table #8644

Merged
merged 1 commit into from
May 6, 2020

Conversation

johnpmitsch
Copy link
Contributor

This adds a new content view table using patternfly 4 to the page "/labs/content_views"

@theforeman-bot
Copy link

Issues: #29298

package.json Outdated Show resolved Hide resolved
webpack/.eslintrc Outdated Show resolved Hide resolved
webpack/.eslintrc Outdated Show resolved Hide resolved
compoundParent: 5,
cells: [
{
title: <VersionsExpansion versions={versions} cvId={id} />,
Copy link
Contributor Author

@johnpmitsch johnpmitsch Apr 2, 2020

Choose a reason for hiding this comment

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

All of these expansion panes are being passed the appropriate info, but the components aren't actually using it yet. That will come in future PRs 👍

@johnpmitsch
Copy link
Contributor Author

johnpmitsch commented Apr 2, 2020

This page uses dummy data, the redux portion is coming in another PR or will be added here later. Feel free to alter the dummy data in webpack/scenes/ContentViews/data/index.json by overwriting it with an index call. There is no search and pagination yet as well.

Since this is under "labs", I kept the dummy data checked into git and many pieces are not hooked up yet, such as the actions and the expandables are empty.

"Details" will be a link to a details modal with more information than what is shown in the expandable panes. There is no link in this PR now.

The expandables will load the details data for the CV, this will be through redux making the API call once that part is ready, for now it is dummy data. It will also only make the details call once, so clicking on the other expandables in that row or closing and re-opening the expandable won't keep calling the API.

CV page list
CV_new_page_index

CV page with some expandables open
cvv_new_page_with_expandables

CV page with no CVs present
cvv_new_page_no_cvs

@johnpmitsch
Copy link
Contributor Author

A note on the API call:
Currently, the Content View index call gives a ton of information. So much that we probably wouldn't need to call the show/details endpoint for a CV for the expandable panes. But, this doesn't seem like proper use of the API and we have a chance to change this in the UI in hopes that we can "slim down" what the content view index endpoint returns.

@johnpmitsch
Copy link
Contributor Author

Lots of comments by me! 😃 Just want to give a brain dump before I forget my various trains of thought during development. Feel free to make use of the "hide comments" option on files 😉

@johnpmitsch
Copy link
Contributor Author

There is some question of what parts of the pf4 table components should be moved to shared components, but its a bit early to tell right now. I think a second page using a pf4 table would give a better idea on how to break it up. But for now, I'll try to keep things as modular as possible and keep in mind that parts may abstracted out. 👍

@johnpmitsch
Copy link
Contributor Author

To test this, check out the PR, change the following in Foreman, and npm i in ~/foreman

diff --git a/package.json b/package.json
index a3db80a..4795653 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
     "create-react-component": "yo react-domain"
   },
   "dependencies": {
-    "@theforeman/vendor": "^4.0.7",
+    "@theforeman/vendor": "^4.3.0",
     "intl": "~1.2.5",
     "jed": "^1.1.1",
     "react-intl": "^2.8.0"
@@ -30,11 +30,11 @@
   },
   "devDependencies": {
     "@babel/core": "^7.7.0",
-    "@theforeman/builder": "^4.0.7",
-    "@theforeman/eslint-plugin-foreman": "^4.0.7",
-    "@theforeman/stories": "^4.0.7",
-    "@theforeman/test": "^4.0.7",
-    "@theforeman/vendor-dev": "^4.0.7",
+    "@theforeman/builder": "^4.3.0",
+    "@theforeman/eslint-plugin-foreman": "^4.3.0",
+    "@theforeman/stories": "^4.3.0",
+    "@theforeman/test": "^4.3.0",
+    "@theforeman/vendor-dev": "^4.3.0",
     "argv-parse": "^1.0.1",
     "babel-eslint": "^10.0.0",
     "babel-loader": "^8.0.0",

Copy link
Contributor

@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.

There are a lot of missing translations here that should be translated with __(). I commented on some of them but didn't get them all.

Also I am seeing these compilation errors:

20:38:29 webpack.1 |     ERROR in ../katello/node_modules/@patternfly/react-styles/css/assets/images/pfbg_992@2x.jpg
20:38:29 webpack.1 |     Module parse failed: Unexpected character '�' (1:0)
20:38:29 webpack.1 |     You may need an appropriate loader to handle this file type.
20:38:29 webpack.1 |     (Source code omitted for this binary file)
20:38:29 webpack.1 |      @ ./node_modules/css-loader!../katello/node_modules/@patternfly/react-styles/css/components/AboutModalBox/about-modal-box.css 6:5304-5350

Seems like we don't have a webpack loader for images?

webpack/scenes/ContentViews/Table/tableDataGenerator.js Outdated Show resolved Hide resolved
webpack/scenes/ContentViews/Table/tableDataGenerator.js Outdated Show resolved Hide resolved
@johnpmitsch johnpmitsch force-pushed the new-cv-table branch 2 times, most recently from 7797a8e to 31be6d8 Compare April 6, 2020 18:21
Copy link
Member

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

Thanks for bringing PF4 into existence in Katello, @johnpmitsch ! Lots of small comments below, but overall I think my main issues are not with the code, but I'm just coming out of this less excited for PF4 than I was before. It appears the same PF3 paradigm applies in PF4 for tables, with lots of arrays and objects being passed in to Patternfly. This is totally in conflict with the composable nature of JSX and React in general, so it seems to me like we're going to end up with a React code base just as complex and difficult as it was with PF3. On the bright side, the visuals and styling do look much better and more modern.

I didn't run the new testing library, so no testing comments for now. But I have played around with react-testing-library before and I really like the philosophy.

One overall visual comment-- it seems like the scale is a bit off. The font sizes and everything look huge, and too bold. It makes the checkboxes and the nav bar menus look tiny. Are these sizes the default?

package.json Outdated Show resolved Hide resolved
webpack/.eslintrc Outdated Show resolved Hide resolved
webpack/.eslintrc Outdated Show resolved Hide resolved
webpack/containers/Application/overrides.scss Show resolved Hide resolved
webpack/scenes/ContentViews/ContentViewPage.js Outdated Show resolved Hide resolved
webpack/scenes/ContentViews/Table/tableDataGenerator.js Outdated Show resolved Hide resolved
webpack/scenes/ContentViews/Table/tableDataGenerator.js Outdated Show resolved Hide resolved
webpack/scenes/ContentViews/expansions/DetailsExpansion.js Outdated Show resolved Hide resolved
@johnpmitsch johnpmitsch force-pushed the new-cv-table branch 3 times, most recently from dce0650 to 5ce8ee1 Compare April 13, 2020 17:01
Copy link
Member

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

Some more comments below

const ContentViewTable = ({
loadContentViewDetails, detailsMap, results, loading,
}) => {
const [table, setTable] = useState({ rows: [], columns: [] });
Copy link
Member

@jeremylenz jeremylenz Apr 22, 2020

Choose a reason for hiding this comment

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

@johnpmitsch (Documenting/expanding our IRC discussion here)

I think that since expandedColumnMap only stores data about what rows are expanded, the right place for this is in internal component state.

For the table rows and columns, I can see the argument for moving to Redux. The task is to take the raw API response and format it in the way that PF4 requires. The question is where should this "data normalization" be done?

There are a few possible approaches:

  1. Use the react-redux hook useSelector to read the API response. Format the data in the component using tableDataGenerator like you're already doing. (May require some refactoring of tableDataGenerator, but otherwise not too much change
  2. Write a custom hook, useForemanTableData, that does the same thing as the above. This way it could be reused across several tables.
  3. Have the Redux reducer format the API response and just store the pre-formatted data in Redux. Then you could use useSelector in the component but not worry about data formatting there.
  4. Write a new Redux middleware that looks for the Content View SUCCESS action, formats the data, and dispatches a new action which stores the formatted data in Redux. This keeps business logic out of the reducer, which really should be only for transforming actions into new state.

Any of these will avoid using useState for the table data. I'd be perfectly happy with #1 for now, and either #2 or #4 eventually.

const ContentViewTable = ({
loadContentViewDetails, detailsMap, results, loading,
}) => {
const [table, setTable] = useState({ rows: [], columns: [] });
Copy link
Member

Choose a reason for hiding this comment

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

Which makes me think, @LaViro is there a way for plugins to add Redux middleware to Foreman? I think we'd probably have to build that.

webpack/containers/Application/overrides.scss Show resolved Hide resolved
Comment on lines 96 to 99
onClick: (_event, rowId, rowInfo) => console.log(`clicked on row ${rowId} with Content View ${cvIdFromRow(rowInfo)}`),
},
];
/* eslint-enable no-console */
Copy link
Member

Choose a reason for hiding this comment

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

I agree these should eventually be in Redux but it's not needed this early

return (
<Table
aria-label="Content View Table"
onSelect={cvsPresent ? onSelect : null}
Copy link
Member

Choose a reason for hiding this comment

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

Why pass null here? Isn't it always expecting a function anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before I refactored, the empty state view when you don't have any rows will display a checkbox if the function is passed in to this parameter. But now that its not displaying the table in that state, this can probably be removed. I'll try it 👍

const row = [
{ title: <ContentViewName composite={composite ? 1 : undefined} name={name} cvId={id} /> },
lastPublished || 'Not yet published',
{ title: __('Details'), props: { isOpen: false, ariaControls: `cv-details-expansion-${id}`, contentviewid: id } },
Copy link
Member

@jeremylenz jeremylenz Apr 22, 2020

Choose a reason for hiding this comment

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

After thinking a bit more over lunch, maybe it's better to handle the data this way:

Your formatted data will be not just the array of rows, but an array of objects like

{
isExpanded: false,
contentViewId: 1,
contentViewDetails: {..},
rowData: [...],
}

That way you have all the data where you need it, attached to the row data. And then you can map THAT to get your rows prop to pass down.

(This will also eliminate the confusing data shape in the expanded rows map.)

Copy link
Member

Choose a reason for hiding this comment

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

(This does maybe put the isExpanded state in Redux where it doesn't need to be. But maybe you could map that separately in the component state.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, let me play around with it. I'm leaning towards some sort of row mapping as well, but not sure how much should live in redux and how much should be in the component. I'll see what feels right!

@johnpmitsch
Copy link
Contributor Author

johnpmitsch commented Apr 30, 2020

@LaViro @jeremylenz I updated to use the API middleware, along with some other updates based on comments. I still have to update both the testing and the linting, and there is some general cleanup still to-do. I'm pushing up now if you want to give feedback on the general direction to make sure its what you expected. Please hold off on any detailed comments until I get everything cleaned up, but feel free to give comments on the general refactoring! 😉

To-do:

  • linting
  • general cleanup
  • update tests

@johnpmitsch johnpmitsch force-pushed the new-cv-table branch 2 times, most recently from 4df69dd to 49b31ef Compare April 30, 2020 16:10
Copy link
Member

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

Holding back detailed comments as requested, just wanted to say the API middleware stuff looks good to me so far. It really does make the code so much cleaner.

"@theforeman/builder": "^4.3.0",
"@theforeman/vendor-dev": "^4.3.0",
"@theforeman/builder": "^4.2.0",
"@theforeman/vendor-dev": "^4.2.0",
Copy link
Member

Choose a reason for hiding this comment

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

Curious why the downgrade?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I gotta change it back, bad git rebase! Really this should be 4.5.0 like theforeman/foreman#7519 - good catch

@johnpmitsch
Copy link
Contributor Author

Holding back detailed comments as requested, just wanted to say the API middleware stuff looks good to me so far. It really does make the code so much cleaner.

Thanks and agreed! Sorry, I should have commented back with an update, everything is fair game to review except the tests, which need to be updated, so have at it 😉

Copy link
Member

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

Thanks @johnpmitsch

Getting there! Some code comments below

Comment on lines 33 to 34
Object.entries(rowMapping).forEach(([cvId, { rowIndex }]) => {
if (rowIndex === rowIdx) id = cvId;
Copy link
Member

Choose a reason for hiding this comment

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

Feel like it's cleaner to use find:

const entry = Object.entries(rowMapping).find(item => item[1].rowIndex === rowIdx);
if (entry) return parseInt(entry[0]);

Comment on lines 72 to 73
const EmptyTitle = __("You currently don't have any Content Views.");
const EmptyBody = __('A Content View can be added by using the "New content view" button below.');
Copy link
Member

Choose a reason for hiding this comment

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

String values should be normal camel case, not title case


contentViews.forEach((contentView, rowIndex) => {
const { id } = contentView;
if (!Object.prototype.hasOwnProperty.call(updatedRowMapping, id)) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm very confused.. why do you need call here? And why use Object.prototype ?

Copy link
Contributor Author

@johnpmitsch johnpmitsch May 5, 2020

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Wow. I can see that it's a reasonable rule but it sure makes the code hard to read and understand. Thanks for explaining :)

Copy link
Member

Choose a reason for hiding this comment

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

Actually I would like a comment pointing to that eslint rule.

{title}
</Title>
<EmptyStateBody>
{body }
Copy link
Member

Choose a reason for hiding this comment

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

nit: trailing space

@@ -0,0 +1,3 @@
.ktable-cell-icon {
margin: 0 8px 0 0;
Copy link
Member

Choose a reason for hiding this comment

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

I can never remember the order for these CSS shortcuts (top bottom left right? top right bottom left?) When it's just a single value, would you be open to just using margin-left or whatever it is?

Comment on lines +18 to +22
useEffect(() => {
if (isOpen && Object.keys(details).length === 0) {
dispatch(getContentViewDetails(cvId));
}
});
Copy link
Member

Choose a reason for hiding this comment

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

Without a dependency array, wouldn't this repeatedly make API requests on every render until the first one comes back?

});

if (status === STATUS.PENDING) return (<Loading size="sm" />);
// Can we display the error message?
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this comment is necessary; the next line is pretty self-explanatory 😋

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that was actually for me to not forget, with this we don't show the actual API response, I can remove it for now though and file an issue

Copy link
Member

Choose a reason for hiding this comment

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

Oh that's fine then, if it's for you to remember something then just stick a TODO in front of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

just stick a TODO in front of it.

I'm not a fan of these, mostly because they just stick around and nothing happens with them and we have a proper issue tracking system, so I'll just file an issue and associate with the tracker

Copy link
Contributor Author

Choose a reason for hiding this comment

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

const combinedReducers = combineReducers({ katello: combineReducers({ [namespace]: reducer }) });
// Namespacing the initial state as well
const initialKatelloState = Immutable({ katello: { [namespace]: initialState } });
const store = createStore(combinedReducers, initialKatelloState, applyMiddleware(thunk));
Copy link
Member

Choose a reason for hiding this comment

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

Now that foremanReact can be used for testing, is this resolved now?

@johnpmitsch
Copy link
Contributor Author

@jeremylenz This has been fully updated now, though the tests will still fail. There will need to be a Foreman PR to remove deprecations that is causing some sort of cyclical dependency and I also had to update the dev dependencies to include a new package to mock network calls. Both of these are semi-related, I'll explain:

Problem and solution 1: mocking network requests

Usually packages are expected to be mocked by the application when they are directly used by the application itself. We have been doing this with axios-mock-adapter as we have been previously using axios by calling it directly in Katello. So in production this looks like:

Katello -> axios -> nodejs -> http call

and we can easily do the following with axios-mock-adapter, __mocks__ folder, or jest.mock during testing to mock the http calls:

Katello -> mocked version of axios

up until now, this has been working fine.

But this PR is now using the API middleware from foremanReact. So we are now doing:

Katello -> foremanReact -> axios -> nodejs -> http call

So this is now is a bit more complicated. If we use axios-mock-adapter, we are actually mocking the version of axios that we are using in Katello, not the one in Foreman. We can only mock our entrypoint into Foreman, which is the API middleware. Which defeats the whole purpose of using foremanReact directly during testing and goes back to what we were doing: mocking each part of foremanReact.

So basically we can't do this:

Katello -> foremanReact -> mocked version of axios

But we can use nock, which will mock Node's http.request method, meaning we can do things a level deeper:

Katello -> foremanReact -> axios -> mocked http module of nodejs

which works! So I used nock in this PR and it allows us to use the foremanReact API middleware as-is, but still mock the network calls. I usually try to avoid adding more dependencies, but this is a just a development dependency and the approach we have been using (which works because we only used axios in Katello) won't work with our architecture.

So from this and react-testing-library, I was able to only mock the network requests and use the component fully connected.

I moved all the testing to the top-level page and made sure I had the same tests as previously. The only one I am going to wait on are the expansion row tests, as there is not much in them yet to check for on screen. I would rather wait until they are built out more to write tests, otherwise I wind up doing things like adding test-id's

And now for the other problem:

Problem and solution 2: foremanReact internal use of deprecation causing issues in test

With no changes to Foreman, The tests show an error and traceback like this:

[vagrant@centos7-katello-devel-stable katello{new-cv-table}]$ npx jest webpack/scenes/ContentViews/__tests__/contentViewPage.test.js 
 FAIL  webpack/scenes/ContentViews/__tests__/contentViewPage.test.js
  ● Test suite failed to run

    TypeError: Cannot read property 'default' of undefined



      at Object.API (../foreman/webpack/assets/javascripts/react_app/redux/API/index.js:34:16)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/API.js:10:16)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/components/Layout/components/ImpersonateIcon/ImpersonateIconActions.js:1:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/components/Layout/components/ImpersonateIcon/index.js:5:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/components/Layout/components/UserDropdowns.js:8:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/components/Layout/Layout.js:11:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/components/Layout/index.js:16:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/redux/reducers/index.js:8:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/redux/index.js:6:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/foreman_navigation.js:4:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/foreman_tools.js:12:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/redux/API/API.js:3:1)
      at Object.<anonymous> (../foreman/webpack/assets/javascripts/react_app/redux/API/index.js:8:1)
      at Object.<anonymous> (webpack/test-utils/react-testing-lib-wrapper.js:7:1)
      at Object.<anonymous> (webpack/scenes/ContentViews/__tests__/contentViewPage.test.js:2:1)

From the research I did, this error is usually caused by cyclical dependencies. My understanding is the reason we haven't seen this in production is webpack is pretty forgiving in this area, but jest won't be. I'm guessing the various files/folders/modules named API are causing some confusion. Notice that line number 34 doesn't even exist in foreman/webpack/assets/javascripts/react_app/redux/API/index.js

We can't mock the specific deprecation for the same reason as above. We would have to mock our entry point into foremanReact, which is the APIMiddleware. We want to use the APIMiddleware in the actual testing. There is no way to say "use APIMiddlware but mock APIMiddleware's usage of the deprecated API further down the stack`.

Fortunately, the fix is easy, its just to remove Foreman's internal use of the deprecated API.js file. I have this done and just have to fix up the tests and will make a PR. It will block this PR until it is merged, but we can still finish up the review.

Hopefully this all makes sense! I tried to explain as thorough as I can as I've been digging into this the last couple days, but let me know if you have any questions :)

@jeremylenz
Copy link
Member

@johnpmitsch Thanks for the explanation! Adding the nock devDependency seems like a reasonable solution for problem 1 (though I still think there must be a way to use partial mocks but it's probably harder), and fixing Foreman's API files is a great solution for problem 2, and something we should have done anyway.

@johnpmitsch
Copy link
Contributor Author

@jeremylenz I opened the Foreman PR here theforeman/foreman#7638 - there is just one test left to figure out there.

Please take another look and see if you have any other concerns for this PR

Copy link
Member

@jeremylenz jeremylenz left a comment

Choose a reason for hiding this comment

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

@johnpmitsch No further concerns, ACK pending successful test runs with the Foreman PR.

109,
108,
111,
110
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 using 2 contents repos and ids would be more than enough as a fixture,
And reduce the size of the snaps too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are no snaps since this is using r-t-lib, is there a more specific concern? These are actual API responses, the file is only 5kb and is just used in testing.

@johnpmitsch
Copy link
Contributor Author

[test katello]

Copy link
Contributor

@parthaa parthaa left a comment

Choose a reason for hiding this comment

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

APJ

This adds a new content view table using patternfly 4 to the page "/labs/content_views"
@johnpmitsch
Copy link
Contributor Author

[test katello]

@johnpmitsch johnpmitsch merged commit 44f7120 into Katello:master May 6, 2020
@johnpmitsch johnpmitsch deleted the new-cv-table branch May 6, 2020 20:48
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.

9 participants