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
15 changes: 10 additions & 5 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,14 @@ MenuButtons--index--metaInfo-button =

MenuButtons--index--full-view = Full View
MenuButtons--index--cancel-upload = Cancel Upload
MenuButtons--index--share-upload =
.label = Upload Local Profile
MenuButtons--index--download =
.label = Download…
Comment thread
fatadel marked this conversation as resolved.

MenuButtons--index--share-re-upload =
.label = Re-upload
MenuButtons--index--share =
.label = Share…

MenuButtons--index--reshare =
.label = Re-share…

MenuButtons--index--share-error-uploading =
.label = Error uploading
Expand Down Expand Up @@ -758,9 +761,11 @@ MenuButtons--publish--renderCheckbox-label-private-browsing-warning-image =
MenuButtons--publish--renderCheckbox-label-argument-values = Include JavaScript execution tracing function argument values
MenuButtons--publish--renderCheckbox-label-argument-values-warning-image =
.title = This profile contains function argument values recorded from the page, which may include personal data
MenuButtons--publish--reupload-performance-profile = Re-upload Performance Profile
MenuButtons--publish--share-performance-profile = Share Performance Profile
MenuButtons--publish--reshare-performance-profile = Re-share Performance Profile
MenuButtons--publish--download-performance-profile = Download Performance Profile
MenuButtons--publish--info-description = Upload your profile and make it accessible to anyone with the link.
MenuButtons--publish--download-info-description = Save this profile as a file on your computer.
MenuButtons--publish--info-description-default = By default, your personal data is removed.
MenuButtons--publish--info-description-firefox-nightly2 = This profile is from { -firefox-nightly-brand-name }, so by default most information is included.
MenuButtons--publish--include-additional-data = Include additional data that may be identifiable
Expand Down
91 changes: 49 additions & 42 deletions src/actions/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,33 @@ import type {
Pid,
TrackIndex,
ProfileIndexTranslationMaps,
ProfileEncodingResult,
SharingMode,
} from 'firefox-profiler/types';
import { compress } from 'firefox-profiler/utils/gz';
import { serializeProfileToJsonString } from 'firefox-profiler/profile-logic/process-profile';

export function updateSharingOption(
mode: SharingMode,
slug: keyof CheckedSharingOptions,
value: boolean
): Action {
return {
type: 'UPDATE_SHARING_OPTION',
mode,
slug,
value,
};
}

export function sanitizedProfileEncodingStarted(
sanitizedProfile: Profile
sanitizedProfile: Profile,
encodingPromise: Promise<ProfileEncodingResult>
): Action {
return {
type: 'SANITIZED_PROFILE_ENCODING_STARTED',
sanitizedProfile,
encodingPromise,
};
}

Expand Down Expand Up @@ -176,8 +182,10 @@ async function persistJustUploadedProfileInformationToDb(
const urlPredictor = getUrlPredictor(prepublishedState);
let predictedUrl;

const removeProfileInformation =
getRemoveProfileInformation(prepublishedState);
const removeProfileInformation = getRemoveProfileInformation(
prepublishedState,
'upload'
);
if (removeProfileInformation) {
// In case you wonder, committedRanges is either an empty array (if the
// range was sanitized) or `null` (otherwise).
Expand Down Expand Up @@ -253,13 +261,6 @@ async function persistJustUploadedProfileInformationToDb(
}
}

export type ProfileEncodingResult =
| {
type: 'SUCCESS';
profileData: Blob;
}
| { type: 'ERROR'; error: Error };

function unwrapEncodedProfile(encodingResult: ProfileEncodingResult): Blob {
if (encodingResult.type === 'ERROR') {
throw encodingResult.error;
Expand All @@ -278,31 +279,33 @@ export type InflightProfileEncoding = {
* - Serialize the profile to a buffer
* - Kick off the asynchronous compression of the buffer
*
* The asynchronous compression can take a few seconds, so we want to kick
* it off immediately when the profile publishing panel is opened. We also
* want to be able to make use of the current in-flight compression if the
* user clicks the upload button before compression is done. This is why
* we return an `InflightProfileEncoding` object from this action; it contains
* a promise which lets other parts of the publishing pipeline wait on the
* compressed results.
* The asynchronous compression can take a few seconds, so we kick it off
* immediately when the download or share panel is opened. The in-flight
* compression is tracked in the redux state, so opening the other panel or
* pressing Upload reuses it (for the same sanitized profile) instead of
* compressing again. The returned promise lets the caller wait on the
* compressed result.
*
* This thunk action is synchronous.
*/
export function encodeSanitizedProfile(
previousInflightEncoding?: InflightProfileEncoding
mode: SharingMode
): ThunkAction<InflightProfileEncoding> {
return (dispatch, getState): InflightProfileEncoding => {
const state = getState();
const sanitizedProfile = getSanitizedProfile(state).profile;

if (previousInflightEncoding?.sanitizedProfile === sanitizedProfile) {
// No need to kick of another compression. The current encoding may still
// be in-flight, and returning the original promise allows the caller to
// await it.
return previousInflightEncoding;
}
const sanitizedProfile = getSanitizedProfile(state, mode).profile;

const encodingState = getSanitizedProfileEncodingState(state);
if (
encodingState.phase === 'ENCODING' &&
encodingState.sanitizedProfile === sanitizedProfile
) {
// A compression for this profile is already in-flight; reuse it.
return {
sanitizedProfile,
encodingPromise: encodingState.encodingPromise,
};
}
if (
encodingState.phase === 'DONE' &&
encodingState.sanitizedProfile === sanitizedProfile
Expand All @@ -320,8 +323,10 @@ export function encodeSanitizedProfile(
// Kick off a new encoding for this profile. Don't await the promise,
// just return it as part of the InflightProfileEncoding.
const encodingPromise: Promise<ProfileEncodingResult> = (async function () {
// Yield so the ENCODING phase (dispatched below) is in the store before a
// synchronous failure here could dispatch FAILED, which the reducer drops.
await Promise.resolve();
try {
dispatch(sanitizedProfileEncodingStarted(sanitizedProfile));
const gzipData = await compress(
serializeProfileToJsonString(sanitizedProfile)
);
Expand All @@ -335,25 +340,24 @@ export function encodeSanitizedProfile(
}
})();

dispatch(
sanitizedProfileEncodingStarted(sanitizedProfile, encodingPromise)
);
return { sanitizedProfile, encodingPromise };
};
}

/**
* This function starts the profile sharing process. Takes an optional argument that
* indicates if the share attempt is being made for the second time. We have two share
* buttons, one for sharing for the first time, and one for sharing after the initial
* share depending on the previous URL share status. People can decide to remove the
* URLs from the profile after sharing with URLs or they can decide to add the URLs after
* sharing without them. We check the current state before attempting to share depending
* on that flag.
* This function starts the profile sharing process.
*
* There are two share buttons: one for the first upload, and one to re-share
* afterwards, which lets people add or remove the profile's URLs between
* attempts.
*
* The return value is used for tests to determine if the request went all the way
* through (true) or was quit early due to the generation value being invalidated (false).
*/
export function attemptToPublish(
previousInflightEncoding?: InflightProfileEncoding
): ThunkAction<Promise<boolean>> {
export function attemptToPublish(): ThunkAction<Promise<boolean>> {
return async (dispatch, getState) => {
try {
sendAnalytics({
Expand Down Expand Up @@ -386,10 +390,11 @@ export function attemptToPublish(
};
dispatch(uploadCompressionStarted(abortfunction));

const sanitizedInformation = getSanitizedProfile(prePublishedState);
const profileEncoding = dispatch(
encodeSanitizedProfile(previousInflightEncoding)
const sanitizedInformation = getSanitizedProfile(
prePublishedState,
'upload'
);
const profileEncoding = dispatch(encodeSanitizedProfile('upload'));
const encodingResult = await profileEncoding.encodingPromise;

// The previous line was async, check to make sure that this request is still valid.
Expand Down Expand Up @@ -431,8 +436,10 @@ export function attemptToPublish(
return false;
}

const removeProfileInformation =
getRemoveProfileInformation(prePublishedState);
const removeProfileInformation = getRemoveProfileInformation(
prePublishedState,
'upload'
);
if (removeProfileInformation) {
const { committedRanges, translationMaps, profile } =
sanitizedInformation;
Expand Down
35 changes: 25 additions & 10 deletions src/components/app/MenuButtons/Publish.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
background-image: var(--internal-uploading-icon);
}

.menuButtonsDownloadButton::before {
background-image: url(../../../../res/img/svg/download.svg);

/* download.svg is 14x20, so size by height to fit the square icon slot. */
background-size: auto 12px;
}

.menuButtonsShareButtonError {
--internal-error-foreground-color: white;
--internal-error-background-color: var(--red-60);
Expand Down Expand Up @@ -58,6 +65,10 @@
--internal-uploading-icon: url(../../../../res/img/svg/sharing-animated-dark-12.svg);
}

.menuButtonsDownloadButton::before {
background-image: url(../../../../res/img/svg/download-light.svg);
}

.menuButtonsShareButtonError {
--internal-error-foreground-color: var(--grey-20);
}
Expand All @@ -75,22 +86,22 @@
.publishPanelContent {
position: relative;

/* This aligns all content, except the big icon. */
padding-left: 70px;
/* Reserve space on the left for the title icon. */
padding-left: 34px;
}

.publishPanelTitle {
/* "60px" This is the value to put the background image at the right location.
* This background image is 44x44, so this puts it 16px left of the text. */
padding-left: 60px;
margin: 0 0 0 -60px;
padding-left: 28px;

/* Negative margin pulls the 18px icon back into the gutter, 10px before the text. */
margin: 0 0 6px -28px;
background: var(--internal-info-icon) left center no-repeat;
line-height: 44px; /* This is the height of the background image */
background-size: 18px 18px;
line-height: 28px;
}

.publishPanelInfoDescription {
flex: 1;
margin-bottom: 1em;
margin: 0 0 12px;
line-height: 1.5;
}

Expand All @@ -111,7 +122,7 @@
.publishPanelButtons {
display: flex;
justify-content: right;
margin-top: 20px;
margin-top: 16px;
}

.publishPanelButton {
Expand Down Expand Up @@ -155,6 +166,10 @@
background: var(--internal-download-icon) center center no-repeat;
}

.publishPanelButtonsDownloadPrimary .publishPanelButtonsSvgDownload {
background-image: url(../../../../res/img/svg/download-light.svg);
}

.menuButtonsDownloadSize {
display: inline-block;
margin: 0 4px;
Expand Down
Loading
Loading