Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions res/.htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ Header set Referrer-Policy same-origin
# NOTE: you can't use the script file directly as the API modifies it before injecting.
# c. We use Google Analytics to track the usage of the application.
# 2. `unsafe-inline` in `style-src` is necessary to support favicons.
# 3. Same for `*` in `img-src`.
# 3. `img-src` defines rules to specifically allow http, https, and data URLs.
# 4. `object-src` is for plugins, we don't need them.
# 5. `connect-src` is `*` to support `from-url`. We also do requests to bitly to shorten URLs.
# 6. `frame-ancestors` is the same purpose as `X-Frame-Options` above.
# 7. `form-action`prevents forms, we don't need this.`
Header always add Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-eRTCQnd2fhPykpATDzCv4gdVk/EOdDq+6yzFXaWgGEw=' 'sha256-AdiT28wTL5FNaRVHWQVFC0ic3E20Gu4/PiC9xukS9+E=' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src *; object-src 'none'; connect-src *; frame-ancestors 'self'; form-action 'none'"
Header always add Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-eRTCQnd2fhPykpATDzCv4gdVk/EOdDq+6yzFXaWgGEw=' 'sha256-AdiT28wTL5FNaRVHWQVFC0ic3E20Gu4/PiC9xukS9+E=' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src http: https: data:; object-src 'none'; connect-src *; frame-ancestors 'self'; form-action 'none'"
4 changes: 2 additions & 2 deletions res/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
# NOTE: you can't use the script file directly as the API modifies it before injecting.
# c. We use Google Analytics to track the usage of the application.
# 2. `unsafe-inline` in `style-src` is necessary to support favicons.
# 3. Same for `*` in `img-src`.
# 3. `img-src` defines rules to specifically allow http, https, and data URLs.
# 4. `object-src` is for plugins, we don't need them.
# 5. `connect-src` is `*` to support `from-url`. We also do requests to bitly to shorten URLs.
# 6. `frame-ancestors` is the same purpose as `X-Frame-Options` above.
# 7. `form-action`prevents forms, we don't need this.`
# 8. `frame-src` allows the embedding of YouTube videos in the docs.
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-eRTCQnd2fhPykpATDzCv4gdVk/EOdDq+6yzFXaWgGEw=' 'sha256-AdiT28wTL5FNaRVHWQVFC0ic3E20Gu4/PiC9xukS9+E=' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src *; object-src 'none'; connect-src *; frame-ancestors 'self'; form-action 'none'; frame-src www.youtube-nocookie.com
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-eRTCQnd2fhPykpATDzCv4gdVk/EOdDq+6yzFXaWgGEw=' 'sha256-AdiT28wTL5FNaRVHWQVFC0ic3E20Gu4/PiC9xukS9+E=' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src http: https: data:; object-src 'none'; connect-src *; frame-ancestors 'self'; form-action 'none'; frame-src www.youtube-nocookie.com
1 change: 1 addition & 0 deletions res/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
</head>
<body style='background-color: #363959; /* ink-70 */'>
<div id="root"></div>
<div id="root-overlay"></div>
</body>
2 changes: 1 addition & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const serverConfig = {
'sha256-AdiT28wTL5FNaRVHWQVFC0ic3E20Gu4/PiC9xukS9+E='
https://www.google-analytics.com;
style-src 'self' 'unsafe-inline';
img-src *;
img-src http: https: data:;
object-src 'none';
connect-src *;
frame-ancestors 'self';
Expand Down
10 changes: 7 additions & 3 deletions src/components/timeline/GlobalTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '../../reducers/profile-view';
import './Track.css';
import TimelineTrackThread from './TrackThread';
import TimelineTrackScreenshots from './TrackScreenshots';
import TimelineLocalTrack from './LocalTrack';
import Reorderable from '../shared/Reorderable';
import type { TabSlug } from '../../app-logic/tabs-handling';
Expand Down Expand Up @@ -100,9 +101,12 @@ class GlobalTrackComponent extends PureComponent<Props> {
}
return <TimelineTrackThread threadIndex={mainThreadIndex} />;
}
case 'screenshots':
// TODO: Add support for screenshots.
return <div />;
case 'screenshots': {
const { threadIndex, id } = globalTrack;
return (
<TimelineTrackScreenshots threadIndex={threadIndex} windowId={id} />
);
}
default:
console.error('Unhandled globalTrack type', (globalTrack: empty));
return null;
Expand Down
44 changes: 44 additions & 0 deletions src/components/timeline/TrackScreenshots.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* 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/. */

.timelineTrackScreenshot {
position: relative;
background: var(--grey-20);
overflow: hidden;
}

.timelineTrackScreenshotImgContainer {
/* This positioning is defined through JavaScript */
position: absolute;
overflow: hidden;
display: flex;
justify-content: center;
}

.timelineTrackScreenshotImgContainer::after {
/* Create a border on the right of the container */
content: "";
position: absolute;
top: 0;
width: 1px;
height: 100%;
background: var(--grey-30);
right: 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wondering why you didn't use a border ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't showing up for some reason. I think it's somehow getting covered up by the sibling containers.


.timelineTrackScreenshotHover {
/* This positioning is defined through JavaScript */
position: absolute;
pointer-events: none;
z-index: 4; /* Ensure this is higher than any other timeline element. */
}

.timelineTrackScreenshotHoverImg {
position: relative;
left: -50%;
border-radius: 5px;
padding: 0.5px;
border: 0.5px solid rgba(0,0,0,.2);
box-shadow:0 2px 4px rgba(0,0,0,.2);
}
253 changes: 253 additions & 0 deletions src/components/timeline/TrackScreenshots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/* 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 React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';
import explicitConnect from '../../utils/connect';
import {
selectorsForThread,
getCommittedRange,
getPreviewSelection,
} from '../../reducers/profile-view';
import { withSize, type SizeProps } from '../shared/WithSize';

import type { ThreadIndex, Thread } from '../../types/profile';
import type { ScreenshotPayload } from '../../types/markers';
import type { TracingMarker } from '../../types/profile-derived';
import type { Milliseconds } from '../../types/units';
import type {
ExplicitConnectOptions,
ConnectedProps,
} from '../../utils/connect';

import { ensureExists } from '../../utils/flow';
import './TrackScreenshots.css';

type OwnProps = {|
+threadIndex: ThreadIndex,
+windowId: string,
...SizeProps,
|};
type StateProps = {|
+thread: Thread,
+rangeStart: Milliseconds,
+rangeEnd: Milliseconds,
+screenshots: TracingMarker[],
+threadName: string,
+isMakingPreviewSelection: boolean,
|};
type DispatchProps = {||};
type Props = ConnectedProps<OwnProps, StateProps, DispatchProps>;
type State = {|
offsetX: null | number,
pageX: null | number,
containerTop: null | number,
|};

// Export the value for tests.
export const TRACK_HEIGHT = 50;
const HOVER_HEIGHT = 100;
const HOVER_MAX_WIDTH_RATIO = 1.75;

class Screenshots extends PureComponent<Props, State> {
state = {
offsetX: null,
pageX: null,
containerTop: null,
};

_overlayElement = ensureExists(
document.querySelector('#root-overlay'),
'Expected to find a root overlay element.'
);

findScreenshotAtMouse(offsetX: number): number | null {
const { width, rangeStart, rangeEnd, screenshots } = this.props;
const rangeLength = rangeEnd - rangeStart;
const mouseTime = offsetX / width * rangeLength + rangeStart;

// Loop backwards to find the latest screenshot that has a time less
// than the current time at the mouse position.
for (let i = screenshots.length - 1; i >= 0; i--) {
const screenshotTime = screenshots[i].start;
if (mouseTime >= screenshotTime) {
return i;
}
}

return null;
}

/**
* This function runs through all of the screenshots, and then samples the last known
* screenshot, and places it on the screen, making a film strip.
*/
renderScreenshotStrip() {
const {
thread,
width: outerContainerWidth,
rangeStart,
rangeEnd,
screenshots,
} = this.props;

if (screenshots.length === 0) {
return null;
}

const images = [];
const rangeLength = rangeEnd - rangeStart;
const imageContainerWidth = TRACK_HEIGHT * 0.75;
const timeToPixel = time =>
outerContainerWidth * (time - rangeStart) / rangeLength;

let screenshotIndex = 0;

This comment was marked as outdated.

for (
let left = timeToPixel(screenshots[0].start);
left < outerContainerWidth;
left += imageContainerWidth
) {
// Try to find the next screenshot to fit in, or re-use the existing one.
for (let i = screenshotIndex; i < screenshots.length; i++) {
if (timeToPixel(screenshots[i].start) <= left) {
screenshotIndex = i;
} else {
break;
}
}
// Coerce the payload into a screenshot one.
const payload: ScreenshotPayload = (screenshots[screenshotIndex]
.data: any);
const { url: urlStringIndex, windowWidth, windowHeight } = payload;
const scaledImageWidth = TRACK_HEIGHT * windowWidth / windowHeight;
images.push(
<div
className="timelineTrackScreenshotImgContainer"
style={{ left, width: imageContainerWidth }}
key={left}
>
{/* The following image is centered and cropped by the outer container. */}
<img
className="timelineTrackScreenshotImg"
src={thread.stringTable.getString(urlStringIndex)}
style={{
width: scaledImageWidth,
height: TRACK_HEIGHT,
}}
/>
</div>
);
}

return images;
}

renderHoverPreview() {
const { pageX, offsetX, containerTop } = this.state;
const { screenshots, thread, isMakingPreviewSelection, width } = this.props;

if (isMakingPreviewSelection || offsetX === null || pageX === null) {
return null;
}

const screenshotIndex = this.findScreenshotAtMouse(offsetX);
if (screenshotIndex === null) {
return null;
}

// Coerce the payload into a screenshot one.
const payload: ScreenshotPayload = (screenshots[screenshotIndex].data: any);
const { url, windowWidth, windowHeight } = payload;

// Compute the hover image's thumbnail size.
let hoverHeight = HOVER_HEIGHT;
let hoverWidth = HOVER_HEIGHT / windowHeight * windowWidth;

if (hoverWidth > HOVER_HEIGHT * HOVER_MAX_WIDTH_RATIO) {
// This is a really wide image, limit the height so it lays out reasonably.
hoverWidth = HOVER_HEIGHT * HOVER_MAX_WIDTH_RATIO;
hoverHeight = hoverWidth / windowWidth * windowHeight;
}

// Set the top so it centers around the track.
const top = containerTop + (TRACK_HEIGHT - hoverHeight) * 0.5;
const left =
offsetX + hoverWidth * 0.5 > width
? // Stick the hover image on to the right side of the container.
pageX - offsetX + width - hoverWidth * 0.5
: // Center the hover image around the mouse.
pageX;

return createPortal(
<div className="timelineTrackScreenshotHover" style={{ left, top }}>
<img
className="timelineTrackScreenshotHoverImg"
src={thread.stringTable.getString(url)}
style={{
height: hoverHeight,
width: hoverWidth,
}}
/>
</div>,
this._overlayElement
);
}

_handleMouseLeave = () => {
this.setState({
offsetX: null,
pageX: null,
containerTop: null,
});
};

_handleMouseMove = (event: SyntheticMouseEvent<HTMLDivElement>) => {
const { top, left } = event.currentTarget.getBoundingClientRect();
this.setState({
pageX: event.pageX,
offsetX: event.pageX - left,
containerTop: top,
});
};

render() {
return (
<div
className="timelineTrackScreenshot"
style={{ height: TRACK_HEIGHT }}
onMouseLeave={this._handleMouseLeave}
onMouseMove={this._handleMouseMove}
>
{this.renderScreenshotStrip()}
{this.renderHoverPreview()}
</div>
);
}
}

const options: ExplicitConnectOptions<OwnProps, StateProps, DispatchProps> = {
mapStateToProps: (state, ownProps) => {
const { threadIndex, windowId } = ownProps;
const selectors = selectorsForThread(threadIndex);
const { start, end } = getCommittedRange(state);
const previewSelection = getPreviewSelection(state);
return {
thread: selectors.getRangeFilteredThread(state),
screenshots: ensureExists(
selectors.getRangeFilteredScreenshotsById(state).get(windowId),
'Expected to find screenshots for the given pid'
),
threadName: selectors.getFriendlyThreadName(state),
rangeStart: start,
rangeEnd: end,
isMakingPreviewSelection:
previewSelection.hasSelection && previewSelection.isModifying,
};
},
component: Screenshots,
};

export default withSize(explicitConnect(options));
Loading