-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Changes from 4 commits
dff08ed
3574c37
ca679e0
91860ac
83b66fd
231d214
9062384
06ee085
361e2a8
8f06918
b8e57cc
dc4c456
a919b5d
84733cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import { memoizeLast } from './memoize'; | ||
|
||
describe('memoizeLast', () => { | ||
type SumFn = (a: number, b: number) => number; | ||
let originalSum: SumFn; | ||
let memoizedSum: SumFn; | ||
|
||
beforeEach(() => { | ||
originalSum = jest.fn((a, b) => a + b); | ||
memoizedSum = memoizeLast(originalSum); | ||
}); | ||
|
||
it('should call through function', () => { | ||
expect(memoizedSum(26, 16)).toBe(42); | ||
expect(originalSum).toHaveBeenCalledWith(26, 16); | ||
}); | ||
|
||
it('should memoize the last call', () => { | ||
memoizedSum(26, 16); | ||
expect(originalSum).toHaveBeenCalledTimes(1); | ||
memoizedSum(26, 16); | ||
expect(originalSum).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should use parameters as cache keys', () => { | ||
expect(memoizedSum(26, 16)).toBe(42); | ||
expect(originalSum).toHaveBeenCalledTimes(1); | ||
expect(memoizedSum(16, 26)).toBe(42); | ||
expect(originalSum).toHaveBeenCalledTimes(2); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
/** | ||
* A simple memoize function, that only stores the last returned value | ||
* and uses the identity of all passed parameters as a cache key. | ||
*/ | ||
function memoizeLast<T extends (...args: any[]) => any>(func: T): T { | ||
let prevResult: any; | ||
let called: boolean = false; | ||
let prevThis: any; | ||
let prevArgs: any[]; | ||
|
||
// We need to use a `function` here for proper this passing. | ||
// tslint:disable-next-line:only-arrow-functions | ||
const memoizedFunction = function(this: any, ...args: any[]) { | ||
if ( | ||
called && | ||
prevThis === this && | ||
prevArgs.length === args.length && | ||
args.every((arg, index) => arg === prevArgs[index]) | ||
) { | ||
return prevResult; | ||
} | ||
called = true; | ||
prevThis = this; | ||
prevArgs = args; | ||
prevResult = func.apply(this, args); | ||
return prevResult; | ||
} as T; | ||
|
||
return memoizedFunction; | ||
} | ||
|
||
export { memoizeLast }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import { get } from 'lodash'; | |
import React from 'react'; | ||
|
||
import { PersistedState } from '../../persisted_state'; | ||
import { memoizeLast } from '../../utils/memoize'; | ||
import { Vis } from '../../vis'; | ||
import { VisualizationChart } from './visualization_chart'; | ||
import { VisualizationNoResults } from './visualization_noresults'; | ||
|
@@ -43,51 +44,28 @@ interface VisualizationProps { | |
visData: any; | ||
} | ||
|
||
interface VisualizationState { | ||
listenOnChange: boolean; | ||
showNoResultsMessage: boolean; | ||
} | ||
|
||
export class Visualization extends React.Component<VisualizationProps, VisualizationState> { | ||
public static getDerivedStateFromProps( | ||
props: VisualizationProps, | ||
prevState: VisualizationState | ||
): Partial<VisualizationState> | null { | ||
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!'); | ||
} | ||
|
||
const showNoResultsMessage = shouldShowNoResultsMessage(props.vis, props.visData); | ||
if (prevState.showNoResultsMessage !== showNoResultsMessage) { | ||
return { showNoResultsMessage }; | ||
} | ||
return null; | ||
} | ||
export class Visualization extends React.Component<VisualizationProps> { | ||
private showNoResultsMessage = memoizeLast(shouldShowNoResultsMessage); | ||
|
||
constructor(props: VisualizationProps) { | ||
super(props); | ||
|
||
const { vis, visData, uiState, listenOnChange } = props; | ||
const { vis, uiState, listenOnChange } = props; | ||
|
||
vis._setUiState(props.uiState); | ||
if (listenOnChange) { | ||
uiState.on('change', this.onUiStateChanged); | ||
} | ||
|
||
this.state = { | ||
listenOnChange, | ||
showNoResultsMessage: shouldShowNoResultsMessage(vis, visData), | ||
}; | ||
} | ||
|
||
public render() { | ||
const { vis, visData, onInit, uiState } = this.props; | ||
|
||
const noResults = this.showNoResultsMessage(vis, visData); | ||
|
||
return ( | ||
<div className="visualization"> | ||
{this.state.showNoResultsMessage ? ( | ||
{noResults ? ( | ||
<VisualizationNoResults onInit={onInit} /> | ||
) : ( | ||
<VisualizationChart vis={vis} visData={visData} onInit={onInit} uiState={uiState} /> | ||
|
@@ -96,10 +74,29 @@ export class Visualization extends React.Component<VisualizationProps, Visualiza | |
); | ||
} | ||
|
||
public shouldComponentUpdate(nextProps: VisualizationProps): boolean { | ||
if (nextProps.uiState !== this.props.uiState) { | ||
throw new Error('Changing uiState on <Visualization/> is not supported!'); | ||
} | ||
return true; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about removing this logic from the You can pass the This will decouple the In the future if we want to move the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
public componentWillUnmount() { | ||
this.props.uiState.off('change', this.onUiStateChanged); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So if the We should probably either copy 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 So I actually moved the check to Of course the ideal solution would still be allowing that |
||
} | ||
|
||
public componentDidUpdate(prevProps: VisualizationProps) { | ||
const { listenOnChange } = this.props; | ||
// If the listenOnChange prop changed, we need to register or deregister from uiState | ||
if (prevProps.listenOnChange !== listenOnChange) { | ||
if (listenOnChange) { | ||
this.props.uiState.on('change', this.onUiStateChanged); | ||
} else { | ||
this.props.uiState.off('change', this.onUiStateChanged); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* In case something in the uiState changed, we need to force a redraw of | ||
* the visualization, since these changes could effect visualization rendering. | ||
|
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 initializingprevArgs
to an array containing a unique symbol that is not exported from this module. Then the conditional insidememoizedFunction
would never be true on the first execution.