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

UiFramework: Modals in react #11500

Merged
merged 9 commits into from
May 3, 2017

Conversation

stacey-gammon
Copy link
Contributor

Add modals in the ui framework. Not used yet in angular.

screen shot 2017-04-28 at 2 22 11 pm

@@ -4,9 +4,15 @@ export {
KuiButtonIcon,
KuiLinkButton,
KuiSubmitButton,
} from './button';
} from './button/index';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason you explicitly have to include index here? This doesn't seem to be related to this PR at all...

const app = uiModules.get('app/kibana', ['react']);
app.directive('toolBarSearchBox', function (reactDirective) {
return reactDirective(KuiToolBarSearchBox);
});
app.directive('confirmModal', function (reactDirective) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any future plans for removing this and being able to use React components like we would use Angular components? Or, I'm assuming this gets auto-loaded somewhere in the Angular bootup, so is the plan to no longer import the components we need wherever we need them? I'm a bit out of touch with our "React in Angular" plans...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It gets imported in public/kibana, so anyone can use them. For ui_framework components, which are generic, it's nice to include them once so you can use them everywhere. For react components that are specific to a certain area, then I would import them where you need them.

So no, no plans to remove this.

export {
KuiConfirmModal,
KuiModalOverlay
} from './modal/index';
Copy link
Contributor

Choose a reason for hiding this comment

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

Here as well, why do you need to include the index?

}

KuiConfirmModal.propTypes = {
message: React.PropTypes.string,
Copy link
Contributor

@w33ble w33ble May 1, 2017

Choose a reason for hiding this comment

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

Note: React.PropTypes is deprecated as of React v15.5. Please use the prop-types library instead.

React.PropTypes has been depricated, so we shouldn't be using them in new code. Use the prop-types module instead.


export function KuiModal({ children, ...rest }) {
return (
<div className="kuiModal" { ...rest }>
Copy link
Contributor

Choose a reason for hiding this comment

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

I really dislike this "rest" pattern. I much prefer to be explicit about the arguments you pass along, or possibly pull them off context or something (though I'll admit I don't even know if that's a thing, I haven't ever used context before...).

But, that conversation aside, doing this means that you can clobber the className on components. Here's an example. Is this intentional? Do you want the className to be overridden?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with you @w33ble, but we've had this discussion in a previous PR for React buttons. Here's some context:

That being said, you're right about the problem with clobbering the className. We should be concatenating these strings, not overriding them:

const {
  className,
  children,
  ...rest
} = props

const classes = classnames('my-class', className)

return <a className={ classes } { ...rest }>{ children }</a>

Copy link
Contributor

Choose a reason for hiding this comment

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

I mostly dislike the "rest" pattern unless it's for low-level components that are basically just "simple DOM wrappers" where we usually want to push multiple attributes through (like a wrapper around a, where we don't actually do anything to href etc in the component).

(Though, I've never built components in this way where they are all just "one-line wrappers all the way down", so I don't have any experience in this context to base my opinions on)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pulled out className to avoid overwriting them.

@kjbekkelund... so you are saying use ...rest for this component because it's a simple wrapper, but for the likes of KuiConfirmModal, which is more complicated, manually pass them in?

I'm fine with whatever.

What I think would be nice is a helper function to build these components, where all they are doing is using a className with children. There are so many of them that look identical except for a class name. But I hear a lot of feedback that I tend to over-DRY so I'll leave that alone for now. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI, these required html props, as far as I can tell, have to be handled somewhat specially because of the - in their name. So instead of doing:

  KuiConfirmModal({ stuff, aria-label, data-test-subj}) {
 ....

You have to do:

  KuiConfirmModal({ stuff, ...rest }) {
  const ariaLabel = rest['aria-label'];
  const dataTestSubj = rest['data-test-subj'];
 ....

Unless anyone else knows a better way to handle that?

Copy link
Contributor

Choose a reason for hiding this comment

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

@stacey-gammon Not sure what I think of this myself, but this was my thinking: My main "concern/thought" around wrappers is that if it's a DOM wrapper you know what can be in rest (any relevant html attribute), but if it's a React component that becomes a bit more unclear (it also makes the api unclear, as it's not super-clear what you actually expect to receive and pass down into lower-level components).

Also: React will let you know if DOM elements receives the wrong attribute (https://facebook.github.io/react/warnings/unknown-prop.html).

However, I also think there are edge-cases to this. E.g. on Cloud we had a couple different Button helpers that receive rest even if they weren't "pure DOM components" (they passed on to lower-level button helpers, e.g. DangerButton wrapped SpinButton that wrapped regular DOM buttons).

I think rest is a good approach if you're expected to see it as a "DOM component" from the outside (e.g. a DangerButton that might have some extra fields, but you can give it everything you can give a normal DOM button).

Historically I've added these rest params based on actual need in the code, but that might be a bit different in this context as 1) the team is much larger, 2) we're building a UI framework.

import { KuiModalFooter } from './modal_footer';
import { KuiModalHeader } from './modal_header';
import { KuiModalBody } from './modal_body';
import { KuiModalBodyText } from './modal_body_text';
Copy link
Contributor

Choose a reason for hiding this comment

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

All of these are exposed in ../modal, why not pull them all from there? Or, pull them all from ../index since they also exist in there? I don't like the root level index personally, but you're already using it below for the button...

Copy link
Contributor

Choose a reason for hiding this comment

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

index.js also exports this module... doesn't it create a circular dependency if you import from that module while also being exported from it?

Copy link
Contributor

@w33ble w33ble May 1, 2017

Choose a reason for hiding this comment

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

Ah, yes, I suppose it would. One more reason for me to dislike the root level index.js that exports all the modules then. 🏆

My point about just using ../modal still stands though.

</button>
</div>
</div>
<confirm-modal
Copy link
Contributor

@w33ble w33ble May 1, 2017

Choose a reason for hiding this comment

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

I assume this works because of the react_components.js file, which I'm assuming is auto loaded somewhere else. It's really odd that the src/ui/public/modals/confirm_modal.* code doesn't actually contain a reference to the React component it's using. That's a fair bit of indirection, and someone unfamiliar with the codebase probably won't know to go digging into the ui-framework code to find this, since it's not imported anywhere close to where it's being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, and yea, it's a good point, but you have to create the angular directives that map to the react component, and I believe there can only be one. So if the react confirm modals in a couple different places, which one is going to register the directive? @weltenwort does that sound accurate?

I tried to find the original discussion for where this model came from, and it appears in the very first react icons PR (#10247), but couldn't find the discussion. Perhaps it was offline - @kjbekkelund or @cjcenizal, do you remember us discussing this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure — I'm not that knowledgeable about our Angular setup. I also agree with @w33ble (if it's not too painful to move in that direction)

Copy link
Member

@weltenwort weltenwort May 3, 2017

Choose a reason for hiding this comment

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

yes, the angular directive is globally registered in the body of
react_component.js:

app.directive('confirmModal', function (reactDirective) {
  return reactDirective(KuiConfirmModal);
});

Since javascript modules are singletons, it will only be executed once, no
matter how often that file is imported. If a directive of the same name is
registered again in some other place, the order of execution of the two
statements matters. The last directive definition wins.

All directives are global in an angular 1 app (as are services, filters,
etc...). I don't think there's a way around that. That's one of the things
angular 2 does better (and react, of course). The import and the usage will
always be in separate files, because you cannot import anything in an angular
template.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case, I think we should stick with this path, so we ensure modules from the uiFramework are only morphed into directives once.

In either case, if we want to have a longer discussion about this, I say we move it out of this specific PR since it's not something new that this PR introduced.

Copy link
Contributor

@w33ble w33ble May 3, 2017

Choose a reason for hiding this comment

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

I say we move it out of this specific PR since it's not something new that this PR introduced

Agreed. I'm very much against auto-loading modules on startup, we used to do that, and it got very messy; explicit imports are much easier to understand. I don't know why the auto-loading stuff has come back and why there hasn't been a strong push against it. But that's not a conversation for this PR, and I'm glad to see it hasn't stopped it from progressing. I was just trying to understand our plans around the Angular+React stuff.

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

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

This is awesome, just had a few requests and questions.

@@ -4,6 +4,7 @@
border: 1px solid $modalBorderColor;
border-radius: $globalBorderRadius;
box-shadow: 0 5px 22px rgba(#000000, 0.25);
z-index: 1001;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you create a variable called $globalModalDepth in the variables file and assign this value there?

@snide Just thought you'd like to see me making this kind of snideresque comment. Cherish this moment!

import { KuiModalFooter } from './modal_footer';
import { KuiModalHeader } from './modal_header';
import { KuiModalBody } from './modal_body';
import { KuiModalBodyText } from './modal_body_text';
Copy link
Contributor

Choose a reason for hiding this comment

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

index.js also exports this module... doesn't it create a circular dependency if you import from that module while also being exported from it?

</div>
</div>
<confirm-modal
className="kui"
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this do? I don't see this class defined anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

whoops, removed

import { KuiModalBodyText } from './modal_body_text';
import { KuiButton } from '../index';

export function KuiConfirmModal({ message, title, onCancel, onConfirm, cancelButtonText, confirmButtonText, ...rest }) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind putting each of these these variables on its own line? I find it much easier to scan when there are so many properties.

export function KuiConfirmModal({
  message,
  title,
  onCancel,
  onConfirm,
  cancelButtonText,
  confirmButtonText,
  ...rest,
}) {

Copy link
Contributor

Choose a reason for hiding this comment

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

If you're going to do that, this looks cleaner:

export function KuiConfirmModal(props) {
  const {
    message,
    title,
    onCancel,
    onConfirm,
    cancelButtonText,
    confirmButtonText,
    ...rest,
  } = props;
  //...
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer @cjcenizal's approach instead of receiving props then immediately destructuring (so keeping as-is, just splitting over multiple lines).

{
title ?
<KuiModalHeader>
<div className="kuiModalHeader__title" data-test-subj="confirmModalTitleText">
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we create a KuiModalHeaderTitle component to wrap this?

Copy link
Contributor Author

@stacey-gammon stacey-gammon May 2, 2017

Choose a reason for hiding this comment

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

Yea. Just to confirm, you are opposed to refactoring these div wrappers that have just a className? There are so many of them and creating a separate file + test for each one creates so much noise. So much copy/pasting when creating one.

expect(component).toMatchSnapshot();

component.find('[data-test-subj="confirmModalConfirmButton"]').simulate('click');
sinon.assert.calledOnce(onConfirm);
Copy link
Contributor

@cjcenizal cjcenizal May 1, 2017

Choose a reason for hiding this comment

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

Do we need this assertion here if we have it in the onConfirm test?

{ ...requiredProps }
/>);
onConfirm.reset();
onCancel.reset();
Copy link
Contributor

Choose a reason for hiding this comment

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

After looking through https://facebook.github.io/jest/docs/setup-teardown.html, I think we should take advantage of the beforeEach setup function.

Can we change the spies to be let variables, and just reassign them inside of a beforeEach?

let onConfirm, onCancel;

beforeEach(() => {
  onConfirm = sinon.spy();
  onCancel = sinon.spy();
});


export function KuiModal({ children, ...rest }) {
return (
<div className="kuiModal" { ...rest }>
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with you @w33ble, but we've had this discussion in a previous PR for React buttons. Here's some context:

That being said, you're right about the problem with clobbering the className. We should be concatenating these strings, not overriding them:

const {
  className,
  children,
  ...rest
} = props

const classes = classnames('my-class', className)

return <a className={ classes } { ...rest }>{ children }</a>

@w33ble w33ble self-assigned this May 1, 2017
>
<KuiModal
aria-label="aria-label"
className="testClass1 testClass2"
Copy link
Contributor

Choose a reason for hiding this comment

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

missing modal class

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch. Fixing the className collision fixed all these.

const onCancel = sinon.spy();

test('renders KuiConfirmModal', () => {
const component = mount(<KuiConfirmModal
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the snapshot look like if you shallow render this instead? (As it's built up of purely lower-level components it might make it include "less noise")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting, didn't know that, yep, looks much better. switched!

@pugnascotia
Copy link
Contributor

Just curious - was the react-overlays package considered at all?

@stacey-gammon
Copy link
Contributor Author

@pugnascotia I haven't looked at that one. https://github.com/tajo/react-portal has been brough tup, but due to the react embedded in angular nature of this, I'm not sure we can take advantage of it just yet. Once we have a react component that needs to use the react modals, we'll take another look, but for right now, we can't get too fancy because of the angular side of things. At least that is my understanding of it.

@w33ble w33ble removed their assignment May 2, 2017
Copy link
Contributor

@w33ble w33ble left a comment

Choose a reason for hiding this comment

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

Down with React.PropTypes

}

KuiModal.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

}

KuiModalBody.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

}

KuiModalBodyText.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

}

KuiModalFooter.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

}

KuiModalHeader.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

}

KuiModalHeaderTitle.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

}

KuiModalOverlay.propTypes = {
className: React.PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

More React.PropTypes, which we should stop using. Please use the prop-types module.

Copy link
Contributor

@cjcenizal cjcenizal left a comment

Choose a reason for hiding this comment

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

LGTM!

@stacey-gammon
Copy link
Contributor Author

Build failure seems unrelated:

INFO - ecMoIzc - meta_data_create_index_service - [.kibana] creating index, cause [api], templates [], shards [1]/[1], mappings [_default_, index-pattern, server, visualization, search, timelion-sheet, config, dashboard, url]
     └- ✖ fail: " "before each" hook: global before each for "should show the kibana plugin as ready""
     │        Service Unavailable
     │         :: {"path":"/.kibana/config/6.0.0-alpha1","query":{},"statusCode":503,"response":""}
     │         at respond (node_modules/elasticsearch/src/lib/transport.js:295:15)
     │         at checkRespForFailure (node_modules/elasticsearch/src/lib/transport.js:254:7)
     │         at HttpConnector.<anonymous> (node_modules/elasticsearch/src/lib/connectors/http.js:157:7)
     │         at IncomingMessage.bound (node_modules/elasticsearch/node_modules/lodash/dist/lodash.js:729:21)
     │         at endReadableNT (_stream_readable.js:974:12)
     │         at _combinedTickCallback (internal/process/next_tick.js:80:11)
     │         at process._tickDomainCallback (internal/process/next_tick.js:128:9)
     │       

Up next: jest tests
- Relies on elastic#10821 getting
checked in first for commonHtmlProps
- Can’t use the modalOverlay or it would be two nested react roots, due
to the way it’s embedded in angular.
I have no idea why the introduction of react would cause this to fail
as it was, but for some reason, grabbing the button reference has to be
after the digest loop.
@stacey-gammon stacey-gammon merged commit 8fca519 into elastic:master May 3, 2017
stacey-gammon added a commit to stacey-gammon/kibana that referenced this pull request May 3, 2017
* Reactify the confirmation modal

Up next: jest tests

* Add tests

- Relies on elastic#10821 getting
checked in first for commonHtmlProps

* Don't include the overlay as part of the confirm modal component

* Use the react version of a confirmation modal

- Can’t use the modalOverlay or it would be two nested react roots, due
to the way it’s embedded in angular.

* Add snapshots

* Fix tests

* fix confirm_modal_promise tests

I have no idea why the introduction of react would cause this to fail
as it was, but for some reason, grabbing the button reference has to be
after the digest loop.

* Address code review comments

* switch over the remaining React.PropType references (in the modals dir anyway)
stacey-gammon added a commit that referenced this pull request May 3, 2017
* Reactify the confirmation modal

Up next: jest tests

* Add tests

- Relies on #10821 getting
checked in first for commonHtmlProps

* Don't include the overlay as part of the confirm modal component

* Use the react version of a confirmation modal

- Can’t use the modalOverlay or it would be two nested react roots, due
to the way it’s embedded in angular.

* Add snapshots

* Fix tests

* fix confirm_modal_promise tests

I have no idea why the introduction of react would cause this to fail
as it was, but for some reason, grabbing the button reference has to be
after the digest loop.

* Address code review comments

* switch over the remaining React.PropType references (in the modals dir anyway)
@stacey-gammon stacey-gammon deleted the modals-in-react branch May 3, 2017 15:06
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.

6 participants