From 996e9aa57976769d15f163e30018e20d223e02c4 Mon Sep 17 00:00:00 2001 From: Greg Tatum Date: Thu, 11 Jan 2018 16:20:59 -0600 Subject: [PATCH] Upgrade flow to v63, and add explicitConnect --- .eslintrc.js | 2 +- package.json | 4 +- src/actions/profile-view.js | 14 +- src/actions/receive-profile.js | 14 +- src/components/app/Home.js | 28 +- src/components/app/Initializing.js | 67 +- src/components/app/ProfileFilterNavigator.js | 28 +- src/components/app/ProfileSharing.js | 45 +- src/components/app/ProfileViewer.js | 51 +- src/components/app/Root.js | 59 +- .../app/SymbolicationStatusOverlay.js | 29 +- src/components/app/UrlManager.js | 58 +- src/components/calltree/CallTree.js | 63 +- src/components/calltree/FilterNavigatorBar.js | 12 +- src/components/calltree/NodeIcon.js | 44 +- .../calltree/ProfileCallTreeContextMenu.js | 66 +- src/components/calltree/TransformNavigator.js | 32 +- src/components/header/EmptyThreadIndicator.js | 3 +- .../header/IntervalMarkerOverview.js | 35 +- .../header/ProfileThreadHeaderBar.js | 46 +- .../header/ProfileThreadHeaderContextMenu.js | 37 +- .../header/ProfileThreadJankOverview.js | 35 +- .../ProfileThreadTracingMarkerOverview.js | 39 +- src/components/header/ProfileViewerHeader.js | 44 +- src/components/header/ThreadStackGraph.js | 2 +- src/components/marker-chart/Canvas.js | 72 +- src/components/marker-chart/index.js | 92 +- src/components/marker-table/ContextMenu.js | 37 +- src/components/marker-table/Settings.js | 30 +- src/components/marker-table/index.js | 25 +- src/components/shared/IdleSearchField.js | 36 +- src/components/shared/StackSearchField.js | 31 +- src/components/shared/Tooltip.js | 7 +- src/components/shared/WithSize.js | 24 +- src/components/shared/chart/Viewport.js | 877 ++++++++++-------- src/components/stack-chart/Canvas.js | 145 ++- src/components/stack-chart/Settings.js | 32 +- src/components/stack-chart/index.js | 92 +- .../tasktracer/ProfileTaskTracerView.js | 13 +- src/reducers/icons.js | 2 +- src/reducers/profile-view.js | 1 - src/reducers/url-state.js | 2 +- src/test/components/MarkerChart.test.js | 4 +- ...ProfileThreadTracingMarkerOverview.test.js | 13 +- .../components/ProfileViewerHeader.test.js | 13 +- src/test/components/StackChart.test.js | 4 +- .../ProfileViewerHeader.test.js.snap | 3 - src/test/fixtures/mocks/canvas-context.js | 3 +- src/test/store/icons.test.js | 6 +- src/test/store/transforms.test.js | 2 +- src/types/actions.js | 3 +- src/types/indexeddb.js | 10 +- src/utils/connect.js | 106 +++ src/utils/flow.js | 18 + yarn.lock | 61 +- 55 files changed, 1540 insertions(+), 1081 deletions(-) create mode 100644 src/utils/connect.js diff --git a/.eslintrc.js b/.eslintrc.js index 44e227a83c..58e3b32265 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -92,7 +92,7 @@ module.exports = { react: { pragma: 'React', version: '15.0', - flowVersion: '0.54.1', + flowVersion: '0.63.1', }, }, }; diff --git a/package.json b/package.json index 093d524763..0712945930 100644 --- a/package.json +++ b/package.json @@ -121,9 +121,9 @@ "express": "^4.15.4", "fake-indexeddb": "^2.0.3", "file-loader": "^0.11.2", - "flow-bin": "^0.54.1", + "flow-bin": "^0.63.1", "flow-coverage-report": "^0.4.0", - "flow-typed": "^2.1.5", + "flow-typed": "^2.2.3", "html-webpack-plugin": "^2.30.1", "http-server": "^0.10.0", "husky": "^0.14.3", diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 1ced41aa48..2a690e5797 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -312,12 +312,14 @@ export function addTransformToStack( } export function popTransformsFromStack( - threadIndex: ThreadIndex, firstRemovedFilterIndex: number -): Action { - return { - type: 'POP_TRANSFORMS_FROM_STACK', - threadIndex, - firstRemovedFilterIndex, +): ThunkAction { + return (dispatch, getState) => { + const threadIndex = getSelectedThreadIndex(getState()); + dispatch({ + type: 'POP_TRANSFORMS_FROM_STACK', + threadIndex, + firstRemovedFilterIndex, + }); }; } diff --git a/src/actions/receive-profile.js b/src/actions/receive-profile.js index 9e1e47b99a..3dde95ba2b 100644 --- a/src/actions/receive-profile.js +++ b/src/actions/receive-profile.js @@ -77,10 +77,16 @@ export function coalescedFunctionsUpdate( }; } -const requestIdleCallbackPolyfill: typeof requestIdleCallback = - typeof window === 'object' && window.requestIdleCallback - ? window.requestIdleCallback - : callback => setTimeout(callback, 0); +let requestIdleCallbackPolyfill: ( + callback: () => void, + _opts?: { timeout: number } +) => mixed; + +if (typeof window === 'object' && window.requestIdleCallback) { + requestIdleCallbackPolyfill = window.requestIdleCallback; +} else { + requestIdleCallbackPolyfill = callback => setTimeout(callback, 0); +} class ColascedFunctionsUpdateDispatcher { _updates: FunctionsUpdatePerThread; diff --git a/src/components/app/Home.js b/src/components/app/Home.js index 3de8b7b15a..09ca1371ff 100644 --- a/src/components/app/Home.js +++ b/src/components/app/Home.js @@ -5,13 +5,18 @@ // @flow import * as React from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import classNames from 'classnames'; import AddonScreenshot from '../../../res/gecko-profiler-screenshot-2018-01-18.png'; import PerfScreenshot from '../../../res/perf-screenshot-2017-09-08.jpg'; import { retrieveProfileFromFile } from '../../actions/receive-profile'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import FooterLinks from './FooterLinks'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + require('./Home.css'); const ADDON_URL = @@ -46,7 +51,7 @@ class InstallButton extends React.PureComponent { } type UploadButtonProps = { - retrieveProfileFromFile: File => void, + retrieveProfileFromFile: typeof retrieveProfileFromFile, }; class UploadButton extends React.PureComponent { @@ -96,10 +101,15 @@ window.geckoProfilerAddonInstalled = function() { } }; -type HomeProps = { - specialMessage?: string, - retrieveProfileFromFile: File => void, -}; +type OwnHomeProps = {| + +specialMessage?: string, +|}; + +type DispatchHomeProps = {| + +retrieveProfileFromFile: typeof retrieveProfileFromFile, +|}; + +type HomeProps = ConnectedProps; type HomeState = { isDragging: boolean, @@ -392,4 +402,8 @@ function _isFirefox(): boolean { return Boolean(navigator.userAgent.match(/Firefox\/\d+\.\d+/)); } -export default connect(state => state, { retrieveProfileFromFile })(Home); +const options: ExplicitConnectOptions = { + mapDispatchToProps: { retrieveProfileFromFile }, + component: Home, +}; +export default explicitConnect(options); diff --git a/src/components/app/Initializing.js b/src/components/app/Initializing.js index 1365dc8b01..1a088b9181 100644 --- a/src/components/app/Initializing.js +++ b/src/components/app/Initializing.js @@ -3,50 +3,31 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @flow -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import React from 'react'; -type Props = { - className: string, - profilerUrl: string, -}; +const PROFILER_URL = 'https://github.com/devtools-html/Gecko-Profiler-Addon'; -class Initializing extends PureComponent { - render() { - const { className, profilerUrl } = this.props; - - return ( -
-
-

Waiting on Gecko Profiler to provide a profile.

-

- Make sure Firefox is running the{' '} - new version of the Gecko Profiler add-on. - You can control the profiler with the following two shortcuts. -

-
    -
  • - Ctrl+Shift+5: Stop / - Restart profiling -
  • -
  • - Ctrl+Shift+6: Capture the - profile and open up this interface. -
  • -
-
+export default function Initializing() { + return ( +
+
+

Waiting on Gecko Profiler to provide a profile.

+

+ Make sure Firefox is running the{' '} + new version of the Gecko Profiler add-on. + You can control the profiler with the following two shortcuts. +

+
    +
  • + Ctrl+Shift+5: Stop / Restart + profiling +
  • +
  • + Ctrl+Shift+6: Capture the + profile and open up this interface. +
  • +
- ); - } +
+ ); } - -Initializing.propTypes = { - className: PropTypes.string.isRequired, - profilerUrl: PropTypes.string.isRequired, -}; - -export default connect(() => ({ - className: 'initializing', - profilerUrl: 'https://github.com/devtools-html/Gecko-Profiler-Addon', -}))(Initializing); diff --git a/src/components/app/ProfileFilterNavigator.js b/src/components/app/ProfileFilterNavigator.js index 55edbb2e43..70b33ea0a5 100644 --- a/src/components/app/ProfileFilterNavigator.js +++ b/src/components/app/ProfileFilterNavigator.js @@ -4,15 +4,22 @@ // @flow -import { connect } from 'react-redux'; -import actions from '../../actions'; +import explicitConnect from '../../utils/connect'; +import { popRangeFiltersAndUnsetSelection } from '../../actions/profile-view'; import { getRangeFilterLabels } from '../../reducers/url-state'; import FilterNavigatorBar from '../calltree/FilterNavigatorBar'; -import type { State } from '../../types/reducers'; +import type { ExplicitConnectOptions } from '../../utils/connect'; +import type { ElementProps } from 'react'; -export default connect( - (state: State) => { +type Props = ElementProps; +type DispatchProps = {| + +onPop: $PropertyType, +|}; +type StateProps = $ReadOnly<$Exact<$Diff>>; + +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => { const items = getRangeFilterLabels(state); return { className: 'profileFilterNavigator', @@ -20,7 +27,10 @@ export default connect( selectedItem: items.length - 1, }; }, - { - onPop: actions.popRangeFiltersAndUnsetSelection, - } -)(FilterNavigatorBar); + mapDispatchToProps: { + onPop: popRangeFiltersAndUnsetSelection, + }, + component: FilterNavigatorBar, +}; + +export default explicitConnect(options); diff --git a/src/components/app/ProfileSharing.js b/src/components/app/ProfileSharing.js index 3dd8ba8bb3..851eaa4dba 100644 --- a/src/components/app/ProfileSharing.js +++ b/src/components/app/ProfileSharing.js @@ -5,7 +5,7 @@ // @flow import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import classNames from 'classnames'; import { getProfile, @@ -29,6 +29,10 @@ import type { StartEndRange } from '../../types/units'; import type { Profile } from '../../types/profile'; import type { Action, DataSource } from '../../types/actions'; import type { SymbolicationStatus } from '../../types/reducers'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; require('./ProfileSharing.css'); @@ -438,14 +442,23 @@ class ProfileDownloadButton extends PureComponent< } } -type ProfileSharingProps = { - profile: Profile, - rootRange: StartEndRange, - dataSource: DataSource, - symbolicationStatus: SymbolicationStatus, - profilePublished: typeof actions.profilePublished, - predictUrl: (Action | Action[]) => string, -}; +type ProfileSharingStateProps = {| + +profile: Profile, + +rootRange: StartEndRange, + +dataSource: DataSource, + +symbolicationStatus: SymbolicationStatus, + +predictUrl: (Action | Action[]) => string, +|}; + +type ProfileSharingDispatchProps = {| + +profilePublished: typeof actions.profilePublished, +|}; + +type ProfileSharingProps = ConnectedProps< + {||}, + ProfileSharingStateProps, + ProfileSharingDispatchProps +>; const ProfileSharing = ({ profile, @@ -466,13 +479,19 @@ const ProfileSharing = ({
; -export default connect( - state => ({ +const options: ExplicitConnectOptions< + {||}, + ProfileSharingStateProps, + ProfileSharingDispatchProps +> = { + mapStateToProps: state => ({ profile: getProfile(state), rootRange: getProfileRootRange(state), dataSource: getDataSource(state), symbolicationStatus: getProfileViewOptions(state).symbolicationStatus, predictUrl: getUrlPredictor(state), }), - { profilePublished: actions.profilePublished } -)(ProfileSharing); + mapDispatchToProps: { profilePublished: actions.profilePublished }, + component: ProfileSharing, +}; +export default explicitConnect(options); diff --git a/src/components/app/ProfileViewer.js b/src/components/app/ProfileViewer.js index 8fc0363b27..a45f4a97a2 100644 --- a/src/components/app/ProfileViewer.js +++ b/src/components/app/ProfileViewer.js @@ -6,7 +6,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import TabBar from './TabBar'; import ProfileCallTreeView from '../calltree/ProfileCallTreeView'; import MarkerTable from '../marker-table'; @@ -16,7 +16,7 @@ import ProfileSharing from './ProfileSharing'; import SymbolicationStatusOverlay from './SymbolicationStatusOverlay'; import StackChart from '../stack-chart/'; import MarkerChart from '../marker-chart/'; -import actions from '../../actions'; +import { changeSelectedTab, changeTabOrder } from '../../actions/app'; import { getProfileViewOptions, getDisplayRange, @@ -27,21 +27,30 @@ import ProfileCallTreeContextMenu from '../calltree/ProfileCallTreeContextMenu'; import MarkerTableContextMenu from '../marker-table/ContextMenu'; import ProfileThreadHeaderContextMenu from '../header/ProfileThreadHeaderContextMenu'; import FooterLinks from './FooterLinks'; +import { toValidTabSlug } from '../../utils/flow'; import type { StartEndRange } from '../../types/units'; import type { Tab } from './TabBar'; -import type { Action } from '../../types/actions'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; require('./ProfileViewer.css'); -type Props = { - className: string, - tabOrder: number[], - timeRange: StartEndRange, - selectedTab: string, - changeSelectedTab: string => void, - changeTabOrder: (number[]) => Action, -}; +type StateProps = {| + +tabOrder: number[], + +selectedTab: string, + +className: string, + +timeRange: StartEndRange, +|}; + +type DispatchProps = {| + +changeSelectedTab: typeof changeSelectedTab, + +changeTabOrder: typeof changeTabOrder, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class ProfileViewer extends PureComponent { _tabs: Tab[]; @@ -72,7 +81,11 @@ class ProfileViewer extends PureComponent { _onSelectTab(selectedTab: string) { const { changeSelectedTab } = this.props; - changeSelectedTab(selectedTab); + const tabSlug = toValidTabSlug(selectedTab); + if (!tabSlug) { + throw new Error('Attempted to change to a tab that does not exist.'); + } + changeSelectedTab(tabSlug); } render() { @@ -131,12 +144,18 @@ ProfileViewer.propTypes = { changeTabOrder: PropTypes.func.isRequired, }; -export default connect( - state => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ tabOrder: getProfileViewOptions(state).tabOrder, selectedTab: getSelectedTab(state), className: 'profileViewer', timeRange: getDisplayRange(state), }), - actions -)(ProfileViewer); + mapDispatchToProps: { + changeSelectedTab, + changeTabOrder, + }, + component: ProfileViewer, +}; + +export default explicitConnect(options); diff --git a/src/components/app/Root.js b/src/components/app/Root.js index 9673aae453..38e66b9fd7 100644 --- a/src/components/app/Root.js +++ b/src/components/app/Root.js @@ -4,7 +4,8 @@ // @flow import React, { PureComponent } from 'react'; -import { connect, Provider } from 'react-redux'; +import { Provider } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { retrieveProfileFromAddon, @@ -13,7 +14,6 @@ import { } from '../../actions/receive-profile'; import ProfileViewer from './ProfileViewer'; import Home from './Home'; -import { urlFromState, stateFromLocation } from '../../url-handling'; import { getView } from '../../reducers/app'; import { getDataSource, @@ -26,6 +26,10 @@ import FooterLinks from './FooterLinks'; import type { Store } from '../../types/store'; import type { AppViewState, State } from '../../types/reducers'; import type { DataSource } from '../../types/actions'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; require('./Root.css'); @@ -66,16 +70,24 @@ function toParagraphs(str: string) { ); }); } - -type ProfileViewProps = { - view: AppViewState, - dataSource: DataSource, - hash: string, - profileUrl: string, - retrieveProfileFromAddon: typeof retrieveProfileFromAddon, - retrieveProfileFromStore: typeof retrieveProfileFromStore, - retrieveProfileFromUrl: typeof retrieveProfileFromUrl, -}; +type ProfileViewStateProps = {| + +view: AppViewState, + +dataSource: DataSource, + +hash: string, + +profileUrl: string, +|}; + +type ProfileViewDispatchProps = {| + +retrieveProfileFromAddon: typeof retrieveProfileFromAddon, + +retrieveProfileFromStore: typeof retrieveProfileFromStore, + +retrieveProfileFromUrl: typeof retrieveProfileFromUrl, +|}; + +type ProfileViewProps = ConnectedProps< + {||}, + ProfileViewStateProps, + ProfileViewDispatchProps +>; class ProfileViewWhenReadyImpl extends PureComponent { componentDidMount() { @@ -199,15 +211,25 @@ class ProfileViewWhenReadyImpl extends PureComponent { } } -const ProfileViewWhenReady = connect( - (state: State) => ({ +const options: ExplicitConnectOptions< + {||}, + ProfileViewStateProps, + ProfileViewDispatchProps +> = { + mapStateToProps: (state: State) => ({ view: getView(state), dataSource: getDataSource(state), hash: getHash(state), profileUrl: getProfileUrl(state), }), - { retrieveProfileFromStore, retrieveProfileFromUrl, retrieveProfileFromAddon } -)(ProfileViewWhenReadyImpl); + mapDispatchToProps: { + retrieveProfileFromStore, + retrieveProfileFromUrl, + retrieveProfileFromAddon, + }, + component: ProfileViewWhenReadyImpl, +}; +const ProfileViewWhenReady = explicitConnect(options); type RootProps = { store: Store, @@ -218,10 +240,7 @@ export default class Root extends PureComponent { const { store } = this.props; return ( - + diff --git a/src/components/app/SymbolicationStatusOverlay.js b/src/components/app/SymbolicationStatusOverlay.js index 7922a38d5e..88c5da5cd9 100644 --- a/src/components/app/SymbolicationStatusOverlay.js +++ b/src/components/app/SymbolicationStatusOverlay.js @@ -5,9 +5,14 @@ // @flow import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; import { getProfileViewOptions } from '../../reducers/profile-view'; +import explicitConnect from '../../utils/connect'; + import type { RequestedLib } from '../../types/reducers'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; function englishSgPlLibrary(count) { return count === 1 ? 'library' : 'libraries'; @@ -26,10 +31,12 @@ function englishListJoin(list) { } } -type Props = { - symbolicationStatus: string, - waitingForLibs: Set, -}; +type StateProps = {| + +symbolicationStatus: string, + +waitingForLibs: Set, +|}; + +type Props = ConnectedProps<{||}, StateProps, {||}>; class SymbolicationStatusOverlay extends PureComponent { render() { @@ -59,7 +66,11 @@ class SymbolicationStatusOverlay extends PureComponent { } } -export default connect(state => ({ - symbolicationStatus: getProfileViewOptions(state).symbolicationStatus, - waitingForLibs: getProfileViewOptions(state).waitingForLibs, -}))(SymbolicationStatusOverlay); +const options: ExplicitConnectOptions<{||}, StateProps, {||}> = { + mapStateToProps: state => ({ + symbolicationStatus: getProfileViewOptions(state).symbolicationStatus, + waitingForLibs: getProfileViewOptions(state).waitingForLibs, + }), + component: SymbolicationStatusOverlay, +}; +export default explicitConnect(options); diff --git a/src/components/app/UrlManager.js b/src/components/app/UrlManager.js index 05c9968f0f..0623352a88 100644 --- a/src/components/app/UrlManager.js +++ b/src/components/app/UrlManager.js @@ -4,26 +4,39 @@ // @flow -import React, { PureComponent } from 'react'; +import * as React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { getIsUrlSetupDone } from '../../reducers/app'; import { updateUrlState, urlSetupDone, show404 } from '../../actions/app'; +import { urlFromState, stateFromLocation } from '../../url-handling'; -type Props = { - stateFromLocation: Location => any, - urlFromState: any => string, - children: any, - urlState: any, - isUrlSetupDone: boolean, - updateUrlState: string => void, - urlSetupDone: void => void, - show404: string => void, -}; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; +import type { UrlState } from '../../types/reducers'; + +type StateProps = {| + +urlState: UrlState, + +isUrlSetupDone: boolean, +|}; + +type DispatchProps = {| + +updateUrlState: typeof updateUrlState, + +urlSetupDone: typeof urlSetupDone, + +show404: typeof show404, +|}; + +type OwnProps = {| + +children: React.Node, +|}; + +type Props = ConnectedProps; -class UrlManager extends PureComponent { +class UrlManager extends React.PureComponent { _updateState() { - const { updateUrlState, stateFromLocation, show404 } = this.props; + const { updateUrlState, show404 } = this.props; if (window.history.state) { updateUrlState(window.history.state); } else { @@ -43,7 +56,7 @@ class UrlManager extends PureComponent { } componentWillReceiveProps(nextProps: Props) { - const { urlFromState, isUrlSetupDone } = this.props; + const { isUrlSetupDone } = this.props; const newUrl = urlFromState(nextProps.urlState); if (newUrl !== window.location.pathname + window.location.search) { if (isUrlSetupDone) { @@ -63,8 +76,6 @@ class UrlManager extends PureComponent { } UrlManager.propTypes = { - stateFromLocation: PropTypes.func.isRequired, - urlFromState: PropTypes.func.isRequired, children: PropTypes.any.isRequired, urlState: PropTypes.object.isRequired, isUrlSetupDone: PropTypes.bool.isRequired, @@ -73,14 +84,17 @@ UrlManager.propTypes = { show404: PropTypes.func.isRequired, }; -export default connect( - state => ({ +const options: ExplicitConnectOptions = { + mapStateToProps: state => ({ urlState: state.urlState, isUrlSetupDone: getIsUrlSetupDone(state), }), - { + mapDispatchToProps: { updateUrlState, urlSetupDone, show404, - } -)(UrlManager); + }, + component: UrlManager, +}; + +export default explicitConnect(options); diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index bef2bbd43f..9586d29e43 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -5,7 +5,7 @@ // @flow import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import TreeView from '../shared/TreeView'; import NodeIcon from './NodeIcon'; import { getCallNodePath } from '../../profile-logic/profile-data'; @@ -38,25 +38,34 @@ import type { CallNodeDisplayData, } from '../../types/profile-derived'; import type { Column } from '../shared/TreeView'; - -type Props = { - threadIndex: ThreadIndex, - scrollToSelectionGeneration: number, - focusCallTreeGeneration: number, - tree: CallTree, - callNodeInfo: CallNodeInfo, - selectedCallNodeIndex: IndexIntoCallNodeTable | null, - expandedCallNodeIndexes: Array, - searchStringsRegExp: RegExp, - disableOverscan: boolean, - callNodeMaxDepth: number, - implementationFilter: ImplementationFilter, - invertCallstack: boolean, - icons: IconWithClassName[], - changeSelectedCallNode: typeof changeSelectedCallNode, - changeExpandedCallNodes: typeof changeExpandedCallNodes, - addTransformToStack: typeof addTransformToStack, -}; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + +type StateProps = {| + +threadIndex: ThreadIndex, + +scrollToSelectionGeneration: number, + +focusCallTreeGeneration: number, + +tree: CallTree, + +callNodeInfo: CallNodeInfo, + +selectedCallNodeIndex: IndexIntoCallNodeTable | null, + +expandedCallNodeIndexes: Array, + +searchStringsRegExp: RegExp | null, + +disableOverscan: boolean, + +invertCallstack: boolean, + +implementationFilter: ImplementationFilter, + +icons: IconWithClassName[], + +callNodeMaxDepth: number, +|}; + +type DispatchProps = {| + +changeSelectedCallNode: typeof changeSelectedCallNode, + +changeExpandedCallNodes: typeof changeExpandedCallNodes, + +addTransformToStack: typeof addTransformToStack, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class CallTreeComponent extends PureComponent { _fixedColumns: Column[]; @@ -209,8 +218,8 @@ class CallTreeComponent extends PureComponent { } } -export default connect( - (state: State) => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: (state: State) => ({ threadIndex: getSelectedThreadIndex(state), scrollToSelectionGeneration: getScrollToSelectionGeneration(state), focusCallTreeGeneration: getFocusCallTreeGeneration(state), @@ -229,11 +238,13 @@ export default connect( icons: getIconsWithClassNames(state), callNodeMaxDepth: selectedThreadSelectors.getCallNodeMaxDepth(state), }), - { + mapDispatchToProps: { changeSelectedCallNode, changeExpandedCallNodes, addTransformToStack, }, - null, - { withRef: true } -)(CallTreeComponent); + options: { withRef: true }, + component: CallTreeComponent, +}; + +export default explicitConnect(options); diff --git a/src/components/calltree/FilterNavigatorBar.js b/src/components/calltree/FilterNavigatorBar.js index 3b311eb472..51a35b782d 100644 --- a/src/components/calltree/FilterNavigatorBar.js +++ b/src/components/calltree/FilterNavigatorBar.js @@ -10,12 +10,12 @@ import classNames from 'classnames'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import './FilterNavigatorBar.css'; -type Props = { - className: string, - items: string[], - onPop: number => *, - selectedItem: number, -}; +type Props = {| + +className: string, + +items: string[], + +onPop: number => *, + +selectedItem: number, +|}; class FilterNavigatorBar extends PureComponent { constructor(props: Props) { diff --git a/src/components/calltree/NodeIcon.js b/src/components/calltree/NodeIcon.js index 8ae74163f9..729aebd48b 100644 --- a/src/components/calltree/NodeIcon.js +++ b/src/components/calltree/NodeIcon.js @@ -6,19 +6,33 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - +import explicitConnect from '../../utils/connect'; import { getIconClassNameForCallNode } from '../../reducers/icons'; -import actions from '../../actions'; +import { iconStartLoading } from '../../actions/icons'; -type Props = { - className: string, - icon: string, - iconStartLoading: string => void, -}; +import type { CallNodeDisplayData } from '../../types/profile-derived'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + +type OwnProps = {| + +displayData: CallNodeDisplayData, +|}; + +type StateProps = {| + +className: string, + +icon: string | null, +|}; + +type DispatchProps = {| + +iconStartLoading: typeof iconStartLoading, +|}; + +type Props = ConnectedProps; class NodeIcon extends PureComponent { - constructor(props) { + constructor(props: Props) { super(props); if (props.icon) { props.iconStartLoading(props.icon); @@ -32,7 +46,7 @@ class NodeIcon extends PureComponent { } render() { - return
; + return
; } } @@ -42,10 +56,12 @@ NodeIcon.propTypes = { iconStartLoading: PropTypes.func.isRequired, }; -export default connect( - (state, { displayData }) => ({ +const options: ExplicitConnectOptions = { + mapStateToProps: (state, { displayData }) => ({ className: getIconClassNameForCallNode(state, displayData), icon: displayData.icon, }), - actions -)(NodeIcon); + mapDispatchToProps: { iconStartLoading }, + component: NodeIcon, +}; +export default explicitConnect(options); diff --git a/src/components/calltree/ProfileCallTreeContextMenu.js b/src/components/calltree/ProfileCallTreeContextMenu.js index 8648488048..675a5b025b 100644 --- a/src/components/calltree/ProfileCallTreeContextMenu.js +++ b/src/components/calltree/ProfileCallTreeContextMenu.js @@ -5,7 +5,7 @@ // @flow import React, { PureComponent } from 'react'; import { ContextMenu, MenuItem } from 'react-contextmenu'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { selectedThreadSelectors } from '../../reducers/profile-view'; import { funcHasRecursiveCall } from '../../profile-logic/transforms'; import { getFunctionName } from '../../profile-logic/function-info'; @@ -24,17 +24,26 @@ import type { CallNodePath, } from '../../types/profile-derived'; import type { Thread, ThreadIndex } from '../../types/profile'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; -type Props = { - thread: Thread, - threadIndex: ThreadIndex, - callNodeInfo: CallNodeInfo, - implementation: ImplementationFilter, - selectedCallNodePath: CallNodePath, - selectedCallNodeIndex: IndexIntoCallNodeTable, - inverted: boolean, - addTransformToStack: typeof addTransformToStack, -}; +type StateProps = {| + +thread: Thread, + +threadIndex: ThreadIndex, + +callNodeInfo: CallNodeInfo, + +implementation: ImplementationFilter, + +inverted: boolean, + +selectedCallNodePath: CallNodePath, + +selectedCallNodeIndex: IndexIntoCallNodeTable | null, +|}; + +type DispatchProps = {| + +addTransformToStack: typeof addTransformToStack, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; require('./ProfileCallTreeContextMenu.css'); @@ -51,6 +60,12 @@ class ProfileCallTreeContextMenu extends PureComponent { callNodeInfo: { callNodeTable }, } = this.props; + if (selectedCallNodeIndex === null) { + throw new Error( + "The context menu assumes there is a selected call node and there wasn't one." + ); + } + const funcIndex = callNodeTable.func[selectedCallNodeIndex]; const isJS = funcTable.isJS[funcIndex]; const stringIndex = funcTable.name[funcIndex]; @@ -66,6 +81,12 @@ class ProfileCallTreeContextMenu extends PureComponent { callNodeInfo: { callNodeTable }, } = this.props; + if (selectedCallNodeIndex === null) { + throw new Error( + "The context menu assumes there is a selected call node and there wasn't one." + ); + } + const funcIndex = callNodeTable.func[selectedCallNodeIndex]; const stringIndex = funcTable.fileName[funcIndex]; if (stringIndex !== null) { @@ -81,6 +102,12 @@ class ProfileCallTreeContextMenu extends PureComponent { callNodeInfo: { callNodeTable }, } = this.props; + if (selectedCallNodeIndex === null) { + throw new Error( + "The context menu assumes there is a selected call node and there wasn't one." + ); + } + let stack = ''; let callNodeIndex = selectedCallNodeIndex; @@ -246,6 +273,13 @@ class ProfileCallTreeContextMenu extends PureComponent { thread: { funcTable }, callNodeInfo: { callNodeTable }, } = this.props; + + if (selectedCallNodeIndex === null) { + throw new Error( + "The context menu assumes there is a selected call node and there wasn't one." + ); + } + const funcIndex = callNodeTable.func[selectedCallNodeIndex]; const isJS = funcTable.isJS[funcIndex]; // This could be the C++ library, or the JS filename. @@ -321,8 +355,8 @@ class ProfileCallTreeContextMenu extends PureComponent { } } -export default connect( - state => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ thread: selectedThreadSelectors.getFilteredThread(state), threadIndex: getSelectedThreadIndex(state), callNodeInfo: selectedThreadSelectors.getCallNodeInfo(state), @@ -335,5 +369,7 @@ export default connect( state ), }), - { addTransformToStack } -)(ProfileCallTreeContextMenu); + mapDispatchToProps: { addTransformToStack }, + component: ProfileCallTreeContextMenu, +}; +export default explicitConnect(options); diff --git a/src/components/calltree/TransformNavigator.js b/src/components/calltree/TransformNavigator.js index 4ba3344aec..b296d0fbd8 100644 --- a/src/components/calltree/TransformNavigator.js +++ b/src/components/calltree/TransformNavigator.js @@ -4,30 +4,34 @@ // @flow -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { selectedThreadSelectors } from '../../reducers/profile-view'; -import { getSelectedThreadIndex } from '../../reducers/url-state'; import FilterNavigatorBar from './FilterNavigatorBar'; -import type { State } from '../../types/reducers'; import { popTransformsFromStack } from '../../actions/profile-view'; +import type { State } from '../../types/reducers'; +import type { ExplicitConnectOptions } from '../../utils/connect'; +import type { ElementProps } from 'react'; + import './TransformNavigator.css'; -export default connect( - (state: State) => { +type Props = ElementProps; +type DispatchProps = {| + +onPop: $PropertyType, +|}; +type StateProps = $Diff; + +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: (state: State) => { const items = selectedThreadSelectors.getTransformLabels(state); return { className: 'calltreeTransformNavigator', items, selectedItem: items.length - 1, - threadIndex: getSelectedThreadIndex(state), }; }, - { popTransformsFromStack }, - (stateProps, dispatchProps) => ({ - className: stateProps.className, - items: stateProps.items, - selectedItem: stateProps.selectedItem, - onPop: i => dispatchProps.popTransformsFromStack(stateProps.threadIndex, i), - }) -)(FilterNavigatorBar); + mapDispatchToProps: { onPop: popTransformsFromStack }, + component: FilterNavigatorBar, +}; + +export default explicitConnect(options); diff --git a/src/components/header/EmptyThreadIndicator.js b/src/components/header/EmptyThreadIndicator.js index bd4fe27828..b74ef9b72a 100644 --- a/src/components/header/EmptyThreadIndicator.js +++ b/src/components/header/EmptyThreadIndicator.js @@ -10,6 +10,7 @@ import { oneLine } from 'common-tags'; import type { Thread } from '../../types/profile'; import type { Milliseconds, StartEndRange } from '../../types/units'; +import type { SizeProps } from '../shared/WithSize'; import './EmptyThreadIndicator.css'; @@ -20,10 +21,10 @@ type SyntheticCssDeclarations = { type Props = {| +rangeStart: Milliseconds, +rangeEnd: Milliseconds, - +width: number, +thread: Thread, +interval: Milliseconds, +unfilteredSamplesRange: StartEndRange | null, + ...SizeProps, |}; class EmptyThreadIndicator extends PureComponent { diff --git a/src/components/header/IntervalMarkerOverview.js b/src/components/header/IntervalMarkerOverview.js index 77ed98cdbc..89df8c21fb 100644 --- a/src/components/header/IntervalMarkerOverview.js +++ b/src/components/header/IntervalMarkerOverview.js @@ -13,26 +13,33 @@ import MarkerTooltipContents from '../shared/MarkerTooltipContents'; import type { Milliseconds, CssPixels } from '../../types/units'; import type { TracingMarker } from '../../types/profile-derived'; import type { SizeProps } from '../shared/WithSize'; +import type { ConnectedProps } from '../../utils/connect'; type MarkerState = 'PRESSED' | 'HOVERED' | 'NONE'; -type Props = SizeProps & {| - className: string, - rangeStart: Milliseconds, - rangeEnd: Milliseconds, - intervalMarkers: TracingMarker[], - threadIndex: number, - threadName: string, - onSelect: any, - styles: any, - isSelected: boolean, - isModifyingSelection: boolean, - overlayFills: { - HOVERED: string, - PRESSED: string, +// Typically this component is wrapped in a connect function, but in other files. +export type OwnProps = {| + +className: string, + +rangeStart: Milliseconds, + +rangeEnd: Milliseconds, + +threadIndex: number, + +onSelect: any, + +isModifyingSelection: boolean, +|}; + +export type StateProps = {| + +intervalMarkers: TracingMarker[], + +isSelected: boolean, + +threadName: string, + +styles: any, + +overlayFills: { + +HOVERED: string, + +PRESSED: string, }, |}; +type Props = ConnectedProps; + type State = { hoveredItem: TracingMarker | null, mouseDownItem: TracingMarker | null, diff --git a/src/components/header/ProfileThreadHeaderBar.js b/src/components/header/ProfileThreadHeaderBar.js index 303499896a..e9ee5a533e 100644 --- a/src/components/header/ProfileThreadHeaderBar.js +++ b/src/components/header/ProfileThreadHeaderBar.js @@ -5,7 +5,7 @@ // @flow import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import ThreadStackGraph from './ThreadStackGraph'; import { selectorsForThread } from '../../reducers/profile-view'; import { getSelectedThreadIndex } from '../../reducers/url-state'; @@ -32,28 +32,39 @@ import type { IndexIntoCallNodeTable, } from '../../types/profile-derived'; import type { State } from '../../types/reducers'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; -type Props = { +type OwnProps = {| +threadIndex: ThreadIndex, - +thread: Thread, - +callNodeInfo: CallNodeInfo, +interval: Milliseconds, +rangeStart: Milliseconds, +rangeEnd: Milliseconds, - +selectedCallNodeIndex: IndexIntoCallNodeTable, - +isSelected: boolean, +isHidden: boolean, +isModifyingSelection: boolean, - +style: Object, +|}; + +type StateProps = {| + +thread: Thread, +threadName: string, +processDetails: string, + +callNodeInfo: CallNodeInfo, + +selectedCallNodeIndex: IndexIntoCallNodeTable | null, + +isSelected: boolean, + +unfilteredSamplesRange: StartEndRange | null, +|}; + +type DispatchProps = {| +changeSelectedThread: typeof changeSelectedThread, +changeRightClickedThread: typeof changeRightClickedThread, +updateProfileSelection: typeof updateProfileSelection, +changeSelectedCallNode: typeof changeSelectedCallNode, +focusCallTree: typeof focusCallTree, - +unfilteredSamplesRange: StartEndRange | null, -}; +|}; + +type Props = ConnectedProps; class ProfileThreadHeaderBar extends PureComponent { constructor(props) { @@ -144,7 +155,6 @@ class ProfileThreadHeaderBar extends PureComponent { callNodeInfo, selectedCallNodeIndex, isSelected, - style, threadName, processDetails, isHidden, @@ -171,7 +181,6 @@ class ProfileThreadHeaderBar extends PureComponent {
  • { } } -export default connect( - (state: State, props) => { - const threadIndex: ThreadIndex = props.index; +const options: ExplicitConnectOptions = { + mapStateToProps: (state: State, ownProps: OwnProps) => { + const { threadIndex } = ownProps; const selectors = selectorsForThread(threadIndex); const selectedThread = getSelectedThreadIndex(state); return { @@ -245,15 +254,16 @@ export default connect( ? selectors.getSelectedCallNodeIndex(state) : -1, isSelected: threadIndex === selectedThread, - threadIndex, unfilteredSamplesRange: selectors.unfilteredSamplesRange(state), }; }, - { + mapDispatchToProps: { changeSelectedThread, updateProfileSelection, changeRightClickedThread, changeSelectedCallNode, focusCallTree, - } -)(ProfileThreadHeaderBar); + }, + component: ProfileThreadHeaderBar, +}; +export default explicitConnect(options); diff --git a/src/components/header/ProfileThreadHeaderContextMenu.js b/src/components/header/ProfileThreadHeaderContextMenu.js index 96d02aa34a..badbc216a0 100644 --- a/src/components/header/ProfileThreadHeaderContextMenu.js +++ b/src/components/header/ProfileThreadHeaderContextMenu.js @@ -10,7 +10,7 @@ import { showThread, isolateThread, } from '../../actions/profile-view'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { getThreads, getRightClickedThreadIndex, @@ -21,17 +21,26 @@ import classNames from 'classnames'; import type { Thread, ThreadIndex } from '../../types/profile'; import type { State } from '../../types/reducers'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; -type Props = {| - threads: Thread[], - threadOrder: ThreadIndex[], - hiddenThreads: ThreadIndex[], - rightClickedThreadIndex: ThreadIndex, - hideThread: typeof hideThread, - showThread: typeof showThread, - isolateThread: typeof isolateThread, +type StateProps = {| + +threads: Thread[], + +threadOrder: ThreadIndex[], + +hiddenThreads: ThreadIndex[], + +rightClickedThreadIndex: ThreadIndex, |}; +type DispatchProps = {| + +hideThread: typeof hideThread, + +showThread: typeof showThread, + +isolateThread: typeof isolateThread, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + class ProfileThreadHeaderContextMenu extends PureComponent { constructor(props: Props) { super(props); @@ -105,12 +114,14 @@ class ProfileThreadHeaderContextMenu extends PureComponent { } } -export default connect( - (state: State) => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: (state: State) => ({ threads: getThreads(state), threadOrder: getThreadOrder(state), hiddenThreads: getHiddenThreads(state), rightClickedThreadIndex: getRightClickedThreadIndex(state), }), - { hideThread, showThread, isolateThread } -)(ProfileThreadHeaderContextMenu); + mapDispatchToProps: { hideThread, showThread, isolateThread }, + component: ProfileThreadHeaderContextMenu, +}; +export default explicitConnect(options); diff --git a/src/components/header/ProfileThreadJankOverview.js b/src/components/header/ProfileThreadJankOverview.js index 314d84c831..ebc58c0b0c 100644 --- a/src/components/header/ProfileThreadJankOverview.js +++ b/src/components/header/ProfileThreadJankOverview.js @@ -4,7 +4,7 @@ // @flow -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import IntervalMarkerOverview from './IntervalMarkerOverview'; import { selectorsForThread } from '../../reducers/profile-view'; import { @@ -13,16 +13,23 @@ import { } from '../../profile-logic/interval-marker-styles'; import { getSelectedThreadIndex } from '../../reducers/url-state'; -export default connect((state, props) => { - const { threadIndex } = props; - const selectors = selectorsForThread(threadIndex); - const threadName = selectors.getFriendlyThreadName(state); - const selectedThread = getSelectedThreadIndex(state); - return { - intervalMarkers: selectors.getJankInstances(state), - isSelected: threadIndex === selectedThread, - threadName, - styles, - overlayFills, - }; -})(IntervalMarkerOverview); +import type { ExplicitConnectOptions } from '../../utils/connect'; +import type { StateProps, OwnProps } from './IntervalMarkerOverview'; + +const options: ExplicitConnectOptions = { + mapStateToProps: (state, props) => { + const { threadIndex } = props; + const selectors = selectorsForThread(threadIndex); + const threadName = selectors.getFriendlyThreadName(state); + const selectedThread = getSelectedThreadIndex(state); + return { + intervalMarkers: selectors.getJankInstances(state), + isSelected: threadIndex === selectedThread, + threadName, + styles, + overlayFills, + }; + }, + component: IntervalMarkerOverview, +}; +export default explicitConnect(options); diff --git a/src/components/header/ProfileThreadTracingMarkerOverview.js b/src/components/header/ProfileThreadTracingMarkerOverview.js index c4526455c7..8a11d385be 100644 --- a/src/components/header/ProfileThreadTracingMarkerOverview.js +++ b/src/components/header/ProfileThreadTracingMarkerOverview.js @@ -4,7 +4,7 @@ // @flow -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import IntervalMarkerOverview from './IntervalMarkerOverview'; import { selectorsForThread } from '../../reducers/profile-view'; import { @@ -13,18 +13,25 @@ import { } from '../../profile-logic/interval-marker-styles'; import { getSelectedThreadIndex } from '../../reducers/url-state'; -export default connect((state, props) => { - const { threadIndex } = props; - const selectors = selectorsForThread(threadIndex); - const selectedThread = getSelectedThreadIndex(state); - const intervalMarkers = selectors.getRangeSelectionFilteredTracingMarkersForHeader( - state - ); - return { - intervalMarkers, - threadName: selectors.getFriendlyThreadName(state), - isSelected: threadIndex === selectedThread, - styles, - overlayFills, - }; -})(IntervalMarkerOverview); +import type { ExplicitConnectOptions } from '../../utils/connect'; +import type { StateProps, OwnProps } from './IntervalMarkerOverview'; + +const options: ExplicitConnectOptions = { + mapStateToProps: (state, props) => { + const { threadIndex } = props; + const selectors = selectorsForThread(threadIndex); + const selectedThread = getSelectedThreadIndex(state); + const intervalMarkers = selectors.getRangeSelectionFilteredTracingMarkersForHeader( + state + ); + return { + intervalMarkers, + threadName: selectors.getFriendlyThreadName(state), + isSelected: threadIndex === selectedThread, + styles, + overlayFills, + }; + }, + component: IntervalMarkerOverview, +}; +export default explicitConnect(options); diff --git a/src/components/header/ProfileViewerHeader.js b/src/components/header/ProfileViewerHeader.js index 7642c1e995..33c8bc9256 100644 --- a/src/components/header/ProfileViewerHeader.js +++ b/src/components/header/ProfileViewerHeader.js @@ -9,7 +9,7 @@ import ProfileThreadHeaderBar from './ProfileThreadHeaderBar'; import Reorderable from '../shared/Reorderable'; import TimeSelectionScrubber from './TimeSelectionScrubber'; import OverflowEdgeIndicator from './OverflowEdgeIndicator'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { getProfile, getProfileViewOptions, @@ -26,22 +26,29 @@ import { import type { Profile, ThreadIndex } from '../../types/profile'; import type { ProfileSelection } from '../../types/actions'; -import type { State } from '../../types/reducers'; import type { Milliseconds, StartEndRange } from '../../types/units'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; -type Props = {| +type StateProps = {| +profile: Profile, - +className: string, - +hiddenThreads: ThreadIndex[], - +threadOrder: ThreadIndex[], +selection: ProfileSelection, + +threadOrder: ThreadIndex[], + +hiddenThreads: ThreadIndex[], +timeRange: StartEndRange, +zeroAt: Milliseconds, +|}; + +type DispatchProps = {| +changeThreadOrder: typeof changeThreadOrder, +addRangeFilterAndUnsetSelection: typeof addRangeFilterAndUnsetSelection, +updateProfileSelection: typeof updateProfileSelection, |}; +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + class ProfileViewerHeader extends PureComponent { constructor(props: Props) { super(props); @@ -56,7 +63,6 @@ class ProfileViewerHeader extends PureComponent { render() { const { profile, - className, threadOrder, changeThreadOrder, selection, @@ -69,7 +75,7 @@ class ProfileViewerHeader extends PureComponent { return ( { onSelectionChange={updateProfileSelection} onZoomButtonClick={this._onZoomButtonClick} > - + { { {threads.map((thread, threadIndex) => { } } -export default connect( - (state: State) => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ profile: getProfile(state), selection: getProfileViewOptions(state).selection, - className: 'profileViewer', threadOrder: getThreadOrder(state), hiddenThreads: getHiddenThreads(state), timeRange: getDisplayRange(state), zeroAt: getZeroAt(state), }), - { + mapDispatchToProps: { changeThreadOrder, updateProfileSelection, addRangeFilterAndUnsetSelection, - } -)(ProfileViewerHeader); + }, + component: ProfileViewerHeader, +}; + +export default explicitConnect(options); diff --git a/src/components/header/ThreadStackGraph.js b/src/components/header/ThreadStackGraph.js index dc5b549340..45041ad4ea 100644 --- a/src/components/header/ThreadStackGraph.js +++ b/src/components/header/ThreadStackGraph.js @@ -23,7 +23,7 @@ type Props = {| +rangeStart: Milliseconds, +rangeEnd: Milliseconds, +callNodeInfo: CallNodeInfo, - +selectedCallNodeIndex: IndexIntoCallNodeTable, + +selectedCallNodeIndex: IndexIntoCallNodeTable | null, +className: string, +onStackClick: (time: Milliseconds) => void, |}; diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index 78a6fcbc04..a9a33ef3b1 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -4,10 +4,14 @@ // @flow import * as React from 'react'; -import withChartViewport from '../shared/chart/Viewport'; +import { + withChartViewport, + type WithChartViewport, +} from '../shared/chart/Viewport'; import ChartCanvas from '../shared/chart/Canvas'; import MarkerTooltipContents from '../shared/MarkerTooltipContents'; import TextMeasurement from '../../utils/text-measurement'; +import { updateProfileSelection } from '../../actions/profile-view'; import { BLUE_40 } from '../../utils/colors'; import type { @@ -20,7 +24,7 @@ import type { MarkerTimingRows, IndexIntoMarkerTiming, } from '../../types/profile-derived'; -import type { Action, ProfileSelection } from '../../types/actions'; +import type { Viewport } from '../shared/chart/Viewport'; type MarkerDrawingInformation = { x: CssPixels, @@ -30,25 +34,24 @@ type MarkerDrawingInformation = { text: string, }; -type Props = { - rangeStart: Milliseconds, - rangeEnd: Milliseconds, - containerWidth: CssPixels, - containerHeight: CssPixels, - viewportLeft: UnitIntervalOfProfileRange, - viewportRight: UnitIntervalOfProfileRange, - viewportTop: CssPixels, - viewportBottom: CssPixels, - markerTimingRows: MarkerTimingRows, - rowHeight: CssPixels, - markers: TracingMarker[], - updateProfileSelection: ProfileSelection => Action, - isDragging: boolean, -}; - -type State = { +type OwnProps = {| + +rangeStart: Milliseconds, + +rangeEnd: Milliseconds, + +markerTimingRows: MarkerTimingRows, + +rowHeight: CssPixels, + +markers: TracingMarker[], + +updateProfileSelection: typeof updateProfileSelection, +|}; + +type Props = {| + ...OwnProps, + // Bring in the viewport props from the higher order Viewport component. + +viewport: Viewport, +|}; + +type State = {| hoveredItem: null | number, -}; +|}; const TEXT_OFFSET_TOP = 11; const TWO_PI = Math.PI * 2; @@ -72,12 +75,14 @@ class MarkerChartCanvas extends React.PureComponent { hoveredItem: IndexIntoMarkerTiming | null ) { const { - viewportTop, - viewportBottom, rowHeight, - containerWidth, - containerHeight, markerTimingRows, + viewport: { + viewportTop, + viewportBottom, + containerWidth, + containerHeight, + }, } = this.props; // Convert CssPixels to Stack Depth const startRow = Math.floor(viewportTop / rowHeight); @@ -153,12 +158,9 @@ class MarkerChartCanvas extends React.PureComponent { const { rangeStart, rangeEnd, - containerWidth, markerTimingRows, rowHeight, - viewportLeft, - viewportRight, - viewportTop, + viewport: { containerWidth, viewportLeft, viewportRight, viewportTop }, } = this.props; const rangeLength: Milliseconds = rangeEnd - rangeStart; @@ -236,8 +238,7 @@ class MarkerChartCanvas extends React.PureComponent { const { markerTimingRows, rowHeight, - viewportTop, - containerWidth, + viewport: { viewportTop, containerWidth }, } = this.props; // Draw separators @@ -283,11 +284,8 @@ class MarkerChartCanvas extends React.PureComponent { rangeStart, rangeEnd, markerTimingRows, - viewportLeft, - viewportRight, - viewportTop, - containerWidth, rowHeight, + viewport: { viewportLeft, viewportRight, viewportTop, containerWidth }, } = this.props; const rangeLength: Milliseconds = rangeEnd - rangeStart; @@ -358,7 +356,7 @@ class MarkerChartCanvas extends React.PureComponent { } render() { - const { containerWidth, containerHeight, isDragging } = this.props; + const { containerWidth, containerHeight, isDragging } = this.props.viewport; return ( { } } -export default withChartViewport(MarkerChartCanvas); +export default (withChartViewport: WithChartViewport)( + MarkerChartCanvas +); diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index a5fcf0b68a..b282ed33a3 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -4,7 +4,7 @@ // @flow import * as React from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import MarkerChartCanvas from './Canvas'; import { selectedThreadSelectors, @@ -12,6 +12,7 @@ import { getProfileInterval, getProfileViewOptions, } from '../../reducers/profile-view'; +import { getSelectedThreadIndex } from '../../reducers/url-state'; import { updateProfileSelection } from '../../actions/profile-view'; import type { @@ -23,25 +24,32 @@ import type { UnitIntervalOfProfileRange, } from '../../types/units'; import type { ProfileSelection } from '../../types/actions'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; require('./index.css'); const ROW_HEIGHT = 16; -type Props = { - isRowExpanded: boolean, - maxMarkerRows: number, - isSelected: boolean, - timeRange: { start: Milliseconds, end: Milliseconds }, - threadIndex: number, - interval: Milliseconds, - updateProfileSelection: typeof updateProfileSelection, - selection: ProfileSelection, - threadName: string, - processDetails: string, - markerTimingRows: MarkerTimingRows, - markers: TracingMarker[], -}; +type DispatchProps = {| + +updateProfileSelection: typeof updateProfileSelection, +|}; + +type StateProps = {| + +markers: TracingMarker[], + +markerTimingRows: MarkerTimingRows, + +maxMarkerRows: number, + +timeRange: { start: Milliseconds, end: Milliseconds }, + +interval: Milliseconds, + +threadIndex: number, + +selection: ProfileSelection, + +threadName: string, + +processDetails: string, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class MarkerChart extends React.PureComponent { /** @@ -54,17 +62,15 @@ class MarkerChart extends React.PureComponent { render() { const { - isRowExpanded, maxMarkerRows, - isSelected, timeRange, threadIndex, markerTimingRows, markers, - updateProfileSelection, selection, threadName, processDetails, + updateProfileSelection, } = this.props; // The viewport needs to know about the height of what it's drawing, calculate @@ -80,35 +86,37 @@ class MarkerChart extends React.PureComponent {
  • ); } } -function viewportNeedsUpdate(prevProps, newProps) { +// This function is given the MarkerChartCanvas's chartProps. +function viewportNeedsUpdate( + prevProps: { +markerTimingRows: MarkerTimingRows }, + newProps: { +markerTimingRows: MarkerTimingRows } +) { return prevProps.markerTimingRows !== newProps.markerTimingRows; } -export default connect( - (state, ownProps) => { - const { threadIndex } = ownProps; +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => { const markers = selectedThreadSelectors.getTracingMarkers(state); const markerTimingRows = selectedThreadSelectors.getMarkerTiming(state); @@ -118,11 +126,13 @@ export default connect( maxMarkerRows: markerTimingRows.length, timeRange: getDisplayRange(state), interval: getProfileInterval(state), - threadIndex, + threadIndex: getSelectedThreadIndex(state), selection: getProfileViewOptions(state).selection, threadName: selectedThreadSelectors.getFriendlyThreadName(state), processDetails: selectedThreadSelectors.getThreadProcessDetails(state), }; }, - { updateProfileSelection } -)(MarkerChart); + mapDispatchToProps: { updateProfileSelection }, + component: MarkerChart, +}; +export default explicitConnect(options); diff --git a/src/components/marker-table/ContextMenu.js b/src/components/marker-table/ContextMenu.js index 6c7f421e70..9e3e25ef42 100644 --- a/src/components/marker-table/ContextMenu.js +++ b/src/components/marker-table/ContextMenu.js @@ -5,7 +5,7 @@ // @flow import React, { PureComponent } from 'react'; import { ContextMenu, MenuItem } from 'react-contextmenu'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { updateProfileSelection } from '../../actions/profile-view'; import { selectedThreadSelectors, @@ -21,15 +21,24 @@ import type { MarkersTable, } from '../../types/profile'; import type { ProfileSelection } from '../../types/actions'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; -type Props = { - thread: Thread, - selectedMarker: IndexIntoMarkersTable, - markers: MarkersTable, - updateProfileSelection: typeof updateProfileSelection, - displayRange: StartEndRange, - selection: ProfileSelection, -}; +type StateProps = {| + +thread: Thread, + +markers: MarkersTable, + +selection: ProfileSelection, + +displayRange: StartEndRange, + +selectedMarker: IndexIntoMarkersTable, +|}; + +type DispatchProps = {| + +updateProfileSelection: typeof updateProfileSelection, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class MarkersContextMenu extends PureComponent { constructor(props: Props) { @@ -129,8 +138,8 @@ class MarkersContextMenu extends PureComponent { } } -export default connect( - state => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ thread: selectedThreadSelectors.getThread(state), markers: selectedThreadSelectors.getSearchFilteredMarkers(state), selection: getProfileViewOptions(state).selection, @@ -138,5 +147,7 @@ export default connect( selectedMarker: selectedThreadSelectors.getViewOptions(state) .selectedMarker, }), - { updateProfileSelection } -)(MarkersContextMenu); + mapDispatchToProps: { updateProfileSelection }, + component: MarkersContextMenu, +}; +export default explicitConnect(options); diff --git a/src/components/marker-table/Settings.js b/src/components/marker-table/Settings.js index da4310a880..b4394b13d0 100644 --- a/src/components/marker-table/Settings.js +++ b/src/components/marker-table/Settings.js @@ -5,17 +5,27 @@ // @flow import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { changeMarkersSearchString } from '../../actions/profile-view'; import { getMarkersSearchString } from '../../reducers/url-state'; import IdleSearchField from '../shared/IdleSearchField'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + import './Settings.css'; -type Props = { - searchString: string, - changeMarkersSearchString: string => void, -}; +type StateProps = {| + +searchString: string, +|}; + +type DispatchProps = {| + +changeMarkersSearchString: typeof changeMarkersSearchString, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class Settings extends PureComponent { constructor(props: Props) { @@ -50,9 +60,11 @@ class Settings extends PureComponent { } } -export default connect( - state => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ searchString: getMarkersSearchString(state), }), - { changeMarkersSearchString } -)(Settings); + mapDispatchToProps: { changeMarkersSearchString }, + component: Settings, +}; +export default explicitConnect(options); diff --git a/src/components/marker-table/index.js b/src/components/marker-table/index.js index 5a92ab1499..b2ebc82647 100644 --- a/src/components/marker-table/index.js +++ b/src/components/marker-table/index.js @@ -6,7 +6,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import TreeView from '../shared/TreeView'; import { getZeroAt, @@ -26,6 +26,10 @@ import type { IndexIntoMarkersTable, } from '../../types/profile'; import type { Milliseconds } from '../../types/units'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; type MarkerDisplayData = {| timestamp: string, @@ -131,15 +135,20 @@ class MarkerTree { } } -type Props = {| +type StateProps = {| + +threadIndex: ThreadIndex, +thread: Thread, +markers: MarkersTable, - +threadIndex: ThreadIndex, +selectedMarker: IndexIntoMarkersTable, +zeroAt: Milliseconds, +|}; + +type DispatchProps = {| +changeSelectedMarker: typeof changeSelectedMarker, |}; +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + class MarkerTable extends PureComponent { _fixedColumns = [ { propName: 'timestamp', title: 'Time Stamp' }, @@ -199,8 +208,8 @@ MarkerTable.propTypes = { changeSelectedMarker: PropTypes.func.isRequired, }; -export default connect( - state => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ threadIndex: getSelectedThreadIndex(state), thread: selectedThreadSelectors.getRangeSelectionFilteredThread(state), markers: selectedThreadSelectors.getSearchFilteredMarkers(state), @@ -208,5 +217,7 @@ export default connect( .selectedMarker, zeroAt: getZeroAt(state), }), - { changeSelectedMarker } -)(MarkerTable); + mapDispatchToProps: { changeSelectedMarker }, + component: MarkerTable, +}; +export default explicitConnect(options); diff --git a/src/components/shared/IdleSearchField.js b/src/components/shared/IdleSearchField.js index ad1c136d27..0f557fdf00 100644 --- a/src/components/shared/IdleSearchField.js +++ b/src/components/shared/IdleSearchField.js @@ -23,38 +23,32 @@ type State = { }; class IdleSearchField extends PureComponent { - _timeout: number; + _timeout: TimeoutID | null = null; _previouslyNotifiedValue: string; constructor(props: Props) { super(props); - (this: any)._onSearchFieldChange = this._onSearchFieldChange.bind(this); - (this: any)._onSearchFieldFocus = this._onSearchFieldFocus.bind(this); - (this: any)._onSearchFieldBlur = this._onSearchFieldBlur.bind(this); - (this: any)._onClearButtonClick = this._onClearButtonClick.bind(this); - (this: any)._onTimeout = this._onTimeout.bind(this); - this._timeout = 0; this.state = { value: props.defaultValue || '', }; this._previouslyNotifiedValue = this.state.value; } - _onSearchFieldFocus(e: SyntheticFocusEvent) { + _onSearchFieldFocus = (e: SyntheticFocusEvent) => { e.currentTarget.select(); if (this.props.onFocus) { this.props.onFocus(); } - } + }; - _onSearchFieldBlur(e: { relatedTarget: Element | null }) { + _onSearchFieldBlur = (e: { relatedTarget: Element | null }) => { if (this.props.onBlur) { this.props.onBlur(e.relatedTarget); } - } + }; - _onSearchFieldChange(e: SyntheticEvent) { + _onSearchFieldChange = (e: SyntheticEvent) => { this.setState({ value: e.currentTarget.value, }); @@ -63,12 +57,12 @@ class IdleSearchField extends PureComponent { clearTimeout(this._timeout); } this._timeout = setTimeout(this._onTimeout, this.props.idlePeriod); - } + }; - _onTimeout() { - this._timeout = 0; + _onTimeout = () => { + this._timeout = null; this._notifyIfChanged(this.state.value); - } + }; _notifyIfChanged(value: string) { if (value !== this._previouslyNotifiedValue) { @@ -77,13 +71,15 @@ class IdleSearchField extends PureComponent { } } - _onClearButtonClick() { - clearTimeout(this._timeout); - this._timeout = 0; + _onClearButtonClick = () => { + if (this._timeout !== null) { + clearTimeout(this._timeout); + this._timeout = null; + } this.setState({ value: '' }); this._notifyIfChanged(''); - } + }; _onClearButtonFocus( e: SyntheticEvent & { relatedTarget: HTMLElement } diff --git a/src/components/shared/StackSearchField.js b/src/components/shared/StackSearchField.js index 2ef56d58f3..5a4ac53a76 100644 --- a/src/components/shared/StackSearchField.js +++ b/src/components/shared/StackSearchField.js @@ -4,9 +4,8 @@ // @flow import * as React from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import classNames from 'classnames'; - import IdleSearchField from './IdleSearchField'; import { changeCallTreeSearchString } from '../../actions/profile-view'; import { @@ -14,14 +13,28 @@ import { getSearchStrings, } from '../../reducers/url-state'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + import './StackSearchField.css'; -type Props = {| - +className?: string, +type OwnProps = {| + +className: string, +|}; + +type StateProps = {| +currentSearchString: string, +searchStrings: string[] | null, +|}; + +type DispatchProps = {| +changeCallTreeSearchString: typeof changeCallTreeSearchString, |}; + +type Props = ConnectedProps; + type State = {| searchFieldFocused: boolean |}; class StackSearchField extends React.PureComponent { @@ -84,10 +97,12 @@ class StackSearchField extends React.PureComponent { } } -export default connect( - state => ({ +const options: ExplicitConnectOptions = { + mapStateToProps: state => ({ currentSearchString: getCurrentSearchString(state), searchStrings: getSearchStrings(state), }), - { changeCallTreeSearchString } -)(StackSearchField); + mapDispatchToProps: { changeCallTreeSearchString }, + component: StackSearchField, +}; +export default explicitConnect(options); diff --git a/src/components/shared/Tooltip.js b/src/components/shared/Tooltip.js index 792aaa6265..248ad2c711 100644 --- a/src/components/shared/Tooltip.js +++ b/src/components/shared/Tooltip.js @@ -124,11 +124,16 @@ export default class Tooltip extends React.PureComponent { top: mouseY - offsetY, }; + const mountElement = this._mountElement; + if (!mountElement) { + throw new Error('There should have been a mount element.'); + } + ReactDOM.render(
    {children}
    , - this._mountElement + mountElement ); } diff --git a/src/components/shared/WithSize.js b/src/components/shared/WithSize.js index 793ef5f785..1fe3f7f8a3 100644 --- a/src/components/shared/WithSize.js +++ b/src/components/shared/WithSize.js @@ -7,11 +7,13 @@ import * as React from 'react'; import { findDOMNode } from 'react-dom'; import type { CssPixels } from '../../types/units'; -export type SizeProps = {| +type State = {| width: CssPixels, height: CssPixels, |}; +export type SizeProps = $ReadOnly; + /** * Wraps a React component and makes 'width' and 'height' available in the * wrapped component's props. These props start out at zero and are updated to @@ -22,16 +24,22 @@ export type SizeProps = {| * Note that the props are *not* updated if the size of the element changes * for reasons other than a window resize. */ -export function withSize( - Wrapped: React.ComponentType<{ ...WrappedProps, ...SizeProps }> -): React.ComponentType<{ ...WrappedProps }> { - return class WithSizeWrapper extends React.PureComponent<*, SizeProps> { +export function withSize< + // The SizeProps act as a bounds on the generic props. This ensures that the props + // that passed in take into account they are being given the width and height. + Props: $ReadOnly<{ ...SizeProps }> +>( + Wrapped: React.ComponentType +): React.ComponentType< + // The component that is returned does not accept width and height parameters, as + // they are injected by this higher order component. + $ReadOnly<$Diff> +> { + return class WithSizeWrapper extends React.PureComponent<*, State> { _resizeListener: Event => void; state = { width: 0, height: 0 }; - _observeSize = ( - wrappedComponent: React.Component<{ ...WrappedProps, ...SizeProps }> - ) => { + _observeSize = (wrappedComponent: React.Component | null) => { if (!wrappedComponent) { return; } diff --git a/src/components/shared/chart/Viewport.js b/src/components/shared/chart/Viewport.js index f098a64ae3..02bfa9732d 100644 --- a/src/components/shared/chart/Viewport.js +++ b/src/components/shared/chart/Viewport.js @@ -5,61 +5,21 @@ // @flow import * as React from 'react'; import classNames from 'classnames'; -import { connect } from 'react-redux'; +import explicitConnect from '../../../utils/connect'; import { getHasZoomedViaMousewheel } from '../../../reducers/app'; import { setHasZoomedViaMousewheel } from '../../../actions/stack-chart'; +import { updateProfileSelection } from '../../../actions/profile-view'; import type { CssPixels, UnitIntervalOfProfileRange, StartEndRange, } from '../../../types/units'; -import typeof { updateProfileSelection as UpdateProfileSelection } from '../../../actions/profile-view'; import type { ProfileSelection } from '../../../types/actions'; - -const { DOM_DELTA_PAGE, DOM_DELTA_LINE } = - typeof window === 'object' && window.WheelEvent - ? new WheelEvent('mouse') - : { DOM_DELTA_LINE: 1, DOM_DELTA_PAGE: 2 }; - -// These are the props consumed by this Higher-Order Component (HOC) -type ViewportProps = { - viewportNeedsUpdate: (ViewportProps, ViewportProps) => boolean, - timeRange: StartEndRange, - maxViewportHeight: number, - maximumZoom: UnitIntervalOfProfileRange, - updateProfileSelection: UpdateProfileSelection, - selection: ProfileSelection, - setHasZoomedViaMousewheel: () => void, - hasZoomedViaMousewheel: boolean, -}; - -// These are the props injected by the HOC to WrappedComponent -type InjectedProps = { - containerWidth: CssPixels, - containerHeight: CssPixels, - viewportLeft: UnitIntervalOfProfileRange, - viewportRight: UnitIntervalOfProfileRange, - viewportTop: CssPixels, - viewportBottom: CssPixels, - isDragging: boolean, -}; - -type State = { - containerWidth: CssPixels, - containerHeight: CssPixels, - containerLeft: CssPixels, - viewportTop: CssPixels, - viewportBottom: CssPixels, - viewportLeft: UnitIntervalOfProfileRange, - viewportRight: UnitIntervalOfProfileRange, - dragX: CssPixels, - dragY: CssPixels, - isDragging: boolean, - isShiftScrollHintVisible: boolean, -}; - -require('./Viewport.css'); +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../../utils/connect'; /** * Viewport terminology: @@ -88,422 +48,523 @@ require('./Viewport.css'); * viewportRight += mouseMoveDelta * unitPixel * viewportLeft += mouseMoveDelta * unitPixel **/ + +const { DOM_DELTA_PAGE, DOM_DELTA_LINE } = + typeof window === 'object' && window.WheelEvent + ? new WheelEvent('mouse') + : { DOM_DELTA_LINE: 1, DOM_DELTA_PAGE: 2 }; + +// These viewport values are computed dynamically by the HOC, and then passed into +// the props of the wrapped component. +export type Viewport = {| + +containerWidth: CssPixels, + +containerHeight: CssPixels, + +viewportLeft: UnitIntervalOfProfileRange, + +viewportRight: UnitIntervalOfProfileRange, + +viewportTop: CssPixels, + +viewportBottom: CssPixels, + +isDragging: boolean, +|}; + +type ViewportStateProps = {| + +hasZoomedViaMousewheel?: boolean, +|}; + +type ViewportDispatchProps = {| + +updateProfileSelection: typeof updateProfileSelection, + +setHasZoomedViaMousewheel?: typeof setHasZoomedViaMousewheel, +|}; + +// These are the props consumed by this Higher-Order Component (HOC), but can be +// optionally used by the wrapped component. +type ViewportOwnProps = {| + +viewportProps: {| + +timeRange: StartEndRange, + +maxViewportHeight: number, + +maximumZoom: UnitIntervalOfProfileRange, + +selection: ProfileSelection, + // These props are defined by the generic variables passed into to the type + // WithChartViewport when calling withChartViewport. This is how the relationship + // is guaranteed. e.g. here with OwnProps: + // + // (withChartViewport: WithChartViewport)( + // MarkerChartCanvas + // ) + +viewportNeedsUpdate: ( + prevProps: ChartProps, + nextProps: ChartProps + ) => boolean, + |}, + +chartProps: ChartProps, +|}; + +type State = {| + containerWidth: CssPixels, + containerHeight: CssPixels, + containerLeft: CssPixels, + viewportTop: CssPixels, + viewportBottom: CssPixels, + viewportLeft: UnitIntervalOfProfileRange, + viewportRight: UnitIntervalOfProfileRange, + dragX: CssPixels, + dragY: CssPixels, + isDragging: boolean, + isShiftScrollHintVisible: boolean, +|}; + +require('./Viewport.css'); + /** - * About the Flow typing: - * - * - `Props` are the props for the returned component. It means that they are the - * props that the user for this component will need to specify. From the - * generic definition ``, they must include the - * properties consumed par this HOC. - * - * - The argument, which is the augmented component (the `WrappedComponent`), - * needs to accept both the `InjectedProps` and some supertype of `Props`. - * A supertype of `Props` is an object will some properties of - * `Props` but not all of them. - * To understand what this means to Flow, we need, like sometimes, to think - * backwards: from the props of `WrappedComponent`, take out `InjectedProps`, - * and make it part of `Props`. - * - * So `Props` will need to hold both `ViewportProps` as said earlier and the - * props from `WrappedComponent` that aren't `Injectedprops`. - * This is exactly what we want Flow to check: that the user of this HOC - * properly passes all these props! + * This is the type signature for the higher order component. It's easier to use generics + * by separating out the type definition. */ -export default function withChartViewport( - WrappedComponent: React.ComponentType> -): React.ComponentType { - class ChartViewport extends React.PureComponent { - shiftScrollId: number; - zoomRangeSelectionScheduled: boolean; - zoomRangeSelectionScrollDelta: number; - _container: HTMLElement | null; - _takeContainerRef = container => (this._container = container); - - constructor(props: Props) { - super(props); - (this: any)._mouseWheelListener = this._mouseWheelListener.bind(this); - (this: any)._mouseDownListener = this._mouseDownListener.bind(this); - (this: any)._mouseMoveListener = this._mouseMoveListener.bind(this); - (this: any)._mouseUpListener = this._mouseUpListener.bind(this); - - (this: any)._setSize = this._setSize.bind(this); - (this: any)._setSizeNextFrame = this._setSizeNextFrame.bind(this); - - this.shiftScrollId = 0; - this.zoomRangeSelectionScheduled = false; - this.zoomRangeSelectionScrollDelta = 0; - this._container = null; - - this.state = this.getDefaultState(props); - } +export type WithChartViewport< + ChartOwnProps: Object, + // The chart component's props are given the viewport object, as well as the original + // ChartOwnProps. + ChartProps: $ReadOnly<{| + ...ChartOwnProps, + viewport: Viewport, + |}> +> = ( + // Take as input the component class that supports the the ViewportProps. The ChartProps + // also contain other things. + ChartComponent: React.ComponentType +) => React.ComponentType< + // Finally the returned component takes as input the InternalViewportProps, and + // the ChartProps, but NOT the ViewportProps. + ViewportOwnProps +>; + +// Create the implementation of the WithChartViewport type, but let flow infer the +// generic parameters. +export const withChartViewport: WithChartViewport<*, *> = + // ChartOwnProps is the only generic actually used in the implementation. Infer + // the type signature of the arguments as the WithChartViewport will apply them. + ( + ChartComponent: React.ComponentType<$Subtype<{ +viewport: Viewport }>> + ): * => { + type ViewportProps = ConnectedProps< + ViewportOwnProps, + ViewportStateProps, + ViewportDispatchProps + >; + + class ChartViewport extends React.PureComponent { + shiftScrollId: number; + zoomRangeSelectionScheduled: boolean; + zoomRangeSelectionScrollDelta: number; + _container: HTMLElement | null; + _takeContainerRef = container => (this._container = container); + + constructor(props: ViewportProps) { + super(props); + (this: any)._mouseWheelListener = this._mouseWheelListener.bind(this); + (this: any)._mouseDownListener = this._mouseDownListener.bind(this); + (this: any)._mouseMoveListener = this._mouseMoveListener.bind(this); + (this: any)._mouseUpListener = this._mouseUpListener.bind(this); + + (this: any)._setSize = this._setSize.bind(this); + (this: any)._setSizeNextFrame = this._setSizeNextFrame.bind(this); + + this.shiftScrollId = 0; + this.zoomRangeSelectionScheduled = false; + this.zoomRangeSelectionScrollDelta = 0; + this._container = null; + + this.state = this.getDefaultState(props); + } - getHorizontalViewport({ selection, timeRange }: ViewportProps) { - if (selection.hasSelection) { - const { selectionStart, selectionEnd } = selection; - const timeRangeLength = timeRange.end - timeRange.start; + getHorizontalViewport(props: ViewportProps) { + const { selection, timeRange } = props.viewportProps; + if (selection.hasSelection) { + const { selectionStart, selectionEnd } = selection; + const timeRangeLength = timeRange.end - timeRange.start; + return { + viewportLeft: (selectionStart - timeRange.start) / timeRangeLength, + viewportRight: (selectionEnd - timeRange.start) / timeRangeLength, + }; + } return { - viewportLeft: (selectionStart - timeRange.start) / timeRangeLength, - viewportRight: (selectionEnd - timeRange.start) / timeRangeLength, + viewportLeft: 0, + viewportRight: 1, }; } - return { - viewportLeft: 0, - viewportRight: 1, - }; - } - getDefaultState(props: ViewportProps) { - const { viewportLeft, viewportRight } = this.getHorizontalViewport(props); - return { - containerWidth: 0, - containerHeight: 0, - containerLeft: 0, - viewportTop: 0, - viewportBottom: 0, - viewportLeft, - viewportRight, - dragX: 0, - dragY: 0, - isDragging: false, - isShiftScrollHintVisible: false, - }; - } + getDefaultState(props: ViewportProps) { + const { viewportLeft, viewportRight } = this.getHorizontalViewport( + props + ); + return { + containerWidth: 0, + containerHeight: 0, + containerLeft: 0, + viewportTop: 0, + viewportBottom: 0, + viewportLeft, + viewportRight, + dragX: 0, + dragY: 0, + isDragging: false, + isShiftScrollHintVisible: false, + }; + } - /** + /** * Let the viewport know when we are actively scrolling. */ - showShiftScrollingHint() { - // Only show this message if we haven't shift zoomed yet. - if (this.props.hasZoomedViaMousewheel) { - return; - } - - const scollId = ++this.shiftScrollId; - if (!this.state.isShiftScrollHintVisible) { - this.setState({ isShiftScrollHintVisible: true }); - } - setTimeout(() => { - if (scollId === this.shiftScrollId) { - this.setState({ isShiftScrollHintVisible: false }); + showShiftScrollingHint() { + // Only show this message if we haven't shift zoomed yet. + if (this.props.viewportProps.hasZoomedViaMousewheel) { + return; } - }, 1000); - } - componentWillReceiveProps(newProps: Props) { - if (this.props.viewportNeedsUpdate(this.props, newProps)) { - this.setState(this.getDefaultState(newProps)); - this._setSizeNextFrame(); - } else if ( - this.props.selection !== newProps.selection || - this.props.timeRange !== newProps.timeRange - ) { - this.setState(this.getHorizontalViewport(newProps)); + const scollId = ++this.shiftScrollId; + if (!this.state.isShiftScrollHintVisible) { + this.setState({ isShiftScrollHintVisible: true }); + } + setTimeout(() => { + if (scollId === this.shiftScrollId) { + this.setState({ isShiftScrollHintVisible: false }); + } + }, 1000); } - } - _setSize() { - if (this._container) { - const rect = this._container.getBoundingClientRect(); + componentWillReceiveProps(newProps: ViewportProps) { if ( - this.state.containerWidth !== rect.width || - this.state.containerHeight !== rect.height + this.props.viewportProps.viewportNeedsUpdate( + this.props.chartProps, + newProps.chartProps + ) + ) { + this.setState(this.getDefaultState(newProps)); + this._setSizeNextFrame(); + } else if ( + this.props.viewportProps.selection !== + newProps.viewportProps.selection || + this.props.viewportPropstimeRange !== newProps.viewportProps.timeRange ) { - this.setState(prevState => ({ - containerWidth: rect.width, - containerHeight: rect.height, - containerLeft: rect.left, - viewportBottom: prevState.viewportTop + rect.height, - })); + this.setState(this.getHorizontalViewport(newProps)); } } - } - _setSizeNextFrame() { - requestAnimationFrame(this._setSize); - } + _setSize() { + if (this._container) { + const rect = this._container.getBoundingClientRect(); + if ( + this.state.containerWidth !== rect.width || + this.state.containerHeight !== rect.height + ) { + this.setState(prevState => ({ + containerWidth: rect.width, + containerHeight: rect.height, + containerLeft: rect.left, + viewportBottom: prevState.viewportTop + rect.height, + })); + } + } + } - _mouseWheelListener(event: SyntheticWheelEvent<>) { - if (event.shiftKey) { - this.zoomRangeSelection(event); - return; + _setSizeNextFrame() { + requestAnimationFrame(this._setSize); } - this.showShiftScrollingHint(); + _mouseWheelListener(event: SyntheticWheelEvent<>) { + if (event.shiftKey) { + this.zoomRangeSelection(event); + return; + } - // Do the work to move the viewport. - const { containerHeight } = this.state; + this.showShiftScrollingHint(); - this.moveViewport( - -getNormalizedScrollDelta(event, containerHeight, 'deltaX'), - -getNormalizedScrollDelta(event, containerHeight, 'deltaY') - ); - } + // Do the work to move the viewport. + const { containerHeight } = this.state; - zoomRangeSelection(event: SyntheticWheelEvent<>) { - if (!this.props.hasZoomedViaMousewheel) { - this.props.setHasZoomedViaMousewheel(); + this.moveViewport( + -getNormalizedScrollDelta(event, containerHeight, 'deltaX'), + -getNormalizedScrollDelta(event, containerHeight, 'deltaY') + ); } - event.preventDefault(); - - // Shift is a modifier that will change some mice to scroll horizontally, check - // for that here. - const deltaKey = event.deltaY === 0 ? 'deltaX' : 'deltaY'; - - // Accumulate the scroll delta here. Only apply it once per frame to avoid - // spamming the Redux store with updates. - this.zoomRangeSelectionScrollDelta += getNormalizedScrollDelta( - event, - this.state.containerHeight, - deltaKey - ); - - // See if an update needs to be scheduled. - if (!this.zoomRangeSelectionScheduled) { - const mouseX = event.clientX; - this.zoomRangeSelectionScheduled = true; - requestAnimationFrame(() => { - // Grab and reset the scroll delta accumulated up until this frame. - // Let another frame be scheduled. - const deltaY = this.zoomRangeSelectionScrollDelta; - this.zoomRangeSelectionScrollDelta = 0; - this.zoomRangeSelectionScheduled = false; - - const { maximumZoom } = this.props; - const { - containerLeft, - containerWidth, - viewportLeft, - viewportRight, - } = this.state; - const mouseCenter = (mouseX - containerLeft) / containerWidth; - - const viewportLength: CssPixels = viewportRight - viewportLeft; - const scale = viewportLength - viewportLength / (1 + deltaY * 0.001); - let newViewportLeft: UnitIntervalOfProfileRange = clamp( - 0, - 1, - viewportLeft - scale * mouseCenter - ); - let newViewportRight: UnitIntervalOfProfileRange = clamp( - 0, - 1, - viewportRight + scale * (1 - mouseCenter) - ); - - if (newViewportRight - newViewportLeft < maximumZoom) { - const newViewportMiddle = (viewportLeft + viewportRight) * 0.5; - newViewportLeft = newViewportMiddle - maximumZoom * 0.5; - newViewportRight = newViewportMiddle + maximumZoom * 0.5; - } - const { updateProfileSelection, timeRange } = this.props; - if (newViewportLeft === 0 && newViewportRight === 1) { - if (viewportLeft === 0 && viewportRight === 1) { - // Do not update if at the maximum bounds. - return; + zoomRangeSelection(event: SyntheticWheelEvent<>) { + const { + hasZoomedViaMousewheel, + setHasZoomedViaMousewheel, + } = this.props; + if (!hasZoomedViaMousewheel && setHasZoomedViaMousewheel) { + setHasZoomedViaMousewheel(); + } + event.preventDefault(); + + // Shift is a modifier that will change some mice to scroll horizontally, check + // for that here. + const deltaKey = event.deltaY === 0 ? 'deltaX' : 'deltaY'; + + // Accumulate the scroll delta here. Only apply it once per frame to avoid + // spamming the Redux store with updates. + this.zoomRangeSelectionScrollDelta += getNormalizedScrollDelta( + event, + this.state.containerHeight, + deltaKey + ); + + // See if an update needs to be scheduled. + if (!this.zoomRangeSelectionScheduled) { + const mouseX = event.clientX; + this.zoomRangeSelectionScheduled = true; + requestAnimationFrame(() => { + // Grab and reset the scroll delta accumulated up until this frame. + // Let another frame be scheduled. + const deltaY = this.zoomRangeSelectionScrollDelta; + this.zoomRangeSelectionScrollDelta = 0; + this.zoomRangeSelectionScheduled = false; + + const { maximumZoom } = this.props.viewportProps; + const { + containerLeft, + containerWidth, + viewportLeft, + viewportRight, + } = this.state; + const mouseCenter = (mouseX - containerLeft) / containerWidth; + + const viewportLength: CssPixels = viewportRight - viewportLeft; + const scale = + viewportLength - viewportLength / (1 + deltaY * 0.001); + let newViewportLeft: UnitIntervalOfProfileRange = clamp( + 0, + 1, + viewportLeft - scale * mouseCenter + ); + let newViewportRight: UnitIntervalOfProfileRange = clamp( + 0, + 1, + viewportRight + scale * (1 - mouseCenter) + ); + + if (newViewportRight - newViewportLeft < maximumZoom) { + const newViewportMiddle = (viewportLeft + viewportRight) * 0.5; + newViewportLeft = newViewportMiddle - maximumZoom * 0.5; + newViewportRight = newViewportMiddle + maximumZoom * 0.5; + } + + const { + updateProfileSelection, + viewportProps: { timeRange }, + } = this.props; + if (newViewportLeft === 0 && newViewportRight === 1) { + if (viewportLeft === 0 && viewportRight === 1) { + // Do not update if at the maximum bounds. + return; + } + updateProfileSelection({ + hasSelection: false, + isModifying: false, + }); + } else { + const timeRangeLength = timeRange.end - timeRange.start; + updateProfileSelection({ + hasSelection: true, + isModifying: false, + selectionStart: + timeRange.start + timeRangeLength * newViewportLeft, + selectionEnd: + timeRange.start + timeRangeLength * newViewportRight, + }); } - updateProfileSelection({ - hasSelection: false, - isModifying: false, - }); - } else { - const timeRangeLength = timeRange.end - timeRange.start; - updateProfileSelection({ - hasSelection: true, - isModifying: false, - selectionStart: - timeRange.start + timeRangeLength * newViewportLeft, - selectionEnd: - timeRange.start + timeRangeLength * newViewportRight, - }); - } - }); + }); + } } - } - _mouseDownListener(event: SyntheticMouseEvent<>) { - this.setState({ - dragX: event.clientX, - dragY: event.clientY, - isDragging: true, - }); - event.stopPropagation(); - event.preventDefault(); - - window.addEventListener('mousemove', this._mouseMoveListener, true); - window.addEventListener('mouseup', this._mouseUpListener, true); - } + _mouseDownListener(event: SyntheticMouseEvent<>) { + this.setState({ + dragX: event.clientX, + dragY: event.clientY, + isDragging: true, + }); + event.stopPropagation(); + event.preventDefault(); - _mouseMoveListener(event: MouseEvent) { - event.stopPropagation(); - event.preventDefault(); + window.addEventListener('mousemove', this._mouseMoveListener, true); + window.addEventListener('mouseup', this._mouseUpListener, true); + } - const { dragX, dragY } = this.state; - const offsetX = event.clientX - dragX; - const offsetY = event.clientY - dragY; + _mouseMoveListener(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); - this.setState({ - dragX: event.clientX, - dragY: event.clientY, - }); + const { dragX, dragY } = this.state; + const offsetX = event.clientX - dragX; + const offsetY = event.clientY - dragY; - this.moveViewport(offsetX, offsetY); - } + this.setState({ + dragX: event.clientX, + dragY: event.clientY, + }); - moveViewport(offsetX: CssPixels, offsetY: CssPixels): boolean { - const { - maxViewportHeight, - timeRange, - updateProfileSelection, - } = this.props; - const { - containerWidth, - containerHeight, - viewportTop, - viewportLeft, - viewportRight, - } = this.state; - - // Calculate left and right in terms of the unit interval of the profile range. - const viewportLength: CssPixels = viewportRight - viewportLeft; - const unitOffsetX: UnitIntervalOfProfileRange = - viewportLength * offsetX / containerWidth; - let newViewportLeft: CssPixels = viewportLeft - unitOffsetX; - let newViewportRight: CssPixels = viewportRight - unitOffsetX; - if (newViewportLeft < 0) { - newViewportLeft = 0; - newViewportRight = viewportLength; - } - if (newViewportRight > 1) { - newViewportLeft = 1 - viewportLength; - newViewportRight = 1; + this.moveViewport(offsetX, offsetY); } - // Calculate top and bottom in terms of pixels. - let newViewportTop: CssPixels = viewportTop - offsetY; - let newViewportBottom: CssPixels = newViewportTop + containerHeight; + moveViewport(offsetX: CssPixels, offsetY: CssPixels): boolean { + const { + updateProfileSelection, + viewportProps: { maxViewportHeight, timeRange }, + } = this.props; + const { + containerWidth, + containerHeight, + viewportTop, + viewportLeft, + viewportRight, + } = this.state; + + // Calculate left and right in terms of the unit interval of the profile range. + const viewportLength: CssPixels = viewportRight - viewportLeft; + const unitOffsetX: UnitIntervalOfProfileRange = + viewportLength * offsetX / containerWidth; + let newViewportLeft: CssPixels = viewportLeft - unitOffsetX; + let newViewportRight: CssPixels = viewportRight - unitOffsetX; + if (newViewportLeft < 0) { + newViewportLeft = 0; + newViewportRight = viewportLength; + } + if (newViewportRight > 1) { + newViewportLeft = 1 - viewportLength; + newViewportRight = 1; + } - // Constrain the viewport to the bottom. - if (newViewportBottom > maxViewportHeight) { - newViewportTop = maxViewportHeight - containerHeight; - newViewportBottom = maxViewportHeight; - } + // Calculate top and bottom in terms of pixels. + let newViewportTop: CssPixels = viewportTop - offsetY; + let newViewportBottom: CssPixels = newViewportTop + containerHeight; - // Constrain the viewport to the top. This must be after constraining to the bottom - // so if the view is extra small the content is anchored to the top, and not the bottom. - if (newViewportTop < 0) { - newViewportTop = 0; - newViewportBottom = containerHeight; - } + // Constrain the viewport to the bottom. + if (newViewportBottom > maxViewportHeight) { + newViewportTop = maxViewportHeight - containerHeight; + newViewportBottom = maxViewportHeight; + } - const timeRangeLength = timeRange.end - timeRange.start; - const viewportHorizontalChanged = newViewportLeft !== viewportLeft; - const viewportVerticalChanged = newViewportTop !== viewportTop; + // Constrain the viewport to the top. This must be after constraining to the bottom + // so if the view is extra small the content is anchored to the top, and not the bottom. + if (newViewportTop < 0) { + newViewportTop = 0; + newViewportBottom = containerHeight; + } - if (viewportHorizontalChanged) { - updateProfileSelection({ - hasSelection: true, - isModifying: false, - selectionStart: timeRange.start + timeRangeLength * newViewportLeft, - selectionEnd: timeRange.start + timeRangeLength * newViewportRight, - }); + const timeRangeLength = timeRange.end - timeRange.start; + const viewportHorizontalChanged = newViewportLeft !== viewportLeft; + const viewportVerticalChanged = newViewportTop !== viewportTop; + + if (viewportHorizontalChanged) { + updateProfileSelection({ + hasSelection: true, + isModifying: false, + selectionStart: timeRange.start + timeRangeLength * newViewportLeft, + selectionEnd: timeRange.start + timeRangeLength * newViewportRight, + }); + } + + if (viewportVerticalChanged) { + this.setState({ + viewportTop: newViewportTop, + viewportBottom: newViewportBottom, + }); + } + + return viewportVerticalChanged || viewportHorizontalChanged; } - if (viewportVerticalChanged) { + _mouseUpListener(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); + window.removeEventListener('mousemove', this._mouseMoveListener, true); + window.removeEventListener('mouseup', this._mouseUpListener, true); this.setState({ - viewportTop: newViewportTop, - viewportBottom: newViewportBottom, + isDragging: false, }); } - return viewportVerticalChanged || viewportHorizontalChanged; - } + componentDidMount() { + window.addEventListener('resize', this._setSizeNextFrame, false); + // The first _setSize ensures that the screen does not blip when mounting + // the component, while the second ensures that it lays out correctly if the DOM + // is not fully layed out correctly yet. + this._setSize(); + this._setSizeNextFrame(); + } - _mouseUpListener(event: MouseEvent) { - event.stopPropagation(); - event.preventDefault(); - window.removeEventListener('mousemove', this._mouseMoveListener, true); - window.removeEventListener('mouseup', this._mouseUpListener, true); - this.setState({ - isDragging: false, - }); - } + componentWillUnmount() { + window.removeEventListener('resize', this._setSizeNextFrame, false); + window.removeEventListener('mousemove', this._mouseMoveListener, true); + window.removeEventListener('mouseup', this._mouseUpListener, true); + } - componentDidMount() { - window.addEventListener('resize', this._setSizeNextFrame, false); - // The first _setSize ensures that the screen does not blip when mounting - // the component, while the second ensures that it lays out correctly if the DOM - // is not fully layed out correctly yet. - this._setSize(); - this._setSizeNextFrame(); - } + render() { + const { chartProps, hasZoomedViaMousewheel } = this.props; + + const { + containerWidth, + containerHeight, + viewportTop, + viewportBottom, + viewportLeft, + viewportRight, + isDragging, + isShiftScrollHintVisible, + } = this.state; + + const viewportClassName = classNames({ + chartViewport: true, + dragging: isDragging, + }); - componentWillUnmount() { - window.removeEventListener('resize', this._setSizeNextFrame, false); - window.removeEventListener('mousemove', this._mouseMoveListener, true); - window.removeEventListener('mouseup', this._mouseUpListener, true); - } + const shiftScrollClassName = classNames({ + chartViewportShiftScroll: true, + hidden: hasZoomedViaMousewheel || !isShiftScrollHintVisible, + }); + + const viewport: Viewport = { + containerWidth, + containerHeight, + viewportLeft, + viewportRight, + viewportTop, + viewportBottom, + isDragging, + }; - render() { - const { hasZoomedViaMousewheel } = this.props; - - const { - containerWidth, - containerHeight, - viewportTop, - viewportBottom, - viewportLeft, - viewportRight, - isDragging, - isShiftScrollHintVisible, - } = this.state; - - const viewportClassName = classNames({ - chartViewport: true, - dragging: isDragging, - }); - - const shiftScrollClassName = classNames({ - chartViewportShiftScroll: true, - hidden: hasZoomedViaMousewheel || !isShiftScrollHintVisible, - }); - - return ( -
    - -
    - Zoom Chart: - Shift - Scroll + return ( +
    + +
    + Zoom Chart: + Shift + Scroll +
    -
    - ); + ); + } } - } - // Connect this component so that it knows whether or not to nag the user to use shift - // for zooming on range selections. - return connect( - state => ({ - hasZoomedViaMousewheel: getHasZoomedViaMousewheel(state), - }), - { setHasZoomedViaMousewheel } - )(ChartViewport); -} + // Connect this component so that it knows whether or not to nag the user to use shift + // for zooming on range selections. + const options: ExplicitConnectOptions< + ViewportOwnProps, + ViewportStateProps, + ViewportDispatchProps + > = { + mapStateToProps: state => ({ + hasZoomedViaMousewheel: getHasZoomedViaMousewheel(state), + }), + mapDispatchToProps: { setHasZoomedViaMousewheel, updateProfileSelection }, + component: ChartViewport, + }; + return explicitConnect(options); + }; function clamp(min, max, value) { return Math.max(min, Math.min(max, value)); diff --git a/src/components/stack-chart/Canvas.js b/src/components/stack-chart/Canvas.js index 82d7164206..70e9667ab9 100644 --- a/src/components/stack-chart/Canvas.js +++ b/src/components/stack-chart/Canvas.js @@ -4,10 +4,14 @@ // @flow import * as React from 'react'; -import withChartViewport from '../shared/chart/Viewport'; +import { + withChartViewport, + type WithChartViewport, +} from '../shared/chart/Viewport'; import ChartCanvas from '../shared/chart/Canvas'; import TextMeasurement from '../../utils/text-measurement'; import { formatNumber } from '../../utils/format-numbers'; +import { updateProfileSelection } from '../../actions/profile-view'; import type { Thread } from '../../types/profile'; import type { @@ -22,32 +26,29 @@ import type { } from '../../profile-logic/stack-timing'; import type { GetCategory } from '../../profile-logic/color-categories'; import type { GetLabel } from '../../profile-logic/labeling-strategies'; -import type { Action, ProfileSelection } from '../../types/actions'; - -type Props = { - thread: Thread, - interval: Milliseconds, - rangeStart: Milliseconds, - rangeEnd: Milliseconds, - containerWidth: CssPixels, - containerHeight: CssPixels, - viewportLeft: UnitIntervalOfProfileRange, - viewportRight: UnitIntervalOfProfileRange, - viewportTop: CssPixels, - viewportBottom: CssPixels, - stackTimingByDepth: StackTimingByDepth, - stackFrameHeight: CssPixels, - getCategory: GetCategory, - getLabel: GetLabel, - updateProfileSelection: ProfileSelection => Action, - isDragging: boolean, - isRowExpanded: boolean, -}; - -type HoveredStackTiming = { - depth: StackTimingDepth, - stackTableIndex: IndexIntoStackTiming, -}; +import type { Viewport } from '../shared/chart/Viewport'; + +type OwnProps = {| + +thread: Thread, + +interval: Milliseconds, + +rangeStart: Milliseconds, + +rangeEnd: Milliseconds, + +stackTimingByDepth: StackTimingByDepth, + +stackFrameHeight: CssPixels, + +getCategory: GetCategory, + +getLabel: GetLabel, + +updateProfileSelection: typeof updateProfileSelection, +|}; + +type Props = $ReadOnly<{| + ...OwnProps, + +viewport: Viewport, +|}>; + +type HoveredStackTiming = {| + +depth: StackTimingDepth, + +stackTableIndex: IndexIntoStackTiming, +|}; require('./Canvas.css'); @@ -83,16 +84,18 @@ class StackChartCanvas extends React.PureComponent { thread, rangeStart, rangeEnd, - containerWidth, getLabel, - containerHeight, stackTimingByDepth, stackFrameHeight, getCategory, - viewportLeft, - viewportRight, - viewportTop, - viewportBottom, + viewport: { + containerWidth, + containerHeight, + viewportLeft, + viewportRight, + viewportTop, + viewportBottom, + }, } = this.props; // Ensure the text measurement tool is created, since this is the first time @@ -192,13 +195,7 @@ class StackChartCanvas extends React.PureComponent { depth, stackTableIndex, }: HoveredStackTiming): React.Node { - const { - thread, - getLabel, - getCategory, - stackTimingByDepth, - isRowExpanded, - } = this.props; + const { thread, getLabel, getCategory, stackTimingByDepth } = this.props; const stackTiming = stackTimingByDepth[depth]; const duration = @@ -211,36 +208,32 @@ class StackChartCanvas extends React.PureComponent { const funcIndex = thread.frameTable.func[frameIndex]; let resourceOrFileName = null; - // Only show resources or filenames if the chart is expanded, as collapsed stacks - // would show incorrect details about a group of stacks. - if (isRowExpanded) { - // Only JavaScript functions have a filename. - const fileNameIndex = thread.funcTable.fileName[funcIndex]; - if (fileNameIndex !== null) { - // Because of our use of Grid Layout, all our elements need to be direct - // children of the grid parent. That's why we use arrays here, to add - // the elements as direct children. - resourceOrFileName = [ -
    - File: -
    , - thread.stringTable.getString(fileNameIndex), - ]; - } else { - const resourceIndex = thread.funcTable.resource[funcIndex]; - if (resourceIndex !== -1) { - const resourceNameIndex = thread.resourceTable.name[resourceIndex]; - if (resourceNameIndex !== -1) { - // Because of our use of Grid Layout, all our elements need to be direct - // children of the grid parent. That's why we use arrays here, to add - // the elements as direct children. - resourceOrFileName = [ -
    - Resource: -
    , - thread.stringTable.getString(resourceNameIndex), - ]; - } + // Only JavaScript functions have a filename. + const fileNameIndex = thread.funcTable.fileName[funcIndex]; + if (fileNameIndex !== null) { + // Because of our use of Grid Layout, all our elements need to be direct + // children of the grid parent. That's why we use arrays here, to add + // the elements as direct children. + resourceOrFileName = [ +
    + File: +
    , + thread.stringTable.getString(fileNameIndex), + ]; + } else { + const resourceIndex = thread.funcTable.resource[funcIndex]; + if (resourceIndex !== -1) { + const resourceNameIndex = thread.resourceTable.name[resourceIndex]; + if (resourceNameIndex !== -1) { + // Because of our use of Grid Layout, all our elements need to be direct + // children of the grid parent. That's why we use arrays here, to add + // the elements as direct children. + resourceOrFileName = [ +
    + Resource: +
    , + thread.stringTable.getString(resourceNameIndex), + ]; } } } @@ -288,11 +281,8 @@ class StackChartCanvas extends React.PureComponent { const { rangeStart, rangeEnd, - viewportLeft, - viewportRight, - viewportTop, - containerWidth, stackTimingByDepth, + viewport: { viewportLeft, viewportRight, viewportTop, containerWidth }, } = this.props; const rangeLength: Milliseconds = rangeEnd - rangeStart; @@ -320,7 +310,7 @@ class StackChartCanvas extends React.PureComponent { } render() { - const { containerWidth, containerHeight, isDragging } = this.props; + const { containerWidth, containerHeight, isDragging } = this.props.viewport; return ( { } } -export default withChartViewport(StackChartCanvas); +// +export default (withChartViewport: WithChartViewport)( + StackChartCanvas +); diff --git a/src/components/stack-chart/Settings.js b/src/components/stack-chart/Settings.js index 3bff5811c0..070ed6e7ec 100644 --- a/src/components/stack-chart/Settings.js +++ b/src/components/stack-chart/Settings.js @@ -5,7 +5,7 @@ // @flow import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import { changeHidePlatformDetails, changeInvertCallstack, @@ -16,15 +16,25 @@ import { } from '../../reducers/url-state'; import StackSearchField from '../shared/StackSearchField'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; + import './Settings.css'; -type Props = {| - +hidePlatformDetails: boolean, +type StateProps = {| +invertCallstack: boolean, - +changeHidePlatformDetails: boolean => void, - +changeInvertCallstack: boolean => void, + +hidePlatformDetails: boolean, |}; +type DispatchProps = {| + +changeHidePlatformDetails: typeof changeHidePlatformDetails, + +changeInvertCallstack: typeof changeInvertCallstack, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + class StackChartSettings extends PureComponent { constructor(props) { super(props); @@ -78,13 +88,15 @@ class StackChartSettings extends PureComponent { } } -export default connect( - state => ({ +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => ({ invertCallstack: getInvertCallstack(state), hidePlatformDetails: getHidePlatformDetails(state), }), - { + mapDispatchToProps: { changeHidePlatformDetails, changeInvertCallstack, - } -)(StackChartSettings); + }, + component: StackChartSettings, +}; +export default explicitConnect(options); diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index ef1de7e0a8..3f827b6b46 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -4,7 +4,7 @@ // @flow import * as React from 'react'; -import { connect } from 'react-redux'; +import explicitConnect from '../../utils/connect'; import StackChartCanvas from './Canvas'; import { selectedThreadSelectors, @@ -16,8 +16,8 @@ import { getCategoryColorStrategy, getLabelingStrategy, } from '../../reducers/stack-chart'; -import { updateProfileSelection } from '../../actions/profile-view'; import StackChartSettings from './Settings'; +import { updateProfileSelection } from '../../actions/profile-view'; import type { Thread } from '../../types/profile'; import type { @@ -28,24 +28,33 @@ import type { StackTimingByDepth } from '../../profile-logic/stack-timing'; import type { GetCategory } from '../../profile-logic/color-categories'; import type { GetLabel } from '../../profile-logic/labeling-strategies'; import type { ProfileSelection } from '../../types/actions'; +import type { + ExplicitConnectOptions, + ConnectedProps, +} from '../../utils/connect'; require('./index.css'); const STACK_FRAME_HEIGHT = 16; -type Props = { - thread: Thread, - maxStackDepth: number, - stackTimingByDepth: StackTimingByDepth, - timeRange: { start: Milliseconds, end: Milliseconds }, - interval: Milliseconds, - getCategory: GetCategory, - getLabel: GetLabel, - updateProfileSelection: typeof updateProfileSelection, - selection: ProfileSelection, - threadName: string, - processDetails: string, -}; +type StateProps = {| + +thread: Thread, + +maxStackDepth: number, + +stackTimingByDepth: StackTimingByDepth, + +timeRange: { start: Milliseconds, end: Milliseconds }, + +interval: Milliseconds, + +getCategory: GetCategory, + +getLabel: GetLabel, + +selection: ProfileSelection, + +threadName: string, + +processDetails: string, +|}; + +type DispatchProps = {| + +updateProfileSelection: typeof updateProfileSelection, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class StackChartGraph extends React.PureComponent { /** @@ -65,10 +74,10 @@ class StackChartGraph extends React.PureComponent { interval, getCategory, getLabel, - updateProfileSelection, selection, threadName, processDetails, + updateProfileSelection, } = this.props; const maxViewportHeight = maxStackDepth * STACK_FRAME_HEIGHT; @@ -83,23 +92,24 @@ class StackChartGraph extends React.PureComponent {
    @@ -107,8 +117,8 @@ class StackChartGraph extends React.PureComponent { } } -export default connect( - state => { +const options: ExplicitConnectOptions<{||}, StateProps, DispatchProps> = { + mapStateToProps: state => { const stackTimingByDepth = selectedThreadSelectors.getStackTimingByDepthForStackChart( state ); @@ -128,9 +138,15 @@ export default connect( processDetails: selectedThreadSelectors.getThreadProcessDetails(state), }; }, - { updateProfileSelection } -)(StackChartGraph); + mapDispatchToProps: { updateProfileSelection }, + component: StackChartGraph, +}; +export default explicitConnect(options); -function viewportNeedsUpdate(prevProps, newProps) { +// This function is given the StackChartCanvas's chartProps. +function viewportNeedsUpdate( + prevProps: { +stackTimingByDepth: StackTimingByDepth }, + newProps: { +stackTimingByDepth: StackTimingByDepth } +) { return prevProps.stackTimingByDepth !== newProps.stackTimingByDepth; } diff --git a/src/components/tasktracer/ProfileTaskTracerView.js b/src/components/tasktracer/ProfileTaskTracerView.js index bf67348403..779a58438c 100644 --- a/src/components/tasktracer/ProfileTaskTracerView.js +++ b/src/components/tasktracer/ProfileTaskTracerView.js @@ -4,8 +4,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import actions from '../../actions'; +import explicitConnect from '../../utils/connect'; import { getTasksByThread, getProfileTaskTracerData, @@ -170,10 +169,12 @@ ProfileTaskTracerView.propTypes = { rangeEnd: PropTypes.number.isRequired, }; -export default connect( - state => ({ +// There is no type coverage for this connect function as it needs +// type annotations with ExplicitConnectOptions. +export default explicitConnect({ + mapStateToProps: state => ({ tasktracer: getProfileTaskTracerData(state), tasksByThread: getTasksByThread(state), }), - actions -)(ProfileTaskTracerView); + component: ProfileTaskTracerView, +}); diff --git a/src/reducers/icons.js b/src/reducers/icons.js index 32cfd258de..ba49e81427 100644 --- a/src/reducers/icons.js +++ b/src/reducers/icons.js @@ -46,7 +46,7 @@ export const getIconClassNameForCallNode = createSelector( (icons, displayData: CallNodeDisplayData) => displayData.icon !== null && icons.has(displayData.icon) ? classNameFromUrl(displayData.icon) - : null + : '' ); export const getIconsWithClassNames: State => IconWithClassName[] = createSelector( diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index febdd755c1..e9d8f7bb63 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -307,7 +307,6 @@ function selection( function scrollToSelectionGeneration(state: number = 0, action: Action) { switch (action.type) { case 'CHANGE_INVERT_CALLSTACK': - case 'CHANGE_JS_ONLY': case 'CHANGE_SELECTED_CALL_NODE': case 'CHANGE_SELECTED_THREAD': case 'HIDE_THREAD': diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index a20c34c81b..06145fcdc4 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -77,7 +77,7 @@ function rangeFilters(state: StartEndRange[] = [], action: Action) { } } -function selectedThread(state: ThreadIndex = 0, action: Action) { +function selectedThread(state: ThreadIndex = 0, action: Action): ThreadIndex { function findDefaultThreadIndex(threads) { const contentThreadId = threads.findIndex( thread => thread.name === 'GeckoMain' && thread.processType === 'tab' diff --git a/src/test/components/MarkerChart.test.js b/src/test/components/MarkerChart.test.js index ad14d44c41..861bcba659 100644 --- a/src/test/components/MarkerChart.test.js +++ b/src/test/components/MarkerChart.test.js @@ -16,7 +16,7 @@ jest.useFakeTimers(); it('renders MarkerChart correctly', () => { // Tie the requestAnimationFrame into jest's fake timers. - window.requestAnimationFrame = fn => setTimeout(fn, 0); + (window: any).requestAnimationFrame = fn => setTimeout(fn, 0); window.devicePixelRatio = 1; const ctx = mockCanvasContext(); @@ -67,7 +67,7 @@ it('renders MarkerChart correctly', () => { const markerChart = renderer.create( - + , { createNodeMock } ); diff --git a/src/test/components/ProfileThreadTracingMarkerOverview.test.js b/src/test/components/ProfileThreadTracingMarkerOverview.test.js index 7b13b1d1a5..33c62b4393 100644 --- a/src/test/components/ProfileThreadTracingMarkerOverview.test.js +++ b/src/test/components/ProfileThreadTracingMarkerOverview.test.js @@ -14,13 +14,18 @@ import ReactDOM from 'react-dom'; import { getBoundingBox } from '../fixtures/utils'; jest.useFakeTimers(); -ReactDOM.findDOMNode = jest.fn(() => ({ - getBoundingClientRect: () => getBoundingBox(200, 300), -})); +ReactDOM.findDOMNode = jest.fn(() => { + // findDOMNode uses nominal typing instead of structural (null | Element | Text), so + // opt out of the type checker for this mock by returning `any`. + const mockEl = ({ + getBoundingClientRect: () => getBoundingBox(200, 300), + }: any); + return mockEl; +}); it('renders ProfileThreadTracingMarkerOverview correctly', () => { // Tie the requestAnimationFrame into jest's fake timers. - window.requestAnimationFrame = fn => setTimeout(fn, 0); + (window: any).requestAnimationFrame = fn => setTimeout(fn, 0); window.devicePixelRatio = 1; const ctx = mockCanvasContext(); diff --git a/src/test/components/ProfileViewerHeader.test.js b/src/test/components/ProfileViewerHeader.test.js index a3fa2bbf1b..027e5e80e8 100644 --- a/src/test/components/ProfileViewerHeader.test.js +++ b/src/test/components/ProfileViewerHeader.test.js @@ -16,9 +16,14 @@ import ReactDOM from 'react-dom'; import type { Profile } from '../../types/profile'; jest.useFakeTimers(); -ReactDOM.findDOMNode = jest.fn(() => ({ - getBoundingClientRect: () => getBoundingBox(300, 300), -})); +ReactDOM.findDOMNode = jest.fn(() => { + // findDOMNode uses nominal typing instead of structural (null | Element | Text), so + // opt out of the type checker for this mock by returning `any`. + const mockEl = ({ + getBoundingClientRect: () => getBoundingBox(300, 300), + }: any); + return mockEl; +}); function _getProfileWithDroppedSamples(): Profile { const { profile } = getProfileFromTextSamples( @@ -69,7 +74,7 @@ function _getProfileWithDroppedSamples(): Profile { describe('calltree/ProfileViewerHeader', function() { it('renders the header', () => { - window.requestAnimationFrame = fn => setTimeout(fn, 0); + (window: any).requestAnimationFrame = fn => setTimeout(fn, 0); window.devicePixelRatio = 1; const ctx = mockCanvasContext(); /** diff --git a/src/test/components/StackChart.test.js b/src/test/components/StackChart.test.js index baa6e55343..36726f9d1b 100644 --- a/src/test/components/StackChart.test.js +++ b/src/test/components/StackChart.test.js @@ -16,7 +16,7 @@ jest.useFakeTimers(); it('renders StackChartGraph correctly', () => { // Tie the requestAnimationFrame into jest's fake timers. - window.requestAnimationFrame = fn => setTimeout(fn, 0); + (window: any).requestAnimationFrame = fn => setTimeout(fn, 0); window.devicePixelRatio = 1; const ctx = mockCanvasContext(); @@ -53,7 +53,7 @@ it('renders StackChartGraph correctly', () => { const stackChart = renderer.create( - + , { createNodeMock } ); diff --git a/src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap b/src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap index c7528e2c26..e206636697 100644 --- a/src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap +++ b/src/test/components/__snapshots__/ProfileViewerHeader.test.js.snap @@ -113,7 +113,6 @@ exports[`calltree/ProfileViewerHeader renders the header 1`] = `
  • { + // This function is extremely polymorphic and defies typing. + return (jest.fn: any)((...args) => { log.push([name, ...args]); if (fn) { return fn(...args); diff --git a/src/test/store/icons.test.js b/src/test/store/icons.test.js index 66eac3f0fb..af1ae8a8b4 100644 --- a/src/test/store/icons.test.js +++ b/src/test/store/icons.test.js @@ -64,11 +64,11 @@ describe('actions/icons', function() { expect(subject).toBeNull(); }); - it('getIconClassNameForCallNode returns null for any icon', function() { + it('getIconClassNameForCallNode returns an empty string for any icon', function() { const subject = iconsAccessors.getIconClassNameForCallNode(state, { icon: validIcons[0], }); - expect(subject).toBeNull(); + expect(subject).toBe(''); }); it('getIconsWithClassNames returns an empty array', function() { @@ -135,7 +135,7 @@ describe('actions/icons', function() { subject = iconsAccessors.getIconClassNameForCallNode(state, { icon: invalidIcon, }); - expect(subject).toBeNull(); + expect(subject).toBe(''); subject = iconsAccessors.getIconsWithClassNames(state); expect(subject).toEqual([]); diff --git a/src/test/store/transforms.test.js b/src/test/store/transforms.test.js index 9de04ceb59..2485437ea7 100644 --- a/src/test/store/transforms.test.js +++ b/src/test/store/transforms.test.js @@ -69,7 +69,7 @@ describe('"focus-subtree" transform', function() { }); it('can remove the transform', function() { - dispatch(popTransformsFromStack(threadIndex, 0)); + dispatch(popTransformsFromStack(0)); const callTree = selectedThreadSelectors.getCallTree(getState()); const formattedTree = formatTree(callTree); expect(formattedTree).toMatchSnapshot(); diff --git a/src/types/actions.js b/src/types/actions.js index 471bbc5fa8..c1ed71ec73 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -124,7 +124,8 @@ type ReceiveProfileAction = type StackChartAction = | { type: 'CHANGE_STACK_CHART_COLOR_STRATEGY', getCategory: GetCategory } - | { type: 'CHANGE_STACK_CHART_LABELING_STRATEGY', getLabel: GetLabel }; + | { type: 'CHANGE_STACK_CHART_LABELING_STRATEGY', getLabel: GetLabel } + | { type: 'HAS_ZOOMED_VIA_MOUSEWHEEL' }; type UrlEnhancerAction = | { type: '@@urlenhancer/urlSetupDone' } diff --git a/src/types/indexeddb.js b/src/types/indexeddb.js index c576f1dd91..bd13ef5a70 100644 --- a/src/types/indexeddb.js +++ b/src/types/indexeddb.js @@ -132,16 +132,18 @@ export interface IDBIndex extends EventTarget { unique: boolean, } +// TODO - Investigate for correctness, see: +// https://github.com/devtools-html/perf.html/issues/718 export interface IDBKeyRange { - static bound( + bound( lower: J, upper: J, lowerOpen?: boolean, upperOpen?: boolean ): IDBKeyRange, - static only(value: J): IDBKeyRange, - static lowerBound(bound: J, open?: boolean): IDBKeyRange, - static upperBound(bound: J, open?: boolean): IDBKeyRange, + only(value: J): IDBKeyRange, + lowerBound(bound: J, open?: boolean): IDBKeyRange, + upperBound(bound: J, open?: boolean): IDBKeyRange, lower: K, upper: K, lowerOpen: boolean, diff --git a/src/utils/connect.js b/src/utils/connect.js new file mode 100644 index 0000000000..b7b02f7097 --- /dev/null +++ b/src/utils/connect.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import * as React from 'react'; +import { connect } from 'react-redux'; +import type { Dispatch, State } from '../types/store'; + +type MapStateToProps = ( + state: State, + ownProps: OwnProps +) => StateProps; + +type MapDispatchToProps = + | ((dispatch: Dispatch, ownProps: OwnProps) => DispatchProps) + | DispatchProps; + +type MergeProps< + StateProps, + DispatchProps: Object, + OwnProps: Object, + Props: Object +> = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnProps +) => Props; + +type ConnectOptions = { + pure?: boolean, + areStatesEqual?: boolean, + areOwnPropsEqual?: boolean, + areStatePropsEqual?: boolean, + areMergedPropsEqual?: boolean, + storeKey?: boolean, + withRef?: boolean, +}; + +export type ExplicitConnectOptions< + OwnProps: Object, + StateProps: Object, + DispatchProps: Object +> = { + mapStateToProps?: MapStateToProps, + mapDispatchToProps?: MapDispatchToProps, + mergeProps?: MergeProps< + StateProps, + DispatchProps, + OwnProps, + ConnectedProps + >, + options?: ConnectOptions, + component: React.ComponentType< + ConnectedProps + >, +}; + +export type ConnectedProps< + OwnProps: Object, + StateProps: Object, + DispatchProps: Object +> = $ReadOnly<{| + ...OwnProps, + ...StateProps, + ...DispatchProps, +|}>; + +export type ConnectedComponent< + OwnProps: Object, + StateProps: Object, + DispatchProps: Object +> = + | React.ComponentType> + | React.StatelessFunctionalComponent< + ConnectedProps + >; + +/** + * react-redux's connect function is too polymorphic and problematic. This function + * is a wrapper to simplify the typing of connect and make it more explicit, and + * less magical. + */ +export default function explicitConnect< + OwnProps: Object, + StateProps: Object, + DispatchProps: Object +>( + connectOptions: ExplicitConnectOptions +): React.ComponentType { + const { + mapStateToProps, + mapDispatchToProps, + mergeProps, + options, + component, + } = connectOptions; + + // Opt out of the flow-typed definition of react-redux's connect, and use our own. + return (connect: any)( + mapStateToProps, + mapDispatchToProps, + mergeProps, + options + )(component); +} diff --git a/src/utils/flow.js b/src/utils/flow.js index bff673410e..5c21ce5a45 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -3,6 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @flow +import type { TabSlug } from '../types/actions'; + /** * This file contains utils that help Flow understand things better. Occasionally * statements can be logically equivalent, but Flow infers them in a specific way. Most @@ -29,3 +31,19 @@ export function unexpectedCase(notValid: empty): void { export function immutableUpdate(object: T, ...rest: Object[]): T { return Object.assign({}, object, ...rest); } + +/** + * This function takes a string and returns either a valid TabSlug or null, this doesn't + * throw an error so that any arbitrary string can be converted, e.g. from a URL. + */ +export function toValidTabSlug(tabSlug: string): TabSlug | null { + switch (tabSlug) { + case 'calltree': + case 'stack-chart': + case 'marker-chart': + case 'marker-table': + return tabSlug; + default: + return null; + } +} diff --git a/yarn.lock b/yarn.lock index c3a8414117..94ef38cfae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,13 +58,6 @@ acorn@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" -agent-base@2: - version "2.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7" - dependencies: - extend "~3.0.0" - semver "~5.0.1" - ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -1889,7 +1882,7 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2, debug@2.6.8, debug@^2.2.0, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8: +debug@*, debug@2.6.8, debug@^2.2.0, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" dependencies: @@ -2625,7 +2618,7 @@ express@^4.13.3, express@^4.15.4: utils-merge "1.0.0" vary "~1.1.1" -extend@3, extend@~3.0.0: +extend@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -2845,9 +2838,9 @@ flow-annotation-check@1.3.1: glob "7.1.1" load-pkg "^3.0.1" -flow-bin@^0.54.1: - version "0.54.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.54.1.tgz#7101bcccf006dc0652714a8aef0c72078a760510" +flow-bin@^0.63.1: + version "0.63.1" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.63.1.tgz#ab00067c197169a5fb5b4996c8f6927b06694828" flow-coverage-report@^0.4.0: version "0.4.0" @@ -2867,14 +2860,14 @@ flow-coverage-report@^0.4.0: terminal-table "0.0.12" yargs "8.0.1" -flow-typed@^2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-2.1.5.tgz#c96912807a286357340042783c9369360f384bbd" +flow-typed@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/flow-typed/-/flow-typed-2.2.3.tgz#e7a35915a0f4cfcf8068c1ce291b5c99e6b89efa" dependencies: babel-polyfill "^6.23.0" colors "^1.1.2" fs-extra "^4.0.0" - github "^9.2.0" + github "0.2.4" glob "^7.1.2" got "^7.1.0" md5 "^2.1.0" @@ -2888,13 +2881,6 @@ flow-typed@^2.1.5: which "^1.2.14" yargs "^4.2.0" -follow-redirects@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-0.0.7.tgz#34b90bab2a911aa347571da90f22bd36ecd8a919" - dependencies: - debug "^2.2.0" - stream-consume "^0.1.0" - for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3043,14 +3029,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -github@^9.2.0: - version "9.3.1" - resolved "https://registry.yarnpkg.com/github/-/github-9.3.1.tgz#6a3c5a9cc2a1cd0b5d097a47baefb9d11caef89e" +github@0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/github/-/github-0.2.4.tgz#24fa7f0e13fa11b946af91134c51982a91ce538b" dependencies: - follow-redirects "0.0.7" - https-proxy-agent "^1.0.0" mime "^1.2.11" - netrc "^0.1.4" glob-base@^0.3.0: version "0.3.0" @@ -3429,14 +3412,6 @@ https-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" -https-proxy-agent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" - dependencies: - agent-base "2" - debug "2" - extend "3" - husky@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" @@ -5072,10 +5047,6 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" -netrc@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/netrc/-/netrc-0.1.4.tgz#6be94fcaca8d77ade0a9670dc460914c94472444" - nise@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/nise/-/nise-1.0.1.tgz#0da92b10a854e97c0f496f6c2845a301280b3eef" @@ -6602,10 +6573,6 @@ semver@^5.1.0, semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" -semver@~5.0.1: - version "5.0.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" - send@0.15.4: version "0.15.4" resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" @@ -6890,10 +6857,6 @@ stream-combiner@~0.0.4: dependencies: duplexer "~0.1.1" -stream-consume@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" - stream-http@^2.3.1: version "2.7.2" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"