From 92bd56edf4acea2f70c6406934bdbeaae4b5c6cc Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 4 May 2023 15:12:37 -0400 Subject: [PATCH 1/2] Replace SplitterLayout with a new ResizableWithSplitter component. This replacement has the following advantages: It's compatible with React 18, and it supports making "max-height" the controlled property, which is useful for our timeline splitter. In the past, we needed to compute the height of the timeline contents, and then make it so you couldn't move the splitter down more than the full timeline height, because we didn't want there to be a gap above the splitter, or even initialize the splitter at a position that creates such a gap. By making the splitter control max-height instead, even if the splitter is dragged low enough to say max-height:1000px, the splitter won't actually move that low because the timeline won't grow beyond its full size (because there's no longer a height property forcing it to grow). --- package.json | 1 - res/css/global.css | 12 -- src/components/app/BottomBox.css | 15 +- src/components/app/BottomBox.tsx | 80 ++++----- src/components/app/Details.css | 5 +- src/components/app/DetailsContainer.css | 31 ++-- src/components/app/DetailsContainer.tsx | 23 ++- src/components/app/ProfileViewer.css | 19 +-- src/components/app/ProfileViewer.tsx | 37 ++-- .../shared/ResizableWithSplitter.css | 34 ++++ .../shared/ResizableWithSplitter.tsx | 159 ++++++++++++++++++ .../timeline/OverflowEdgeIndicator.css | 1 + .../timeline/OverflowEdgeIndicator.tsx | 5 +- src/components/timeline/Selection.css | 1 + src/components/timeline/index.css | 8 +- src/index.tsx | 1 - .../__snapshots__/Timeline.test.tsx.snap | 4 +- yarn.lock | 36 +++- 18 files changed, 339 insertions(+), 133 deletions(-) create mode 100644 src/components/shared/ResizableWithSplitter.css create mode 100644 src/components/shared/ResizableWithSplitter.tsx diff --git a/package.json b/package.json index 67ff4c974e..fa3ece6a92 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "react-dom": "^18.3.1", "react-intersection-observer": "^10.0.3", "react-redux": "^9.2.0", - "react-splitter-layout": "^4.0.0", "redux": "^5.0.1", "redux-logger": "^3.0.6", "redux-thunk": "^3.1.0", diff --git a/res/css/global.css b/res/css/global.css index 5353f161f0..6f8247df9c 100644 --- a/res/css/global.css +++ b/res/css/global.css @@ -184,15 +184,3 @@ a:active { .colored-border.ellipsis { opacity: 0; } - -.splitter-layout.splitter-layout > .layout-splitter { - background-color: var(--wide-splitter-color); -} - -.splitter-layout.splitter-layout > .layout-splitter:hover { - background-color: var(--wide-splitter-hover-color); -} - -.splitter-layout.splitter-layout.layout-changing > .layout-splitter { - background-color: var(--wide-splitter-pressed-color); -} diff --git a/src/components/app/BottomBox.css b/src/components/app/BottomBox.css index dc80d0345b..29e7afb40c 100644 --- a/src/components/app/BottomBox.css +++ b/src/components/app/BottomBox.css @@ -2,13 +2,21 @@ * 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/. */ +.bottom-box { + display: flex; + min-height: 0; + flex-flow: row nowrap; +} + .bottom-box-pane { --internal-sourceview-background-color: var(--grey-20); --internal-close-icon: url(../../../res/img/svg/close-dark.svg); --internal-assembly-icon: url(../../../res/img/svg/asm-icon.svg); display: flex; - height: 100%; /* direct child of SplitterLayout */ + overflow: hidden; + min-width: 0; + flex: 1; flex-flow: column nowrap; color: var(--base-foreground-color); } @@ -23,7 +31,7 @@ background: var(--internal-sourceview-background-color); } -.bottom-box .layout-splitter { +.bottom-box .resizableWithSplitterSplitter { position: relative; /* containing block for absolute ::before */ width: 1px; border: none; @@ -31,7 +39,7 @@ } /* Provide 3px extra grabbable surface on each side of the splitter */ -.bottom-box .layout-splitter::before { +.bottom-box .resizableWithSplitterSplitter::before { position: absolute; z-index: var(--z-bottom-box); display: block; @@ -44,6 +52,7 @@ overflow: hidden; height: 24px; flex-flow: row; + flex-shrink: 0; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--panel-border-color); diff --git a/src/components/app/BottomBox.tsx b/src/components/app/BottomBox.tsx index 96558f3b9c..0534b6c43f 100644 --- a/src/components/app/BottomBox.tsx +++ b/src/components/app/BottomBox.tsx @@ -3,11 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from 'react'; -import SplitterLayout from 'react-splitter-layout'; import classNames from 'classnames'; import { SourceView } from '../shared/SourceView'; import { AssemblyView } from '../shared/AssemblyView'; +import { ResizableWithSplitter } from '../shared/ResizableWithSplitter'; import { AssemblyViewToggleButton } from './AssemblyViewToggleButton'; import { AssemblyViewNativeSymbolNavigator } from './AssemblyViewNativeSymbolNavigator'; import { IonGraphView } from '../shared/IonGraphView'; @@ -217,43 +217,47 @@ class BottomBoxImpl extends React.PureComponent { return (
- -
-
-

{path ?? '(no source file)'}

- {assemblyViewIsOpen ? null : trailingHeaderButtons} -
-
- {displayIonGraph ? ( - - ) : null} - {displaySourceView ? ( - - ) : null} - {sourceViewCode !== undefined && - sourceViewCode.type === 'LOADING' ? ( - - ) : null} - {sourceViewCode !== undefined && - sourceViewCode.type === 'ERROR' ? ( - - ) : null} -
+
+
+

{path ?? '(no source file)'}

+ {assemblyViewIsOpen ? null : trailingHeaderButtons} +
+
+ {displayIonGraph ? ( + + ) : null} + {displaySourceView ? ( + + ) : null} + {sourceViewCode !== undefined && + sourceViewCode.type === 'LOADING' ? ( + + ) : null} + {sourceViewCode !== undefined && sourceViewCode.type === 'ERROR' ? ( + + ) : null}
+
- {assemblyViewIsOpen ? ( + {assemblyViewIsOpen ? ( +

@@ -289,8 +293,8 @@ class BottomBoxImpl extends React.PureComponent { ) : null}

- ) : null} - +
+ ) : null}
); } diff --git a/src/components/app/Details.css b/src/components/app/Details.css index ed1cafd550..43a051246c 100644 --- a/src/components/app/Details.css +++ b/src/components/app/Details.css @@ -3,8 +3,11 @@ --internal-sidebar-expand-icon: url(../../../res/img/svg/pane-expand.svg); display: flex; + + /* If .Details is squished to a very small size between the sidebar and/or bottom bar, + don't spill out our contents over those other elements. */ overflow: clip; - flex: auto; + flex: 1; flex-direction: column; } diff --git a/src/components/app/DetailsContainer.css b/src/components/app/DetailsContainer.css index 21bcae24d1..78bd724119 100644 --- a/src/components/app/DetailsContainer.css +++ b/src/components/app/DetailsContainer.css @@ -1,26 +1,29 @@ -.DetailsContainer .layout-pane > * { - width: 100%; - height: 100%; +.DetailsContainer { + position: relative; + z-index: 0; + display: flex; box-sizing: border-box; + flex: 1; + flex-flow: row nowrap; + contain: size; } -.DetailsContainer .layout-pane:not(.layout-pane-primary) { - max-width: 600px; +.DetailsContainer .resizableWithSplitterInner > * { + min-width: 0; + flex: 1; } -/* overriding defaults from splitter-layout */ -.DetailsContainer { - /* SplitterLayout injects position: absolute, this conflicts with our using - * Flex Layout. */ - position: unset; +.DetailsContainerResizableSidebarWrapper { + max-width: 600px; } -.DetailsContainer.splitter-layout > .layout-splitter { +/* overriding defaults from ResizableWithSplitter.css */ +.DetailsContainer .resizableWithSplitterSplitter { border-top: 1px solid var(--panel-border-color); border-left: 1px solid var(--panel-border-color); - background: var(--panel-background-color); + background: var(--panel-background-color); /* Same background as sidebars */ } -.DetailsContainer.splitter-layout > .layout-splitter:hover { - background: var(--panel-background-color); +.DetailsContainer .resizableWithSplitterSplitter:hover { + background: var(--panel-background-color); /* same as the border above */ } diff --git a/src/components/app/DetailsContainer.tsx b/src/components/app/DetailsContainer.tsx index b1fe74f00d..879af498da 100644 --- a/src/components/app/DetailsContainer.tsx +++ b/src/components/app/DetailsContainer.tsx @@ -1,10 +1,9 @@ /* 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/. */ -// React imported for JSX -import SplitterLayout from 'react-splitter-layout'; import { Details } from './Details'; +import { ResizableWithSplitter } from 'firefox-profiler/components/shared/ResizableWithSplitter'; import { selectSidebar } from 'firefox-profiler/components/sidebar'; import { getSelectedTab } from 'firefox-profiler/selectors/url-state'; @@ -27,14 +26,20 @@ function DetailsContainerImpl({ selectedTab, isSidebarOpen }: Props) { const Sidebar = selectSidebar(selectedTab); return ( - +
- {Sidebar && isSidebarOpen ? : null} - + {Sidebar && isSidebarOpen ? ( + + + + ) : null} +
); } diff --git a/src/components/app/ProfileViewer.css b/src/components/app/ProfileViewer.css index 1fdf52b78f..447647b6a1 100644 --- a/src/components/app/ProfileViewer.css +++ b/src/components/app/ProfileViewer.css @@ -47,6 +47,10 @@ } .profileViewer { + /* Create a stacking context, so that the KeyboardShortcut can overlay the profile + viewer. */ + position: relative; + z-index: 0; display: flex; min-width: 0; /* This allows Flexible Layout to shrink this further than its min-content */ flex: 1; @@ -109,21 +113,6 @@ } } -.profileViewerSplitter { - /* Position relative for layout. Don't set z-index here to avoid creating a - stacking context that would trap the context menu below the screenshot hover. - This allows both the screenshot hover and context menu to be compared - at the .profileViewer level, ensuring the context menu appears on top. */ - position: relative; -} - -.profileViewerSplitter > .layout-pane:not(.layout-pane-primary) { - display: flex; - overflow: hidden; - max-height: var(--profile-viewer-splitter-max-height); - flex-direction: column; -} - .profileViewerTopBar { display: flex; height: 24px; diff --git a/src/components/app/ProfileViewer.tsx b/src/components/app/ProfileViewer.tsx index bc140e3af4..6db180df4b 100644 --- a/src/components/app/ProfileViewer.tsx +++ b/src/components/app/ProfileViewer.tsx @@ -5,6 +5,7 @@ import { PureComponent } from 'react'; import explicitConnect from 'firefox-profiler/utils/connect'; +import { ResizableWithSplitter } from 'firefox-profiler/components/shared/ResizableWithSplitter'; import { DetailsContainer } from './DetailsContainer'; import { SourceCodeFetcher } from './SourceCodeFetcher'; import { AssemblyCodeFetcher } from './AssemblyCodeFetcher'; @@ -20,7 +21,6 @@ import { KeyboardShortcut } from './KeyboardShortcut'; import { returnToZipFileList } from 'firefox-profiler/actions/zipped-profiles'; import { Timeline } from 'firefox-profiler/components/timeline'; import { getHasZipFile } from 'firefox-profiler/selectors/zipped-profiles'; -import SplitterLayout from 'react-splitter-layout'; import { getTimelineHeight } from 'firefox-profiler/selectors/app'; import { getIsBottomBoxOpen } from 'firefox-profiler/selectors/url-state'; import { @@ -125,28 +125,25 @@ class ProfileViewerImpl extends PureComponent { /> ) : null}
- - + + {isBottomBoxOpen ? ( + - - {isBottomBoxOpen ? : null} - - + + + ) : null}
diff --git a/src/components/shared/ResizableWithSplitter.css b/src/components/shared/ResizableWithSplitter.css new file mode 100644 index 0000000000..af0ed0918d --- /dev/null +++ b/src/components/shared/ResizableWithSplitter.css @@ -0,0 +1,34 @@ +.resizableWithSplitterOuter, +.resizableWithSplitterInner { + display: flex; + min-width: 0; + min-height: 0; + flex-direction: inherit; +} + +.resizableWithSplitterInner { + flex: 1; +} + +.resizableWithSplitterSplitter { + flex-shrink: 0; + background-color: var(--wide-splitter-color); +} + +.resizableWithSplitterSplitter:hover { + background-color: var(--wide-splitter-hover-color); +} + +.resizableWithSplitterSplitter.dragging { + background-color: var(--wide-splitter-pressed-color); +} + +.resizableWithSplitterSplitter.resizesWidth { + width: 5px; + cursor: col-resize; +} + +.resizableWithSplitterSplitter.resizesHeight { + height: 5px; + cursor: row-resize; +} diff --git a/src/components/shared/ResizableWithSplitter.tsx b/src/components/shared/ResizableWithSplitter.tsx new file mode 100644 index 0000000000..abbb1f89c9 --- /dev/null +++ b/src/components/shared/ResizableWithSplitter.tsx @@ -0,0 +1,159 @@ +/* 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/. */ + +import * as React from 'react'; +import classNames from 'classnames'; + +import type { CssPixels } from '../../types/units'; + +import './ResizableWithSplitter.css'; + +type Props = { + // If 'start', the splitter is placed before the resized element, otherwise after. + splitterPosition: 'start' | 'end'; + // The CSS property which gets changed if the splitter is dragged. + controlledProperty: + | 'width' + | 'height' + | 'min-width' + | 'min-height' + | 'max-width' + | 'max-height'; + // The initial size, as a valid CSS length. For example "200px" or "30%". + // This prop is read only once, during the initial render. + initialSize: string; + // True if the value for controlledProperty should be set as a percentage, + // false if it should be set in CSS "px". + percent: boolean; + // An extra className for the outer div. + className?: string; + // The sized contents. These are placed inside an element with the className + // "resizableWithSplitterInner" and should have a non-zero CSS "flex" value + // set on them, so that they resize when "resizableWithSplitterInner" resizes. + children: React.ReactNode; +}; + +type DragState = { + outerDim: CssPixels; + parentDim: CssPixels; + isWidth: boolean; + splitterPosition: 'start' | 'end'; + percent: boolean; + startX: number; + startY: number; +}; + +export function ResizableWithSplitter({ + splitterPosition, + controlledProperty, + initialSize, + percent, + className, + children, +}: Props) { + const [size, setSize] = React.useState(initialSize); + const [dragging, setDragging] = React.useState(false); + const dragState = React.useRef(null); + const isWidth = controlledProperty.endsWith('width'); + const orientClassName = isWidth ? 'resizesWidth' : 'resizesHeight'; + + const onPointerDown = React.useCallback( + (e: React.PointerEvent) => { + if (e.button !== 0) { + return; + } + e.stopPropagation(); + e.preventDefault(); + e.currentTarget.setPointerCapture(e.pointerId); + const outer = e.currentTarget.parentElement; + const parent = outer?.parentElement; + if (!outer || !parent) { + return; + } + const outerRect = outer.getBoundingClientRect(); + const parentRect = parent.getBoundingClientRect(); + dragState.current = { + outerDim: isWidth ? outerRect.width : outerRect.height, + parentDim: isWidth ? parentRect.width : parentRect.height, + isWidth, + splitterPosition, + percent, + startX: e.pageX, + startY: e.pageY, + }; + setDragging(true); + }, + [isWidth, splitterPosition, percent] + ); + + const applyDrag = React.useCallback( + (ds: DragState, pageX: number, pageY: number) => { + const delta = ds.isWidth ? pageX - ds.startX : pageY - ds.startY; + const adjusted = Math.max( + 0, + ds.splitterPosition === 'end' + ? ds.outerDim + delta + : ds.outerDim - delta + ); + setSize( + ds.percent + ? `${((adjusted / ds.parentDim) * 100).toFixed(2)}%` + : `${adjusted}px` + ); + }, + [] + ); + + const onPointerMove = React.useCallback( + (e: React.PointerEvent) => { + const ds = dragState.current; + if (!ds) { + return; + } + applyDrag(ds, e.pageX, e.pageY); + }, + [applyDrag] + ); + + const onPointerUpOrCancel = React.useCallback( + (e: React.PointerEvent) => { + const ds = dragState.current; + if (!ds) { + return; + } + applyDrag(ds, e.pageX, e.pageY); + dragState.current = null; + setDragging(false); + }, + [applyDrag] + ); + + const splitter = ( +
+ ); + + return ( +
+ {splitterPosition === 'start' ? splitter : null} +
{children}
+ {splitterPosition === 'end' ? splitter : null} +
+ ); +} diff --git a/src/components/timeline/OverflowEdgeIndicator.css b/src/components/timeline/OverflowEdgeIndicator.css index e4aa0a11b5..7725c056ac 100644 --- a/src/components/timeline/OverflowEdgeIndicator.css +++ b/src/components/timeline/OverflowEdgeIndicator.css @@ -6,6 +6,7 @@ position: relative; display: flex; width: calc(100% + var(--vertical-scrollbar-reserved-width)); + min-height: 0; flex: 1; flex-direction: column; } diff --git a/src/components/timeline/OverflowEdgeIndicator.tsx b/src/components/timeline/OverflowEdgeIndicator.tsx index b73d562f21..293d7bb292 100644 --- a/src/components/timeline/OverflowEdgeIndicator.tsx +++ b/src/components/timeline/OverflowEdgeIndicator.tsx @@ -112,10 +112,7 @@ class OverflowEdgeIndicator extends React.PureComponent {
diff --git a/src/components/timeline/Selection.css b/src/components/timeline/Selection.css index 47c1bbb72c..9d749915a1 100644 --- a/src/components/timeline/Selection.css +++ b/src/components/timeline/Selection.css @@ -27,6 +27,7 @@ position: relative; display: flex; width: var(--content-width); + min-height: 0; flex: 1; flex-direction: column; margin-left: var(--thread-label-column-width); diff --git a/src/components/timeline/index.css b/src/components/timeline/index.css index 5abd986b91..caa50149b2 100644 --- a/src/components/timeline/index.css +++ b/src/components/timeline/index.css @@ -2,16 +2,15 @@ * 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/. */ -.timelineOverflowEdgeIndicatorScrollbox { +.timelineOverflowEdgeIndicator > .overflowEdgeIndicatorScrollbox { --internal-background-stop1-color: #eee; --internal-background-stop2-color: var(--grey-30); - position: absolute; + position: relative; /* if the platform scrollbar ends up being larger than --vertical-scrollbar-reserved-width (which is unexpected), make sure there is no horizontal scrollbar */ overflow: hidden auto; width: 100%; - height: 100%; padding-left: var(--thread-label-column-width); margin-right: calc(var(--vertical-scrollbar-reserved-width) * -1); margin-left: calc(var(--thread-label-column-width) * -1); @@ -125,7 +124,6 @@ /* Moving the timeline 1 pixel below the timeline header. This way, the first * visible track's top border will be hidden as expected. */ margin-top: -1px; - margin-bottom: -1px; } @media (forced-colors: active) { @@ -150,7 +148,7 @@ } :root.dark-mode { - .timelineOverflowEdgeIndicatorScrollbox { + .timelineOverflowEdgeIndicator > .overflowEdgeIndicatorScrollbox { --internal-background-stop1-color: var(--grey-80); --internal-background-stop2-color: var(--grey-70); } diff --git a/src/index.tsx b/src/index.tsx index 30658b2910..45a56f62c5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,6 @@ import '../res/css/style.css'; import '../res/css/global.css'; import '../res/css/categories.css'; import '../res/css/network.css'; -import 'react-splitter-layout/lib/index.css'; import 'devtools-reps/reps.css'; // React imported for JSX in Root component diff --git a/src/test/components/__snapshots__/Timeline.test.tsx.snap b/src/test/components/__snapshots__/Timeline.test.tsx.snap index 4c3ac88f95..204cca2f27 100644 --- a/src/test/components/__snapshots__/Timeline.test.tsx.snap +++ b/src/test/components/__snapshots__/Timeline.test.tsx.snap @@ -51,7 +51,7 @@ exports[`TimelineSelection does not crash when there are no samples or markers 1 class="overflowEdgeIndicatorEdge leftEdge" />
Date: Fri, 10 Apr 2026 11:57:09 -0400 Subject: [PATCH 2/2] Remove timeline height computation. In the past this was used to prevent the splitter from creating a gap between the timeline contents and the splitter. But these gaps only appeared because the splitter set a height. Now it only sets a max-height so the gaps don't appear. --- src/components/app/ProfileViewer.tsx | 13 +- src/selectors/app.tsx | 148 +-------------------- src/test/components/ProfileViewer.test.tsx | 71 ---------- 3 files changed, 2 insertions(+), 230 deletions(-) delete mode 100644 src/test/components/ProfileViewer.test.tsx diff --git a/src/components/app/ProfileViewer.tsx b/src/components/app/ProfileViewer.tsx index 6db180df4b..71d835d377 100644 --- a/src/components/app/ProfileViewer.tsx +++ b/src/components/app/ProfileViewer.tsx @@ -21,7 +21,6 @@ import { KeyboardShortcut } from './KeyboardShortcut'; import { returnToZipFileList } from 'firefox-profiler/actions/zipped-profiles'; import { Timeline } from 'firefox-profiler/components/timeline'; import { getHasZipFile } from 'firefox-profiler/selectors/zipped-profiles'; -import { getTimelineHeight } from 'firefox-profiler/selectors/app'; import { getIsBottomBoxOpen } from 'firefox-profiler/selectors/url-state'; import { getUploadProgress, @@ -35,14 +34,13 @@ import { BackgroundImageStyleDef } from 'firefox-profiler/components/shared/Styl import classNames from 'classnames'; import { DebugWarning } from 'firefox-profiler/components/app/DebugWarning'; -import type { CssPixels, IconsWithClassNames } from 'firefox-profiler/types'; +import type { IconsWithClassNames } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; import './ProfileViewer.css'; type StateProps = { readonly hasZipFile: boolean; - readonly timelineHeight: CssPixels | null; readonly uploadProgress: number; readonly isUploading: boolean; readonly isHidingStaleProfile: boolean; @@ -62,7 +60,6 @@ class ProfileViewerImpl extends PureComponent { const { hasZipFile, returnToZipFileList, - timelineHeight, isUploading, uploadProgress, isHidingStaleProfile, @@ -92,13 +89,6 @@ class ProfileViewerImpl extends PureComponent { hasSanitizedProfile && !isHidingStaleProfile, profileViewerFadeOut: isHidingStaleProfile, })} - style={ - timelineHeight === null - ? {} - : ({ - '--profile-viewer-splitter-max-height': `${timelineHeight}px`, - } as React.CSSProperties) - } >
{hasZipFile ? ( @@ -160,7 +150,6 @@ class ProfileViewerImpl extends PureComponent { export const ProfileViewer = explicitConnect<{}, StateProps, DispatchProps>({ mapStateToProps: (state) => ({ hasZipFile: getHasZipFile(state), - timelineHeight: getTimelineHeight(state), uploadProgress: getUploadProgress(state), isUploading: getUploadPhase(state) === 'uploading', isHidingStaleProfile: getIsHidingStaleProfile(state), diff --git a/src/selectors/app.tsx b/src/selectors/app.tsx index ec78760d84..0501ee6608 100644 --- a/src/selectors/app.tsx +++ b/src/selectors/app.tsx @@ -3,28 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { createSelector } from 'reselect'; -import { - getDataSource, - getSelectedTab, - getHiddenGlobalTracks, - getHiddenLocalTracksByPid, -} from './url-state'; -import { getGlobalTracks, getLocalTracksByPid } from './profile'; +import { getDataSource, getSelectedTab } from './url-state'; import { getZipFileState } from './zipped-profiles'; -import { assertExhaustiveCheck, ensureExists } from '../utils/types'; -import { - FULL_TRACK_SCREENSHOT_HEIGHT, - TRACK_NETWORK_HEIGHT, - TRACK_MEMORY_HEIGHT, - TRACK_BANDWIDTH_HEIGHT, - TRACK_IPC_HEIGHT, - TRACK_PROCESS_BLANK_HEIGHT, - TIMELINE_RULER_HEIGHT, - TRACK_VISUAL_PROGRESS_HEIGHT, - TRACK_EVENT_DELAY_HEIGHT, - TRACK_PROCESS_CPU_HEIGHT, - TRACK_MARKER_HEIGHT, -} from '../app-logic/constants'; import type { AppState, @@ -35,7 +15,6 @@ import type { ThreadsKey, ExperimentalFlags, UploadedProfileInformation, - Pid, } from 'firefox-profiler/types'; import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; import type { @@ -88,131 +67,6 @@ export const getCurrentProfileUploadedInformation: Selector< UploadedProfileInformation | null > = (state) => getApp(state).currentProfileUploadedInformation; -/** - * This selector takes all of the tracks, and deduces the height in CssPixels - * of the timeline. This is here to calculate the max-height of the timeline - * for the splitter component. - * - * The height of the component is determined by the sizing of each track in the list. - * Most sizes are pretty static, and are set through values in the component. The only - * tricky value to determine is the thread track. These values get reported to the store - * and get added in here. - */ -export const getTimelineHeight: Selector = createSelector( - getGlobalTracks, - getLocalTracksByPid, - getHiddenGlobalTracks, - getHiddenLocalTracksByPid, - getTrackThreadHeights, - ( - globalTracks, - localTracksByPid, - hiddenGlobalTracks, - hiddenLocalTracksByPid, - trackThreadHeights - ) => { - let height = TIMELINE_RULER_HEIGHT; - const border = 1; - for (const [trackIndex, globalTrack] of globalTracks.entries()) { - if (!hiddenGlobalTracks.has(trackIndex)) { - switch (globalTrack.type) { - case 'screenshots': - height += FULL_TRACK_SCREENSHOT_HEIGHT + border; - break; - case 'visual-progress': - case 'perceptual-visual-progress': - case 'contentful-visual-progress': - height += TRACK_VISUAL_PROGRESS_HEIGHT; - break; - case 'process': { - // The thread tracks have enough complexity that it warrants measuring - // them rather than statically using a value like the other tracks. - const { mainThreadIndex } = globalTrack; - if (mainThreadIndex === null) { - height += TRACK_PROCESS_BLANK_HEIGHT + border; - } else { - const trackThreadHeight = trackThreadHeights[mainThreadIndex]; - if (trackThreadHeight === undefined) { - // The height isn't computed yet, return. - return null; - } - height += trackThreadHeight + border; - } - break; - } - default: - throw assertExhaustiveCheck(globalTrack); - } - } - } - - // Figure out which PIDs are hidden. - const hiddenPids = new Set(); - for (const trackIndex of hiddenGlobalTracks) { - const globalTrack = globalTracks[trackIndex]; - if (globalTrack.type === 'process') { - hiddenPids.add(globalTrack.pid); - } - } - - for (const [pid, localTracks] of localTracksByPid) { - if (hiddenPids.has(pid)) { - // This track is hidden already. - continue; - } - for (const [trackIndex, localTrack] of localTracks.entries()) { - const hiddenLocalTracks = ensureExists( - hiddenLocalTracksByPid.get(pid), - 'Could not look up the hidden local tracks from the given PID' - ); - if (!hiddenLocalTracks.has(trackIndex)) { - switch (localTrack.type) { - case 'thread': - { - // The thread tracks have enough complexity that it warrants measuring - // them rather than statically using a value like the other tracks. - const trackThreadHeight = - trackThreadHeights[localTrack.threadIndex]; - if (trackThreadHeight === undefined) { - // The height isn't computed yet, return. - return null; - } - height += trackThreadHeight + border; - } - - break; - case 'network': - height += TRACK_NETWORK_HEIGHT + border; - break; - case 'memory': - height += TRACK_MEMORY_HEIGHT + border; - break; - case 'bandwidth': - height += TRACK_BANDWIDTH_HEIGHT + border; - break; - case 'event-delay': - height += TRACK_EVENT_DELAY_HEIGHT + border; - break; - case 'ipc': - height += TRACK_IPC_HEIGHT + border; - break; - case 'process-cpu': - case 'power': - height += TRACK_PROCESS_CPU_HEIGHT + border; - break; - case 'marker': - height += TRACK_MARKER_HEIGHT + border; - break; - default: - throw assertExhaustiveCheck(localTrack); - } - } - } - } - return height; - } -); - /** * This selector lets us know if it is safe to load a new profile. If * the app is already busy loading a profile, this selector returns diff --git a/src/test/components/ProfileViewer.test.tsx b/src/test/components/ProfileViewer.test.tsx deleted file mode 100644 index e5fba9e71f..0000000000 --- a/src/test/components/ProfileViewer.test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* 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/. */ -import ReactDOM from 'react-dom'; -import { Provider } from 'react-redux'; - -import { render } from 'firefox-profiler/test/fixtures/testing-library'; -import { ProfileViewer } from 'firefox-profiler/components/app/ProfileViewer'; -import { getTimelineHeight } from 'firefox-profiler/selectors/app'; -import { updateUrlState } from 'firefox-profiler/actions/app'; -import { viewProfile } from 'firefox-profiler/actions/receive-profile'; -import { stateFromLocation } from 'firefox-profiler/app-logic/url-handling'; - -import { blankStore } from '../fixtures/stores'; -import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; -import { autoMockCanvasContext } from '../fixtures/mocks/canvas-context'; -import { mockRaf } from '../fixtures/mocks/request-animation-frame'; -import { - autoMockElementSize, - getElementWithFixedSize, -} from '../fixtures/mocks/element-size'; -import { autoMockIntersectionObserver } from '../fixtures/mocks/intersection-observer'; - -describe('ProfileViewer', function () { - autoMockCanvasContext(); - autoMockElementSize({ width: 200, height: 300 }); - autoMockIntersectionObserver(); - - beforeEach(() => { - jest - .spyOn(ReactDOM, 'findDOMNode') - .mockImplementation(() => - getElementWithFixedSize({ width: 300, height: 300 }) - ); - }); - - function setup() { - // WithSize uses requestAnimationFrame - const flushRafCalls = mockRaf(); - - const store = blankStore(); - store.dispatch( - updateUrlState( - stateFromLocation({ - pathname: '/from-browser', - search: '', - hash: '', - }) - ) - ); - store.dispatch(viewProfile(getProfileWithNiceTracks())); - - const renderResult = render( - - - - ); - - // Flushing the requestAnimationFrame calls so we can see the actual height of tracks. - flushRafCalls(); - - return { ...renderResult, ...store }; - } - - it('calculates the full timeline height correctly', () => { - const { getState } = setup(); - - // Note: You should update this total height if you changed the height calculation algorithm. - expect(getTimelineHeight(getState())).toBe(1224); - }); -});