diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 643c6e6dec..73d3018bcf 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -73,6 +73,7 @@ import type { Tid, GlobalTrack, KeyboardModifiers, + TableViewOptions, } from 'firefox-profiler/types'; import { funcHasDirectRecursiveCall, @@ -1924,6 +1925,17 @@ export function changeMouseTimePosition( }; } +export function changeTableViewOptions( + tab: TabSlug, + tableViewOptions: TableViewOptions +): Action { + return { + type: 'CHANGE_TABLE_VIEW_OPTIONS', + tab, + tableViewOptions, + }; +} + export function openSourceView(file: string, currentTab: TabSlug): Action { return { type: 'OPEN_SOURCE_VIEW', diff --git a/src/components/app/ZipFileViewer.js b/src/components/app/ZipFileViewer.js index 228b4ed457..550eeb34e4 100644 --- a/src/components/app/ZipFileViewer.js +++ b/src/components/app/ZipFileViewer.js @@ -24,6 +24,7 @@ import { import { getPathInZipFileFromUrl } from 'firefox-profiler/selectors/url-state'; import { TreeView } from 'firefox-profiler/components/shared/TreeView'; import { ProfileViewer } from './ProfileViewer'; +import { defaultTableViewOptions } from 'firefox-profiler/reducers/profile-view'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; import type { ZipFileState } from 'firefox-profiler/types'; @@ -269,6 +270,7 @@ class ZipFileViewerImpl extends React.PureComponent { rowHeight={30} indentWidth={15} onEnterKey={this._onEnterKey} + viewOptions={defaultTableViewOptions} /> diff --git a/src/components/calltree/CallTree.css b/src/components/calltree/CallTree.css index 7134a4daa8..3f5702c346 100644 --- a/src/components/calltree/CallTree.css +++ b/src/components/calltree/CallTree.css @@ -7,20 +7,6 @@ text-align: right; } -.treeViewFixedColumn.total { - width: 70px; -} - -/* The header for the total column spans both totalPercent and total columns */ -.treeViewHeaderColumn.total { - width: 120px; -} - -.treeViewFixedColumn.totalPercent { - width: 50px; - border-right: none; -} - /* The header for the totalPercent column is not visible */ .treeViewHeaderColumn.totalPercent { display: none; @@ -32,7 +18,6 @@ .treeViewFixedColumn.icon { display: flex; - width: 19px; flex-flow: column nowrap; align-items: center; } diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index 464efc9bf1..c23189f6b1 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -21,6 +21,7 @@ import { getFocusCallTreeGeneration, getPreviewSelection, getCategories, + getCurrentTableViewOptions, } from 'firefox-profiler/selectors/profile'; import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { @@ -30,6 +31,7 @@ import { addTransformToStack, handleCallNodeTransformShortcut, openSourceView, + changeTableViewOptions, } from 'firefox-profiler/actions/profile-view'; import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; @@ -42,10 +44,14 @@ import type { IndexIntoCallNodeTable, CallNodeDisplayData, WeightType, + TableViewOptions, } from 'firefox-profiler/types'; import type { CallTree as CallTreeType } from 'firefox-profiler/profile-logic/call-tree'; -import type { Column } from 'firefox-profiler/components/shared/TreeView'; +import type { + Column, + MaybeResizableColumn, +} from 'firefox-profiler/components/shared/TreeView'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; import './CallTree.css'; @@ -66,6 +72,7 @@ type StateProps = {| +implementationFilter: ImplementationFilter, +callNodeMaxDepth: number, +weightType: WeightType, + +tableViewOptions: TableViewOptions, |}; type DispatchProps = {| @@ -75,6 +82,7 @@ type DispatchProps = {| +addTransformToStack: typeof addTransformToStack, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, +openSourceView: typeof openSourceView, + +onTableViewOptionsChange: (TableViewOptions) => any, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; @@ -96,46 +104,97 @@ class CallTreeImpl extends PureComponent { * appropriate labels for the call tree based on this weight. */ _weightTypeToColumns = memoize( - (weightType: WeightType): Column[] => { + (weightType: WeightType): MaybeResizableColumn[] => { switch (weightType) { case 'tracing-ms': return [ - { propName: 'totalPercent', titleL10nId: '' }, + { + propName: 'totalPercent', + titleL10nId: '', + initialWidth: 50, + hideDividerAfter: true, + }, { propName: 'total', titleL10nId: 'CallTree--tracing-ms-total', + minWidth: 30, + initialWidth: 70, + resizable: true, + headerWidthAdjustment: 50, }, { propName: 'self', titleL10nId: 'CallTree--tracing-ms-self', + minWidth: 30, + initialWidth: 70, + resizable: true, + }, + { + propName: 'icon', + titleL10nId: '', + component: Icon, + initialWidth: 10, }, - { propName: 'icon', titleL10nId: '', component: Icon }, ]; case 'samples': return [ - { propName: 'totalPercent', titleL10nId: '' }, + { + propName: 'totalPercent', + titleL10nId: '', + initialWidth: 50, + hideDividerAfter: true, + }, { propName: 'total', titleL10nId: 'CallTree--samples-total', + minWidth: 30, + initialWidth: 70, + resizable: true, + headerWidthAdjustment: 50, }, { propName: 'self', titleL10nId: 'CallTree--samples-self', + minWidth: 30, + initialWidth: 70, + resizable: true, + }, + { + propName: 'icon', + titleL10nId: '', + component: Icon, + initialWidth: 10, }, - { propName: 'icon', titleL10nId: '', component: Icon }, ]; case 'bytes': return [ - { propName: 'totalPercent', titleL10nId: '' }, + { + propName: 'totalPercent', + titleL10nId: '', + initialWidth: 50, + hideDividerAfter: true, + }, { propName: 'total', titleL10nId: 'CallTree--bytes-total', + minWidth: 30, + initialWidth: 140, + resizable: true, + headerWidthAdjustment: 50, }, { propName: 'self', titleL10nId: 'CallTree--bytes-self', + minWidth: 30, + initialWidth: 90, + resizable: true, + }, + { + propName: 'icon', + titleL10nId: '', + component: Icon, + initialWidth: 10, }, - { propName: 'icon', titleL10nId: '', component: Icon }, ]; default: throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.'); @@ -300,6 +359,8 @@ class CallTreeImpl extends PureComponent { disableOverscan, callNodeMaxDepth, weightType, + tableViewOptions, + onTableViewOptionsChange, } = this.props; if (tree.getRoots().length === 0) { return ; @@ -326,6 +387,8 @@ class CallTreeImpl extends PureComponent { onKeyDown={this._onKeyDown} onEnterKey={this._onEnterOrDoubleClick} onDoubleClick={this._onEnterOrDoubleClick} + viewOptions={tableViewOptions} + onViewOptionsChange={onTableViewOptionsChange} /> ); } @@ -355,6 +418,7 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({ callNodeMaxDepth: selectedThreadSelectors.getFilteredCallNodeMaxDepth(state), weightType: selectedThreadSelectors.getWeightTypeForCallTree(state), + tableViewOptions: getCurrentTableViewOptions(state), }), mapDispatchToProps: { changeSelectedCallNode, @@ -363,6 +427,8 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({ addTransformToStack, handleCallNodeTransformShortcut, openSourceView, + onTableViewOptionsChange: (options: TableViewOptions) => + changeTableViewOptions('calltree', options), }, component: CallTreeImpl, }); diff --git a/src/components/marker-table/index.js b/src/components/marker-table/index.js index 1f85812064..b222efeafb 100644 --- a/src/components/marker-table/index.js +++ b/src/components/marker-table/index.js @@ -14,12 +14,14 @@ import { getZeroAt, getScrollToSelectionGeneration, getMarkerSchemaByName, + getCurrentTableViewOptions, } from '../../selectors/profile'; import { selectedThreadSelectors } from '../../selectors/per-thread'; import { getSelectedThreadsKey } from '../../selectors/url-state'; import { changeSelectedMarker, changeRightClickedMarker, + changeTableViewOptions, } from '../../actions/profile-view'; import { MarkerSettings } from '../shared/MarkerSettings'; import { formatSeconds, formatTimestamp } from '../../utils/format-numbers'; @@ -32,6 +34,7 @@ import type { MarkerIndex, Milliseconds, MarkerSchemaByName, + TableViewOptions, } from 'firefox-profiler/types'; import type { ConnectedProps } from '../../utils/connect'; @@ -149,20 +152,40 @@ type StateProps = {| +scrollToSelectionGeneration: number, +markerSchemaByName: MarkerSchemaByName, +getMarkerLabel: (MarkerIndex) => string, + +tableViewOptions: TableViewOptions, |}; type DispatchProps = {| +changeSelectedMarker: typeof changeSelectedMarker, +changeRightClickedMarker: typeof changeRightClickedMarker, + +onTableViewOptionsChange: (TableViewOptions) => any, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; class MarkerTableImpl extends PureComponent { _fixedColumns = [ - { propName: 'start', titleL10nId: 'MarkerTable--start' }, - { propName: 'duration', titleL10nId: 'MarkerTable--duration' }, - { propName: 'type', titleL10nId: 'MarkerTable--type' }, + { + propName: 'start', + titleL10nId: 'MarkerTable--start', + minWidth: 30, + initialWidth: 90, + resizable: true, + }, + { + propName: 'duration', + titleL10nId: 'MarkerTable--duration', + minWidth: 30, + initialWidth: 80, + resizable: true, + }, + { + propName: 'type', + titleL10nId: 'MarkerTable--type', + minWidth: 30, + initialWidth: 150, + resizable: true, + }, ]; _mainColumn = { propName: 'name', titleL10nId: 'MarkerTable--description' }; _expandedNodeIds: Array = []; @@ -247,6 +270,8 @@ class MarkerTableImpl extends PureComponent { contextMenuId="MarkerContextMenu" rowHeight={16} indentWidth={10} + viewOptions={this.props.tableViewOptions} + onViewOptionsChange={this.props.onTableViewOptionsChange} /> )} @@ -266,7 +291,13 @@ export const MarkerTable = explicitConnect<{||}, StateProps, DispatchProps>({ zeroAt: getZeroAt(state), markerSchemaByName: getMarkerSchemaByName(state), getMarkerLabel: selectedThreadSelectors.getMarkerTableLabelGetter(state), + tableViewOptions: getCurrentTableViewOptions(state), }), - mapDispatchToProps: { changeSelectedMarker, changeRightClickedMarker }, + mapDispatchToProps: { + changeSelectedMarker, + changeRightClickedMarker, + onTableViewOptionsChange: (tableViewOptions) => + changeTableViewOptions('marker-table', tableViewOptions), + }, component: MarkerTableImpl, }); diff --git a/src/components/shared/TreeView.css b/src/components/shared/TreeView.css index dda999b6ad..684def6524 100644 --- a/src/components/shared/TreeView.css +++ b/src/components/shared/TreeView.css @@ -77,29 +77,45 @@ .treeViewHeaderColumn { position: relative; - box-sizing: border-box; - padding: 0 5px; line-height: 15px; white-space: nowrap; } .treeViewFixedColumn { overflow: hidden; - padding: 0 5px; text-overflow: ellipsis; } +.treeViewColumnDivider { + display: flex; + width: 20px; + flex: none; + align-items: stretch; + justify-content: center; + margin-right: -5px; + margin-left: -5px; +} + +.treeViewColumnDivider.isResizable, +.treeView.isResizingColumns { + cursor: col-resize; +} + +.treeViewColumnDivider::before { + border-right: 1px solid var(--grey-30); + content: ''; +} + +.treeViewColumnDivider.isResizable::before { + width: 1px; + border-left: 1px solid var(--grey-30); +} + .treeViewHeaderColumn.treeViewFixedColumn { /* The fixed columns in the row don't shrink because they're positioning using * position: sticky, therefore we prevent shrinking for the columns in the * header too. */ flex: none; - border-right: 1px solid #e2e2e2; -} - -.treeViewRowColumn.treeViewFixedColumn { - box-sizing: border-box; - border-right: 1px solid var(--grey-30); } .treeBadge { diff --git a/src/components/shared/TreeView.js b/src/components/shared/TreeView.js index fa0c0a03ce..72a3fbd2f0 100644 --- a/src/components/shared/TreeView.js +++ b/src/components/shared/TreeView.js @@ -16,7 +16,7 @@ import { VirtualList } from './VirtualList'; import { ContextMenuTrigger } from './ContextMenuTrigger'; -import type { CssPixels } from 'firefox-profiler/types'; +import type { CssPixels, TableViewOptions } from 'firefox-profiler/types'; import './TreeView.css'; @@ -44,6 +44,9 @@ function PermissiveLocalized(props: React.ElementConfig) { // See https://github.com/facebook/flow/issues/4099 type RegExpResult = null | ({ index: number, input: string } & string[]); type NodeIndex = number; +type TableViewOptionsWithDefault = {| + fixedColumnWidths: Array, +|}; export type Column = {| +propName: string, @@ -53,40 +56,95 @@ export type Column = {| |}>, |}; +export type MaybeResizableColumn = {| + ...Column, + /** defaults to initialWidth */ + +minWidth?: CssPixels, + /** This is the initial width, this can be changed in resizable columns */ + +initialWidth: CssPixels, + /** found width + adjustment = width of header column */ + +headerWidthAdjustment?: CssPixels, + // false by default + +resizable?: boolean, + // is the divider after the column hidden? false by default + +hideDividerAfter?: boolean, +|}; + type TreeViewHeaderProps = {| - +fixedColumns: Column[], + +fixedColumns: MaybeResizableColumn[], +mainColumn: Column, + +viewOptions: TableViewOptionsWithDefault, + // called when the users moves the divider right of the column, + // passes the column index and the start x coordinate + +onColumnWidthChangeStart: (number, CssPixels) => void, + +onColumnWidthReset: (number) => void, |}; -const TreeViewHeader = ({ - fixedColumns, - mainColumn, -}: TreeViewHeaderProps) => { - if (fixedColumns.length === 0 && !mainColumn.titleL10nId) { - // If there is nothing to display in the header, do not render it. - return null; - } - return ( -
- {fixedColumns.map((col) => ( +class TreeViewHeader extends React.PureComponent< + TreeViewHeaderProps +> { + _onDividerMouseDown = (event: SyntheticMouseEvent) => { + this.props.onColumnWidthChangeStart( + Number(event.currentTarget.dataset.columnIndex), + event.clientX + ); + }; + + _onDividerDoubleClick = (event: SyntheticMouseEvent) => { + this.props.onColumnWidthReset( + Number(event.currentTarget.dataset.columnIndex) + ); + }; + + render() { + const { fixedColumns, mainColumn, viewOptions } = this.props; + const columnWidths = viewOptions.fixedColumnWidths; + if (fixedColumns.length === 0 && !mainColumn.titleL10nId) { + // If there is nothing to display in the header, do not render it. + return null; + } + return ( +
+ {fixedColumns.map((col, i) => { + const width = columnWidths[i] + (col.headerWidthAdjustment || 0); + return ( + + + + + {col.hideDividerAfter !== true ? ( + + ) : null} + + ); + })} - ))} - - - -
- ); -}; +
+ ); + } +} function reactStringWithHighlightedSubstrings( string: string, @@ -123,13 +181,14 @@ function reactStringWithHighlightedSubstrings( type TreeViewRowFixedColumnsProps = {| +displayData: DisplayData, +nodeId: NodeIndex, - +columns: Column[], + +columns: MaybeResizableColumn[], +index: number, +isSelected: boolean, +isRightClicked: boolean, +onClick: (NodeIndex, SyntheticMouseEvent<>) => mixed, +highlightRegExp: RegExp | null, +rowHeightStyle: { height: CssPixels, lineHeight: string }, + +viewOptions: TableViewOptionsWithDefault, |}; class TreeViewRowFixedColumns extends React.PureComponent< @@ -144,12 +203,14 @@ class TreeViewRowFixedColumns extends React.PureComponent< const { displayData, columns, + viewOptions, index, isSelected, isRightClicked, highlightRegExp, rowHeightStyle, } = this.props; + const columnWidths = viewOptions.fixedColumnWidths; return (
extends React.PureComponent< style={rowHeightStyle} onMouseDown={this._onClick} > - {columns.map((col) => { + {columns.map((col, i) => { const RenderComponent = col.component; const text = displayData[col.propName] || ''; - return ( - - {RenderComponent ? ( - - ) : ( - reactStringWithHighlightedSubstrings( - text, - highlightRegExp, - 'treeViewHighlighting' - ) - )} - + + + {RenderComponent ? ( + + ) : ( + reactStringWithHighlightedSubstrings( + text, + highlightRegExp, + 'treeViewHighlighting' + ) + )} + + {col.hideDividerAfter !== true ? ( + + ) : null} + ); })}
@@ -373,7 +438,7 @@ interface Tree { } type TreeViewProps = {| - +fixedColumns: Column[], + +fixedColumns: MaybeResizableColumn[], +mainColumn: Column, +tree: Tree, +expandedNodeIds: Array, @@ -393,14 +458,41 @@ type TreeViewProps = {| +rowHeight: CssPixels, +indentWidth: CssPixels, +onKeyDown?: (SyntheticKeyboardEvent<>) => void, + +viewOptions: TableViewOptions, + +onViewOptionsChange?: (TableViewOptions) => void, +|}; + +type TreeViewState = {| + +fixedColumnWidths: Array | null, + +isResizingColumns: boolean, |}; export class TreeView extends React.PureComponent< - TreeViewProps + TreeViewProps, + TreeViewState > { _list: VirtualList | null = null; _takeListRef = (list: VirtualList | null) => (this._list = list); + // This contains the information about the current column resizing happening currently. + _currentMovedColumnState: {| + columnIndex: number, + startX: CssPixels, + initialWidth: CssPixels, + |} | null = null; + + state = { + // This contains the current widths, while or after the user resizes them. + fixedColumnWidths: null, + + // This is true when the user is currently resizing a column. + isResizingColumns: false, + }; + + // This is incremented when a column changed its size. We use this to force a + // rerender of the VirtualList component. + _columnSizeChangedCounter: number = 0; + // The tuple `specialItems` always contains 2 elements: the first element is // the selected node id (if any), and the second element is the right clicked // id (if any). @@ -421,6 +513,100 @@ export class TreeView extends React.PureComponent< { limit: 1 } ); + _computeInitialColumnWidthsMemoized = memoize( + (fixedColumns: Array>): CssPixels[] => + fixedColumns.map((c) => c.initialWidth) + ); + + // This returns the column widths from several possible sources, in this order: + // * the current state (this means the user changed them recently, or is + // currently changing them) + // * the view options (this comes from the redux state, this means the user + // changed them in the past) + // * or finally the initial values from the fixedColumns information. + _getCurrentFixedColumnWidths = (): Array => { + return ( + this.state.fixedColumnWidths || + this.props.viewOptions.fixedColumnWidths || + this._computeInitialColumnWidthsMemoized(this.props.fixedColumns) + ); + }; + + _getCurrentViewOptions = (): TableViewOptionsWithDefault => { + return { + fixedColumnWidths: this._getCurrentFixedColumnWidths(), + }; + }; + + _onColumnWidthChangeStart = (columnIndex: number, startX: CssPixels) => { + this._currentMovedColumnState = { + columnIndex, + startX, + initialWidth: this._getCurrentFixedColumnWidths()[columnIndex], + }; + this.setState({ isResizingColumns: true }); + window.addEventListener('mousemove', this._onColumnWidthChangeMouseMove); + window.addEventListener('mouseup', this._onColumnWidthChangeMouseUp); + }; + + _cleanUpMouseHandlers = () => { + window.removeEventListener('mousemove', this._onColumnWidthChangeMouseMove); + window.removeEventListener('mouseup', this._onColumnWidthChangeMouseUp); + }; + + _onColumnWidthChangeMouseMove = (event: MouseEvent) => { + const columnState = this._currentMovedColumnState; + if (columnState !== null) { + const { columnIndex, startX, initialWidth } = columnState; + const column = this.props.fixedColumns[columnIndex]; + const fixedColumnWidths = this._getCurrentFixedColumnWidths(); + const diff = event.clientX - startX; + const newWidth = Math.max(initialWidth + diff, column.minWidth || 10); + this.setState((prevState) => { + this._columnSizeChangedCounter++; + const newFixedColumnWidths = ( + prevState.fixedColumnWidths || fixedColumnWidths + ).slice(); + newFixedColumnWidths[columnIndex] = newWidth; + return { + fixedColumnWidths: newFixedColumnWidths, + }; + }); + } + }; + + _onColumnWidthChangeMouseUp = () => { + this.setState({ isResizingColumns: false }); + this._cleanUpMouseHandlers(); + this._currentMovedColumnState = null; + this._propagateColumnWidthChange(this._getCurrentFixedColumnWidths()); + }; + + componentWillUnmount = () => { + this._cleanUpMouseHandlers(); + }; + + _onColumnWidthReset = (columnIndex: number) => { + const column = this.props.fixedColumns[columnIndex]; + const fixedColumnWidths = this._getCurrentFixedColumnWidths(); + const newFixedColumnWidths = fixedColumnWidths.slice(); + newFixedColumnWidths[columnIndex] = column.initialWidth || 10; + this._columnSizeChangedCounter++; + this.setState({ fixedColumnWidths: newFixedColumnWidths }); + this._propagateColumnWidthChange(newFixedColumnWidths); + }; + + // triggers a re-render + _propagateColumnWidthChange = (fixedColumnWidths: Array) => { + const { onViewOptionsChange, viewOptions } = this.props; + if (onViewOptionsChange) { + onViewOptionsChange({ + ...viewOptions, + fixedColumnWidths, + }); + } + }; + _computeAllVisibleRowsMemoized = memoize( (tree: Tree, expandedNodes: Set) => { function _addVisibleRowsFromNode(tree, expandedNodes, arr, nodeId) { @@ -482,6 +668,7 @@ export class TreeView extends React.PureComponent< extends React.PureComponent< rowHeight, selectedNodeId, } = this.props; + const { isResizingColumns } = this.state; return ( -
- +
+ extends React.PureComponent< focusable={true} onKeyDown={this._onKeyDown} specialItems={this._getSpecialItems()} - disableOverscan={!!disableOverscan} + disableOverscan={!!disableOverscan || isResizingColumns} onCopy={this._onCopy} // If there is a deep call node depth, expand the width, or else keep it // at 3000 wide. containerWidth={Math.max(3000, maxNodeDepth * 10 + 2000)} ref={this._takeListRef} + forceRender={this._columnSizeChangedCounter} /> {contextMenu} diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index 6474fcd0f1..ee85da8795 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -24,17 +24,20 @@ import type { SymbolicationStatus, ThreadViewOptions, ThreadViewOptionsPerThreads, + TableViewOptionsPerTab, RightClickedCallNode, MarkerReference, ActiveTabTimeline, CallNodePath, ThreadsKey, Milliseconds, + TableViewOptions, } from 'firefox-profiler/types'; import { applyFuncSubstitutionToCallPath, applyFuncSubstitutionToPathSetAndIncludeNewAncestors, } from '../profile-logic/symbolication'; +import type { TabSlug } from '../app-logic/tabs-handling'; import { objectMap } from '../utils/flow'; @@ -405,6 +408,39 @@ const viewOptionsPerThread: Reducer = ( } }; +export const defaultTableViewOptions: TableViewOptions = { + fixedColumnWidths: null, +}; + +function _updateTableViewOptions( + state: TableViewOptionsPerTab, + tab: TabSlug, + updates: $Shape +): TableViewOptionsPerTab { + const newState = { ...state }; + newState[tab] = { + ...(state[tab] ?? defaultTableViewOptions), + ...updates, + }; + return newState; +} + +const tableViewOptionsPerTab: Reducer = ( + state = ({}: TableViewOptionsPerTab), + action +): TableViewOptionsPerTab => { + switch (action.type) { + case 'CHANGE_TABLE_VIEW_OPTIONS': + return _updateTableViewOptions( + state, + action.tab, + action.tableViewOptions + ); + default: + return state; + } +}; + const waitingForLibs: Reducer> = ( state = new Set(), action @@ -721,6 +757,7 @@ const profileViewReducer: Reducer = wrapReducerInResetter( rightClickedMarker, hoveredMarker, mouseTimePosition, + perTab: tableViewOptionsPerTab, }), profile, full: combineReducers({ diff --git a/src/selectors/profile.js b/src/selectors/profile.js index 5b819ca7ab..1d6614acd9 100644 --- a/src/selectors/profile.js +++ b/src/selectors/profile.js @@ -21,6 +21,8 @@ import { } from '../profile-logic/marker-data'; import { markerSchemaFrontEndOnly } from '../profile-logic/marker-schema'; import { getDefaultCategories } from 'firefox-profiler/profile-logic/data-structures'; +import { defaultTableViewOptions } from '../reducers/profile-view'; +import type { TabSlug } from '../app-logic/tabs-handling'; import type { Profile, @@ -75,6 +77,7 @@ import type { SampleUnits, IndexIntoSamplesTable, ExtraProfileInfoSection, + TableViewOptions, } from 'firefox-profiler/types'; export const getProfileView: Selector = (state) => @@ -93,6 +96,9 @@ export const getOriginsProfileView: Selector = (state) => export const getProfileViewOptions: Selector< $PropertyType > = (state) => getProfileView(state).viewOptions; +export const getCurrentTableViewOptions: Selector = (state) => + getProfileViewOptions(state).perTab[UrlState.getSelectedTab(state)] || + defaultTableViewOptions; export const getProfileRootRange: Selector = (state) => getProfileViewOptions(state).rootRange; export const getSymbolicationStatus: Selector = (state) => @@ -122,6 +128,12 @@ export const getCommittedRange: Selector = createSelector( export const getMouseTimePosition: Selector = (state) => getProfileViewOptions(state).mouseTimePosition; +export const getTableViewOptionSelectors: (TabSlug) => Selector = + (tab) => (state) => { + const options = getProfileViewOptions(state).perTab[tab]; + return options || defaultTableViewOptions; + }; + export const getPreviewSelection: Selector = (state) => getProfileViewOptions(state).previewSelection; diff --git a/src/test/components/MarkerTable.test.js b/src/test/components/MarkerTable.test.js index ba67e6a2d7..2d70401c1c 100644 --- a/src/test/components/MarkerTable.test.js +++ b/src/test/components/MarkerTable.test.js @@ -9,7 +9,11 @@ import { stripIndent } from 'common-tags'; // This module is mocked. import copy from 'copy-to-clipboard'; -import { render, screen } from 'firefox-profiler/test/fixtures/testing-library'; +import { + render, + screen, + fireEvent, +} from 'firefox-profiler/test/fixtures/testing-library'; import { MarkerTable } from '../../components/marker-table'; import { MaybeMarkerContextMenu } from '../../components/shared/MarkerContextMenu'; import { @@ -19,6 +23,7 @@ import { hideLocalTrack, selectTrackWithModifiers, } from '../../actions/profile-view'; +import { changeSelectedTab } from 'firefox-profiler/actions/app'; import { ensureExists } from '../../utils/flow'; import { getEmptyThread } from 'firefox-profiler/profile-logic/data-structures'; @@ -420,6 +425,58 @@ describe('MarkerTable', function () { expect(screen.queryByText(/Select the/)).not.toBeInTheDocument(); }); }); + + describe('column resizing', () => { + it('can resize a column, then move it back to its initial size', () => { + const store = storeWithProfile(getMarkerTableProfile()); + store.dispatch(changeSelectedTab('marker-table')); + const { unmount } = render( + + + + ); + + let dividerForFirstColumn = ensureExists( + document.querySelector('.treeViewColumnDivider') + ); + let firstColumn = screen.getByText('Start'); + expect(firstColumn).toHaveStyle({ width: '90px' }); + fireEvent.mouseDown(dividerForFirstColumn, { clientX: 90 }); + + const body = ensureExists(document.body); + + // Move right + fireEvent.mouseMove(body, { clientX: 100 }); + expect(firstColumn).toHaveStyle({ width: '100px' }); + + // Move left + fireEvent.mouseMove(body, { clientX: 80 }); + expect(firstColumn).toHaveStyle({ width: '80px' }); + + // Release the mouse -> still the same style + fireEvent.mouseUp(body); + expect(firstColumn).toHaveStyle({ width: '80px' }); + + // Now we'll unmount and render again. + unmount(); + render( + + + + ); + + // Make sure the first column kept its width + firstColumn = screen.getByText('Start'); + expect(firstColumn).toHaveStyle({ width: '80px' }); + + // Now double click to reset the style. + dividerForFirstColumn = ensureExists( + document.querySelector('.treeViewColumnDivider') + ); + fireEvent.dblClick(dividerForFirstColumn, { detail: 2 }); + expect(firstColumn).toHaveStyle({ width: '90px' }); + }); + }); }); function getReflowMarker( diff --git a/src/test/components/SourceView.test.js b/src/test/components/SourceView.test.js index 407649b1d2..cc9c337bc1 100644 --- a/src/test/components/SourceView.test.js +++ b/src/test/components/SourceView.test.js @@ -101,6 +101,7 @@ describe('SourceView', () => { const { sourceView } = setup(); const frameElement = screen.getByRole('treeitem', { name: /^A/ }); + fireFullClick(frameElement); fireFullClick(frameElement, { detail: 2 }); expect(sourceView()).toBeInTheDocument(); diff --git a/src/test/components/__snapshots__/MarkerTable.test.js.snap b/src/test/components/__snapshots__/MarkerTable.test.js.snap index a46aaa6901..cb1fe477d0 100644 --- a/src/test/components/__snapshots__/MarkerTable.test.js.snap +++ b/src/test/components/__snapshots__/MarkerTable.test.js.snap @@ -88,19 +88,34 @@ exports[`MarkerTable renders some basic markers and updates when needed 1`] = ` > Start + Duration + Type + @@ -136,22 +151,34 @@ exports[`MarkerTable renders some basic markers and updates when needed 1`] = ` > 0.000s + 0s + UserTiming +
0.002s + + Paint +
0.108s + unknown + IPC +
0.153s + 584.00ns + Text +
0.153s + 584.00ns + Text +
0.158s + + Log +
0.162s + 1ms + FileIO +
diff --git a/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap b/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap index 53f3829e06..25b2329a40 100644 --- a/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap +++ b/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap @@ -150,9 +150,11 @@ exports[`ProfileCallTreeView with JS Allocations matches the snapshot for JS all > Total Size (bytes) + Self (bytes) + + 100% 15 + +
+
100% 15 + +
+
80% 12 + +
+
80% 12 + 5 +
+
47% 7 + +
+
47% 7 + 7 +
+
20% 3 + +
+
@@ -821,9 +928,11 @@ exports[`ProfileCallTreeView with balanced native allocations matches the snapsh > Total Size (bytes) + Self (bytes) + + -100% -15 + +
+
-100% -15 + +
+
-80% -12 + +
+
-80% -12 + -5 +
+
-47% -7 + +
+
-47% -7 + -7 +
+
-20% -3 + +
+
@@ -1492,9 +1706,11 @@ exports[`ProfileCallTreeView with balanced native allocations matches the snapsh > Total Size (bytes) + Self (bytes) + + 100% 41 + +
+
100% 41 + +
+
73% 30 + +
+
73% 30 + 13 +
+
41% 17 + +
+
41% 17 + 17 +
+
27% 11 + +
+
@@ -2151,9 +2472,11 @@ exports[`ProfileCallTreeView with unbalanced native allocations matches the snap > Total Size (bytes) + Self (bytes) + + 100% 15 + +
+
100% 15 + +
+
80% 12 + +
+
80% 12 + 5 +
+
47% 7 + +
+
47% 7 + 7 +
+
20% 3 + +
+
@@ -2810,9 +3238,11 @@ exports[`ProfileCallTreeView with unbalanced native allocations matches the snap > Total Size (bytes) + Self (bytes) + + -100% -41 + +
+
-100% -41 + +
+
-59% -24 + +
+
-41% -17 + +
+
-41% -17 + -17 +
+
@@ -3765,9 +4274,11 @@ exports[`calltree/ProfileCallTreeView renders an inverted call tree 1`] = ` > Total (samples) + Self + + 67% 2 + 2 +
+
67% 2 + +
+
67% 2 + +
+
33% 1 + +
+
33% 1 + +
+
33% 1 + +
+
33% 1 + 1 +
+
@@ -4391,9 +5007,11 @@ exports[`calltree/ProfileCallTreeView renders an unfiltered call tree 1`] = ` > Total (samples) + Self + + 100% 3 + +
+
100% 3 + +
+
67% 2 + +
+
33% 1 + +
+
33% 1 + 1 +
+
33% 1 + +
+
33% 1 + +
+
@@ -5017,9 +5740,11 @@ exports[`calltree/ProfileCallTreeView renders an unfiltered call tree with filen > Total (samples) + Self + + 100% 1 + +
+
100% 1 + +
+
100% 1 + +
+
100% 1 + 1 +
+
@@ -5466,9 +6257,11 @@ exports[`calltree/ProfileCallTreeView renders call tree with some search strings > Total (samples) + Self + + 100% 3 + +
+
100% 3 + +
+
67% 2 + +
+
33% 1 + +
+
33% 1 + 1 +
+
33% 1 + +
+
33% 1 + +
+
@@ -6092,9 +6990,11 @@ exports[`calltree/ProfileCallTreeView renders call tree with some search strings > Total (samples) + Self + + 100% 2 + +
+
100% 2 + +
+
100% 2 + +
+
50% 1 + +
+
50% 1 + 1 +
+
50% 1 + +
+
@@ -6659,9 +7651,11 @@ exports[`calltree/ProfileCallTreeView renders call tree with some search strings > Total (samples) + Self + + 100% 2 + +
+
100% 2 + +
+
100% 2 + +
+
50% 1 + +
+
50% 1 + 1 +
+
50% 1 + +
+
@@ -7226,9 +8312,11 @@ exports[`calltree/ProfileCallTreeView renders call tree with some search strings > Total (samples) + Self + + 100% 1 + +
+
100% 1 + +
+
100% 1 + +
+
100% 1 + +
+
@@ -7676,9 +8830,11 @@ exports[`calltree/ProfileCallTreeView renders call tree with some search strings > Total (samples) + Self + + 100% 1 + +
+
100% 1 + +
+
100% 1 + +
+
100% 1 + +
+
@@ -8126,9 +9348,11 @@ exports[`calltree/ProfileCallTreeView renders call tree with some search strings > Total (samples) + Self + + 100% 2 + +
+
100% 2 + +
+
100% 2 + +
+
50% 1 + +
+
50% 1 + 1 +
+
50% 1 + +
+
diff --git a/src/types/actions.js b/src/types/actions.js index fbdc6ca10a..599d5b3b72 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -37,6 +37,7 @@ import type { State, UploadedProfileInformation, SourceLoadingError, + TableViewOptions, } from './state'; import type { CssPixels, StartEndRange, Milliseconds } from './units'; import type { BrowserConnectionStatus } from '../app-logic/browser-connection'; @@ -503,6 +504,11 @@ type UrlStateAction = +type: 'CHANGE_MOUSE_TIME_POSITION', +mouseTimePosition: Milliseconds | null, |} + | {| + +type: 'CHANGE_TABLE_VIEW_OPTIONS', + +tab: TabSlug, + +tableViewOptions: TableViewOptions, + |} | {| +type: 'TOGGLE_RESOURCES_PANEL', +selectedThreadIndexes: Set, diff --git a/src/types/state.js b/src/types/state.js index 9cd8fb2a23..6c423b4984 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -57,6 +57,12 @@ export type ThreadViewOptions = {| export type ThreadViewOptionsPerThreads = { [ThreadsKey]: ThreadViewOptions }; +export type TableViewOptions = {| + +fixedColumnWidths: Array | null, +|}; + +export type TableViewOptionsPerTab = { [TabSlug]: TableViewOptions }; + export type RightClickedCallNode = {| +threadsKey: ThreadsKey, +callNodePath: CallNodePath, @@ -108,6 +114,7 @@ export type ProfileViewState = { rightClickedMarker: MarkerReference | null, hoveredMarker: MarkerReference | null, mouseTimePosition: Milliseconds | null, + perTab: TableViewOptionsPerTab, |}, +profile: Profile | null, +full: FullProfileViewState,