RecordingButton.js (7839B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 // @ts-check 5 6 /** 7 * @template P 8 * @typedef {import("react-redux").ResolveThunks<P>} ResolveThunks<P> 9 */ 10 11 /** 12 * @typedef {object} StateProps 13 * @property {RecordingState} recordingState 14 * @property {boolean | null} isSupportedPlatform 15 * @property {boolean} recordingUnexpectedlyStopped 16 * @property {PageContext} pageContext 17 */ 18 19 /** 20 * @typedef {object} OwnProps 21 * @property {import("../../@types/perf").OnProfileReceived} onProfileReceived 22 * @property {import("../../@types/perf").PerfFront} perfFront 23 */ 24 25 /** 26 * @typedef {object} ThunkDispatchProps 27 * @property {typeof actions.startRecording} startRecording 28 * @property {typeof actions.getProfileAndStopProfiler} getProfileAndStopProfiler 29 * @property {typeof actions.stopProfilerAndDiscardProfile} stopProfilerAndDiscardProfile 30 */ 31 32 /** 33 * @typedef {ResolveThunks<ThunkDispatchProps>} DispatchProps 34 * @typedef {StateProps & DispatchProps & OwnProps} Props 35 * @typedef {import("../../@types/perf").RecordingState} RecordingState 36 * @typedef {import("../../@types/perf").State} StoreState 37 * @typedef {import("../../@types/perf").PageContext} PageContext 38 */ 39 40 "use strict"; 41 42 const { 43 createFactory, 44 PureComponent, 45 } = require("resource://devtools/client/shared/vendor/react.mjs"); 46 const { 47 div, 48 button, 49 span, 50 img, 51 } = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 52 const { 53 connect, 54 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 55 const actions = require("resource://devtools/client/performance-new/store/actions.js"); 56 const selectors = require("resource://devtools/client/performance-new/store/selectors.js"); 57 const Localized = createFactory( 58 require("resource://devtools/client/shared/vendor/fluent-react.js").Localized 59 ); 60 61 /** 62 * This component is not responsible for the full life cycle of recording a profile. It 63 * is only responsible for the actual act of stopping and starting recordings. It 64 * also reacts to the changes of the recording state from external changes. 65 * 66 * @augments {React.PureComponent<Props>} 67 */ 68 class RecordingButton extends PureComponent { 69 _onStartButtonClick = () => { 70 const { startRecording, perfFront } = this.props; 71 startRecording(perfFront); 72 }; 73 74 _onCaptureButtonClick = async () => { 75 const { getProfileAndStopProfiler, onProfileReceived, perfFront } = 76 this.props; 77 try { 78 const profileAndAdditionalInformation = 79 await getProfileAndStopProfiler(perfFront); 80 onProfileReceived(profileAndAdditionalInformation); 81 } catch (e) { 82 const assertedError = /** @type {Error | string} */ (e); 83 onProfileReceived(null, assertedError); 84 } 85 }; 86 87 _onStopButtonClick = () => { 88 const { stopProfilerAndDiscardProfile, perfFront } = this.props; 89 stopProfilerAndDiscardProfile(perfFront); 90 }; 91 92 render() { 93 const { 94 recordingState, 95 isSupportedPlatform, 96 recordingUnexpectedlyStopped, 97 } = this.props; 98 99 if (!isSupportedPlatform) { 100 return renderButton({ 101 label: startRecordingLabel(), 102 isPrimary: true, 103 disabled: true, 104 additionalMessage: 105 // No need to localize as this string is not displayed to Tier-1 platforms. 106 "Your platform is not supported. The Gecko Profiler only " + 107 "supports Tier-1 platforms.", 108 }); 109 } 110 111 switch (recordingState) { 112 case "not-yet-known": 113 return null; 114 115 case "available-to-record": 116 return renderButton({ 117 onClick: this._onStartButtonClick, 118 isPrimary: true, 119 label: startRecordingLabel(), 120 additionalMessage: recordingUnexpectedlyStopped 121 ? Localized( 122 { id: "perftools-status-recording-stopped-by-another-tool" }, 123 div(null, "The recording was stopped by another tool.") 124 ) 125 : null, 126 }); 127 128 case "request-to-stop-profiler": 129 return renderButton({ 130 label: Localized( 131 { id: "perftools-request-to-stop-profiler" }, 132 "Stopping recording" 133 ), 134 disabled: true, 135 }); 136 137 case "request-to-get-profile-and-stop-profiler": 138 return renderButton({ 139 label: Localized( 140 { id: "perftools-request-to-get-profile-and-stop-profiler" }, 141 "Capturing profile" 142 ), 143 disabled: true, 144 }); 145 146 case "request-to-start-recording": 147 case "recording": 148 return renderButton({ 149 label: span( 150 null, 151 Localized( 152 { id: "perftools-button-capture-recording" }, 153 "Capture recording" 154 ), 155 img({ 156 className: "perf-button-image", 157 alt: "", 158 /* This icon is actually the "open in new page" icon. */ 159 src: "chrome://devtools/skin/images/dock-undock.svg", 160 }) 161 ), 162 isPrimary: true, 163 onClick: this._onCaptureButtonClick, 164 disabled: recordingState === "request-to-start-recording", 165 additionalButton: { 166 label: Localized( 167 { id: "perftools-button-cancel-recording" }, 168 "Cancel recording" 169 ), 170 onClick: this._onStopButtonClick, 171 }, 172 }); 173 174 default: 175 throw new Error("Unhandled recording state"); 176 } 177 } 178 } 179 180 /** 181 * @param {{ 182 * disabled?: boolean, 183 * label?: React.ReactNode, 184 * onClick?: any, 185 * additionalMessage?: React.ReactNode, 186 * isPrimary?: boolean, 187 * pageContext?: PageContext, 188 * additionalButton?: { 189 * label: React.ReactNode, 190 * onClick: any, 191 * }, 192 * }} buttonSettings 193 */ 194 function renderButton(buttonSettings) { 195 const { 196 disabled, 197 label, 198 onClick, 199 additionalMessage, 200 isPrimary, 201 // pageContext, 202 additionalButton, 203 } = buttonSettings; 204 205 const buttonClass = isPrimary ? "primary" : "default"; 206 207 return div( 208 { className: "perf-button-container" }, 209 div( 210 null, 211 button( 212 { 213 className: `perf-photon-button perf-photon-button-${buttonClass} perf-button`, 214 disabled, 215 onClick, 216 }, 217 label 218 ), 219 additionalButton 220 ? button( 221 { 222 className: `perf-photon-button perf-photon-button-default perf-button`, 223 onClick: additionalButton.onClick, 224 disabled, 225 }, 226 additionalButton.label 227 ) 228 : null 229 ), 230 additionalMessage 231 ? div({ className: "perf-additional-message" }, additionalMessage) 232 : null 233 ); 234 } 235 236 function startRecordingLabel() { 237 return span( 238 null, 239 Localized({ id: "perftools-button-start-recording" }, "Start recording"), 240 img({ 241 className: "perf-button-image", 242 alt: "", 243 /* This icon is actually the "open in new page" icon. */ 244 src: "chrome://devtools/skin/images/dock-undock.svg", 245 }) 246 ); 247 } 248 249 /** 250 * @param {StoreState} state 251 * @returns {StateProps} 252 */ 253 function mapStateToProps(state) { 254 return { 255 recordingState: selectors.getRecordingState(state), 256 isSupportedPlatform: selectors.getIsSupportedPlatform(state), 257 recordingUnexpectedlyStopped: 258 selectors.getRecordingUnexpectedlyStopped(state), 259 pageContext: selectors.getPageContext(state), 260 }; 261 } 262 263 /** @type {ThunkDispatchProps} */ 264 const mapDispatchToProps = { 265 startRecording: actions.startRecording, 266 stopProfilerAndDiscardProfile: actions.stopProfilerAndDiscardProfile, 267 getProfileAndStopProfiler: actions.getProfileAndStopProfiler, 268 }; 269 270 module.exports = connect(mapStateToProps, mapDispatchToProps)(RecordingButton);