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

TypeScriptify visualization components #20940

Merged
merged 14 commits into from
Jul 23, 2018
Merged

Conversation

timroes
Copy link
Contributor

@timroes timroes commented Jul 18, 2018

This PR converts the components used in the visualization rendering infrastructure to TypeScript. It also fixes a couple of issues:

  • We now call onInit also when no data should be shown (earlier onInit was only called in case the no data screen wasn't shown)
  • We now pass in uiState properly to the <VisualizationChart/> component (which was missing earlier), so it can now again correctly calculate the update status for uiState.

It also improves the code a bit:

  • Replace the "storing observer mechanism" by using a Subject in VisualizationChart

@timroes timroes added bug Fixes for quality problems that affect the customer experience chore Feature:Visualizations Generic visualization features (in case no more specific feature label is available) v7.0.0 v6.4.0 labels Jul 18, 2018
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Member

@markov00 markov00 left a comment

Choose a reason for hiding this comment

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

Code LGTM.
I've left few minor comments and one question/doubt.
Could you please rebase/merge master so we can test this after the flickering fix?

@@ -69,6 +52,28 @@ describe('Visualization', () => {
set: () => {}

This comment was marked as resolved.

if (this.state.showNoResultsMessage && this.props.onInit) {
this.props.onInit();
}
}
Copy link
Member

Choose a reason for hiding this comment

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

What about removing this logic from the Visualization component and add it to the VisualizationNoResults component?

You can pass the onInit function to VisualizationNoResults, build it as a Component instead of a SFC and call the onInit there.

This will decouple the onInit calls from the Visualization and moving the initialization callback responsibility to the rendered components. In a way it will be more clear to have the onInit inside each rendered component, than having a statement that check this on it's parent component.

In the future if we want to move the VisualizationNoResults logic/display inside the VisualizationChart we can do just removing the VisualizationNoResults component and passing showNoResultsMessage , without change any other logic.

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 point. Will do that. Also I think we should discuss next week with Peter, if we can actually already remove it, because it really seems unused inside Kibana, and we can just declare that a change for visualizations in general, to bring their own no data screen.

filter(
({ vis, visData, container }) => vis && container && (!vis.type.requiresSearch || visData)
),
debounceTime(100),
Copy link
Member

Choose a reason for hiding this comment

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

I know that this was here before, but is there any more "pragmatic" way to avoid this?
Adding timers and debouncing usually increase the possibility to have flaky tests because we are not tied to a specific and clear state and we are saying: ok maybe there will be other 2 or 3 subsequent call/updates of the renderSubject one next to the other and it's better to wait some time.
I think this kind of uncertainty brings the flakyness to our test.
Any thoughts on that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The most common use-case at the moment is resizing the visualization, e.g. by resizing the window or resizing the dashboard panel it's in. In case we don't debounce, we are very laggy due to too many renders, while scaling a dashboard panel. Though it wouldn't be needed in all rendering triggering cases, but I am not sure if we actually simplify the logic and increase maintainability if we separate between renders, that need the debounce and some that doesn't.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@timroes
Copy link
Contributor Author

timroes commented Jul 19, 2018

Jenkins, test this

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

Not being too familiar with the visualization code, I could not find anything wrong with the TypeScript usage. I left some comments regarding the React usage below. If you think these will be resolved in follow-up PRs anyway, feel free to ignore them for now.


export class VisualizationController {
constructor(element: HTMLElement, vis: Vis);
public render(visData: any, update: { [key in Status]: boolean }): Promise<void>;
Copy link
Member

Choose a reason for hiding this comment

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

Given that { [key in Status]: boolean } is used in more than one place now (e.g. here and the return value of getUpdateStatus), how about factoring it out into its own type in update_status.ts?

export type StatusUpdates<Keys extends Status> = {
  [key in Keys]: boolean;
}

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 think that's actually not working that well, since in the getUpdateStatus we actually use a different index signature, since we actually make sure, it returns an object only containing the status updates, that have been request as parameter. So I currently don't see any other place we would be using this - and hopefully it won't become more, because we are trying to get rid of the status calculation and not increasing it :D

Copy link
Member

Choose a reason for hiding this comment

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

good point 👍

const listenOnChangeChanged = props.listenOnChange !== prevState.listenOnChange;
const uiStateChanged = props.uiState && props.uiState !== props.vis.getUiState();
if (listenOnChangeChanged || uiStateChanged) {
throw new Error('Changing listenOnChange or uiState props is not allowed!');
Copy link
Member

Choose a reason for hiding this comment

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

As we discussed directly already, I think abusing this function as a "validator" for props by consciously throwing an error is problematic for a few reasons. Primarily, react components do not support write-once properties which means this violates the general component contract. On the contrary, props that communicate state (as the name of uiState suggests it does) must change identity to signal that state change. Additionally, which stack trace this error bubbles up through is an implementation detail that should not be relied upon.

Instead I would recommend to check for a change of uiState here to possibly set a flag on the state and render an appropriate error message in render(). Changes to listenOnChange could be easily handled by checking for a change in componentDidUpdate and (un)subscribing accordingly.

throw new Error('Changing listenOnChange or uiState props is not allowed!');
}

const showNoResultsMessage = shouldShowNoResultsMessage(props.vis, props.visData);
Copy link
Member

Choose a reason for hiding this comment

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

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 addressed that and memoized the function instead (way better readable also :D)

}

public componentWillUnmount() {
this.props.uiState.off('change', this.onUiStateChanged);
Copy link
Member

Choose a reason for hiding this comment

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

So if the uiState was replaced with a new object and getDerivedStateFromProps threw the problematic error, the component will unmount and this will be called. Is this.props.uiState still the old object that we subscribed on or do we leak memory here?

We should probably either copy uiState to this.state (meh) or, on subscription, save an unsubscribe function:

private unsubscribeFromUiStateChange: (() => void)) = () => undefined;

private subscribeToUiStateChange() {
  const { uiState } = this.props;

  uiState.on('change', this.onUiStateChanged);
  
  this.unsubscribeFromUiStateChange = () => {
    uiState.off('change', this.onUiStateChanged);
    this.unsubscribeFromUiStateChange = () => undefined;
  }
}

public componentWillUnmount() {
  this.unsubscribeFromUiStateChange();
}

We could also just un- and re-subscribe in componentDidUpdate() whenever the prop changed, but as long as getDerivedStateFromProps() throws that error, I would rely on that working quite right.

Copy link
Contributor Author

@timroes timroes Jul 20, 2018

Choose a reason for hiding this comment

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

I addressed this issue now in a different way. Since I was able to remove the state completely, using getDerivedStateFromProps would anyway be nasty, because as long as that method exists I need to initialize an empty state (even if my typing said there is none), see previous test failure.

So I actually moved the check to shouldComponentUpdate and throw the error there. This will also have the sideeffect, that in case we throw there, the properties in componenWillUnmount are still the old ones, and we deregister from the correct uiState. (see that example: http://jsfiddle.net/us5ojhwv/10/)

Of course the ideal solution would still be allowing that uiState switch, but I fear that's a bit too much trouble to change right now, and since I also didn't introduce it with this PR, I would rather postpone this to a later PR/point in time, if it would be okay for you?


public componentDidUpdate() {
if (this.props.onInit) {
this.props.onInit();
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, why should onInit() be called after every render?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you use the visualization loader to render a visualization, you might end up calling it again, in case something changed on your behalf. If you then render "over" an already mounted component, that component would just update, but nevertheless we want to resolve the promise returned from that called (that's passed to the onInit method of the chart).

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@timroes
Copy link
Contributor Author

timroes commented Jul 20, 2018

Jenkins, test this

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

That looks quite good (pending green CI)! Thanks for investing the time.

*/
function memoizeLast<T extends (...args: any[]) => any>(func: T): T {
let prevResult: any;
let called: boolean = false;
Copy link
Member

Choose a reason for hiding this comment

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

We could get rid of called by initializing prevArgs to an array containing a unique symbol that is not exported from this module. Then the conditional inside memoizedFunction would never be true on the first execution.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@timroes
Copy link
Contributor Author

timroes commented Jul 23, 2018

Jenkins, test this

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@timroes timroes merged commit d171aa8 into elastic:master Jul 23, 2018
@timroes timroes deleted the ts-visualization branch July 23, 2018 14:21
timroes added a commit to timroes/kibana that referenced this pull request Jul 23, 2018
* Refactor vis components to TypeScript

* Fix issue with ResizeChecker

* Fix calling onInit for no data

* Explicit named export

* Add title to vistype

* Fix error in test file

* Move onInit to no VisualizationNoResults

* Make listenOnChange changeable

* Add memoize util

* Use memoize for no results check

* Address issue with uiState

* Optimize memoize function
timroes added a commit that referenced this pull request Jul 23, 2018
* Refactor vis components to TypeScript

* Fix issue with ResizeChecker

* Fix calling onInit for no data

* Explicit named export

* Add title to vistype

* Fix error in test file

* Move onInit to no VisualizationNoResults

* Make listenOnChange changeable

* Add memoize util

* Use memoize for no results check

* Address issue with uiState

* Optimize memoize function
@bhavyarm
Copy link
Contributor

bhavyarm commented Aug 7, 2018

How do we test this @timroes ? Do I need to look at anything in UI or it's just code changes? Thanks!

@timroes
Copy link
Contributor Author

timroes commented Aug 7, 2018

@bhavyarm This should not change any functionality but should just be code changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Fixes for quality problems that affect the customer experience chore Feature:Visualizations Generic visualization features (in case no more specific feature label is available) v6.4.0 v7.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants