@@ -18,7 +18,13 @@ import {
1818 saveDatasetAsJson ,
1919 saveToIndexedDB ,
2020} from "./localStorage" ;
21- import { exportToGoogleSheet , requestSheetsToken } from "./GoogleSheets" ;
21+ import {
22+ exportToGoogleSheet ,
23+ requestSheetsToken ,
24+ extractSpreadsheetId ,
25+ importFromGoogleSheet ,
26+ getApiType ,
27+ } from "./GoogleSheets" ;
2228import _ from "lodash" ;
2329import { getQueryStringValue , setQueryStringValue } from "./queryString" ;
2430import "./global.css" ;
@@ -177,6 +183,16 @@ class App extends React.Component {
177183 this . updateMapAndAssociatedData ( ) ;
178184 } ) ;
179185
186+ const pendingSheetId = getQueryStringValue ( "sheetId" ) ;
187+ if ( pendingSheetId ) {
188+ // Remove the sheetId from the URL to prevent reload loops
189+ const url = new URL ( window . location . href ) ;
190+ url . searchParams . delete ( "sheetId" ) ;
191+ window . history . replaceState ( { } , document . title , url . toString ( ) ) ;
192+
193+ this . loadSharedSheet ( pendingSheetId ) ;
194+ }
195+
180196 // Initialize the Broadcast Channel
181197 this . syncChannel = new BroadcastChannel ( "app_playback_sync" ) ;
182198 this . syncChannel . onmessage = this . handleSyncMessage ;
@@ -203,6 +219,50 @@ class App extends React.Component {
203219 }
204220 } ;
205221
222+ loadSharedSheet = async ( sheetId ) => {
223+ this . setState ( { pendingSheetId : null } ) ;
224+ try {
225+ toast . info ( "Loading Google Sheet..." ) ;
226+ const token = await requestSheetsToken ( ) ;
227+ const logs = await importFromGoogleSheet ( sheetId , token ) ;
228+
229+ const logsWithTimestamp = logs . map ( ( logEntry ) => ( {
230+ ...logEntry ,
231+ timestamp : logEntry . timestamp || new Date ( ) . toISOString ( ) ,
232+ } ) ) ;
233+
234+ await uploadCloudLogs ( logsWithTimestamp , "_clipboard" ) ;
235+
236+ toast . success ( "Dataset from Google Sheet loaded. You can now Paste into any dataset." , { autoClose : false } ) ;
237+ } catch ( error ) {
238+ log ( `Error loading shared sheet: ${ error . message } ` , error ) ;
239+
240+ // If the browser blocks the popup (often happens on page load without user gesture)
241+ if ( error . message && error . message . includes ( "Failed to open login popup window" ) ) {
242+ toast . warning (
243+ < div >
244+ Popup blocked by browser.
245+ < br />
246+ < br />
247+ < button
248+ className = "toggle-button toggle-button-active"
249+ onClick = { ( ) => {
250+ toast . dismiss ( ) ;
251+ this . loadSharedSheet ( sheetId ) ;
252+ } }
253+ style = { { padding : "4px 8px" , fontSize : "14px" } }
254+ >
255+ Load Google Sheet
256+ </ button >
257+ </ div > ,
258+ { autoClose : false , closeOnClick : false }
259+ ) ;
260+ } else {
261+ toast . error ( `Failed to load shared sheet: ${ error . message } ` ) ;
262+ }
263+ }
264+ } ;
265+
206266 updateToggleState ( newValue , toggleName , jsonPaths ) {
207267 this . setState ( ( prevState ) => {
208268 const newToggleOptions = {
@@ -587,12 +647,16 @@ class App extends React.Component {
587647 try {
588648 const token = await requestSheetsToken ( ) ;
589649 const sheetUrl = await exportToGoogleSheet ( index , token ) ;
650+ const spreadsheetId = extractSpreadsheetId ( sheetUrl ) ;
651+ const deepLink = `${ window . location . origin } ${ window . location . pathname } ?sheetId=${ spreadsheetId } ` ;
590652 toast . success (
591653 < span >
592654 Exported to{ " " }
593655 < a href = { sheetUrl } target = "_blank" rel = "noopener noreferrer" >
594656 Google Sheet
595657 </ a >
658+ < br />
659+ Shareable Fleet Debugger URL < a href = { deepLink } > { deepLink } </ a >
596660 </ span > ,
597661 { autoClose : false }
598662 ) ;
@@ -602,6 +666,71 @@ class App extends React.Component {
602666 }
603667 } ;
604668
669+ const handleCopyClick = async ( e ) => {
670+ e . stopPropagation ( ) ;
671+ log ( `Copy dataset ${ index } initiated` ) ;
672+ this . setState ( { activeMenuIndex : null } ) ;
673+
674+ try {
675+ const data = await getUploadedData ( index ) ;
676+ if ( ! data || ! data . rawLogs || data . rawLogs . length === 0 ) {
677+ toast . warning ( "Dataset is empty, nothing to copy." ) ;
678+ return ;
679+ }
680+
681+ let logsToCopy = data . rawLogs ;
682+ const { logTypes } = this . state . filters ;
683+ const totalFilterCount = Object . keys ( logTypes ) . length ;
684+ const activeFilterCount = Object . values ( logTypes ) . filter ( Boolean ) . length ;
685+
686+ if ( activeFilterCount !== totalFilterCount ) {
687+ if ( window . confirm ( "Do you want to copy only the currently filtered log types?" ) ) {
688+ logsToCopy = data . rawLogs . filter ( ( logEntry ) => {
689+ const apiType = getApiType ( logEntry ) ;
690+ return logTypes [ apiType ] !== false ;
691+ } ) ;
692+ }
693+ }
694+
695+ const dataToSave = { ...data , rawLogs : logsToCopy } ;
696+ await saveToIndexedDB ( dataToSave , "_clipboard" ) ;
697+ toast . success ( `Dataset ${ index + 1 } copied to clipboard!` ) ;
698+ } catch ( error ) {
699+ log ( `Error copying dataset: ${ error . message } ` , error ) ;
700+ toast . error ( `Error copying dataset: ${ error . message } ` ) ;
701+ }
702+ } ;
703+
704+ const handlePasteClick = async ( e ) => {
705+ e . stopPropagation ( ) ;
706+ log ( `Paste to dataset ${ index } initiated` ) ;
707+ this . setState ( { activeMenuIndex : null } ) ;
708+
709+ try {
710+ const clipboardData = await getUploadedData ( "_clipboard" ) ;
711+ if ( ! clipboardData || ! clipboardData . rawLogs ) {
712+ toast . warning ( "Clipboard is empty." ) ;
713+ return ;
714+ }
715+ await saveToIndexedDB ( clipboardData , index ) ;
716+ toast . success ( `Pasted into Dataset ${ index + 1 } !` ) ;
717+
718+ this . setState (
719+ ( prevState ) => {
720+ const newUploadedDatasets = [ ...prevState . uploadedDatasets ] ;
721+ newUploadedDatasets [ index ] = "Uploaded" ;
722+ return { uploadedDatasets : newUploadedDatasets } ;
723+ } ,
724+ ( ) => {
725+ this . switchDataset ( index ) ;
726+ }
727+ ) ;
728+ } catch ( error ) {
729+ log ( `Error pasting dataset: ${ error . message } ` , error ) ;
730+ toast . error ( `Error pasting dataset: ${ error . message } ` ) ;
731+ }
732+ } ;
733+
605734 const handlePruneClick = async ( e ) => {
606735 e . stopPropagation ( ) ;
607736 log ( `Prune initiated for dataset ${ index } ` ) ;
@@ -680,6 +809,25 @@ class App extends React.Component {
680809 }
681810
682811 try {
812+ if ( result . pasteClipboard ) {
813+ const clipboardData = await getUploadedData ( "_clipboard" ) ;
814+ if ( ! clipboardData || ! clipboardData . rawLogs ) {
815+ toast . warning ( "Clipboard is empty." ) ;
816+ return ;
817+ }
818+ await saveToIndexedDB ( clipboardData , index ) ;
819+ toast . success ( `Pasted into Dataset ${ index + 1 } !` ) ;
820+ this . setState (
821+ ( prevState ) => {
822+ const newUploadedDatasets = [ ...prevState . uploadedDatasets ] ;
823+ newUploadedDatasets [ index ] = "Uploaded" ;
824+ return { uploadedDatasets : newUploadedDatasets } ;
825+ } ,
826+ ( ) => this . switchDataset ( index )
827+ ) ;
828+ return ;
829+ }
830+
683831 if ( result . file ) {
684832 const uploadEvent = { target : { files : [ result . file ] } } ;
685833 await this . handleFileUpload ( uploadEvent , index ) ;
@@ -706,7 +854,9 @@ class App extends React.Component {
706854 newUploadedDatasets [ index ] = "Uploaded" ;
707855 return { uploadedDatasets : newUploadedDatasets } ;
708856 } ,
709- ( ) => this . switchDataset ( index )
857+ ( ) => {
858+ this . switchDataset ( index ) ;
859+ }
710860 ) ;
711861 }
712862 } catch ( error ) {
@@ -748,29 +898,34 @@ class App extends React.Component {
748898 isActive ? "dataset-button-active" : isUploaded ? "dataset-button-uploaded" : "dataset-button-empty"
749899 } `}
750900 >
751- { isUploaded ? `Dataset ${ index + 1 } ` : `Select Dataset ${ index + 1 } ` }
752-
901+ { isUploaded ? `Dataset ${ index + 1 } ` : `Select Data ${ index + 1 } ` }
753902 { isUploaded && isActive && (
754903 < span className = "dataset-button-actions" onClick = { toggleMenu } >
755904 ▼
756- { isMenuOpen && (
757- < div className = "dataset-button-menu" >
758- < div className = "dataset-button-menu-item export" onClick = { handleSaveClick } >
759- Export File
760- </ div >
761- < div className = "dataset-button-menu-item export" onClick = { handleGoogleSheetExport } >
762- Export GSheet
763- </ div >
764- < div className = "dataset-button-menu-item prune" onClick = { handlePruneClick } >
765- Prune
766- </ div >
767- < div className = "dataset-button-menu-item delete" onClick = { handleDeleteClick } >
768- Delete
769- </ div >
770- </ div >
771- ) }
772905 </ span >
773906 ) }
907+ { isMenuOpen && (
908+ < div className = "dataset-button-menu" style = { { top : "100%" , right : 0 } } >
909+ < div className = "dataset-button-menu-item paste" onClick = { handlePasteClick } >
910+ Paste Dataset
911+ </ div >
912+ < div className = "dataset-button-menu-item copy" onClick = { handleCopyClick } >
913+ Copy Dataset
914+ </ div >
915+ < div className = "dataset-button-menu-item export" onClick = { handleSaveClick } >
916+ Export File
917+ </ div >
918+ < div className = "dataset-button-menu-item export" onClick = { handleGoogleSheetExport } >
919+ Export GSheet
920+ </ div >
921+ < div className = "dataset-button-menu-item prune" onClick = { handlePruneClick } >
922+ Prune
923+ </ div >
924+ < div className = "dataset-button-menu-item delete" onClick = { handleDeleteClick } >
925+ Delete
926+ </ div >
927+ </ div >
928+ ) }
774929 </ button >
775930 </ div >
776931 ) ;
@@ -858,6 +1013,7 @@ class App extends React.Component {
8581013 onLogsReceived : handleCloudLogsReceived ,
8591014 onExtraLogsReceived : handleExtraLogsReceived ,
8601015 onFileUpload : handleFileUpload ,
1016+ onPasteClipboard : ( ) => cleanupAndResolve ( { pasteClipboard : true } ) ,
8611017 hasExtraDataSource : HAS_EXTRA_DATA_SOURCE ,
8621018 } ) ;
8631019 dialogRoot . render ( datasetLoadingComponent ) ;
0 commit comments