-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Conversation
💚 Build Succeeded |
There was a problem hiding this 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.
This comment was marked as resolved.
Sorry, something went wrong.
if (this.state.showNoResultsMessage && this.props.onInit) { | ||
this.props.onInit(); | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
💔 Build Failed |
Jenkins, test this |
💚 Build Succeeded |
There was a problem hiding this 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>; |
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!'); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about moving this to render()
? See "What about memoization?" in "You Probably Don't Need Derived State"
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
💚 Build Succeeded |
💔 Build Failed |
💔 Build Failed |
Jenkins, test this |
There was a problem hiding this 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.
src/ui/public/utils/memoize.ts
Outdated
*/ | ||
function memoizeLast<T extends (...args: any[]) => any>(func: T): T { | ||
let prevResult: any; | ||
let called: boolean = false; |
There was a problem hiding this comment.
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.
💔 Build Failed |
💔 Build Failed |
💔 Build Failed |
Jenkins, test this |
💚 Build Succeeded |
* 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
* 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
How do we test this @timroes ? Do I need to look at anything in UI or it's just code changes? Thanks! |
@bhavyarm This should not change any functionality but should just be code changes. |
This PR converts the components used in the visualization rendering infrastructure to TypeScript. It also fixes a couple of issues:
onInit
also when no data should be shown (earlieronInit
was only called in case the no data screen wasn't shown)uiState
properly to the<VisualizationChart/>
component (which was missing earlier), so it can now again correctly calculate the update status foruiState
.It also improves the code a bit:
Subject
inVisualizationChart