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
196 changes: 176 additions & 20 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import {
saveDatasetAsJson,
saveToIndexedDB,
} from "./localStorage";
import { exportToGoogleSheet, requestSheetsToken } from "./GoogleSheets";
import {
exportToGoogleSheet,
requestSheetsToken,
extractSpreadsheetId,
importFromGoogleSheet,
getApiType,
} from "./GoogleSheets";
import _ from "lodash";
import { getQueryStringValue, setQueryStringValue } from "./queryString";
import "./global.css";
Expand Down Expand Up @@ -177,6 +183,16 @@ class App extends React.Component {
this.updateMapAndAssociatedData();
});

const pendingSheetId = getQueryStringValue("sheetId");
if (pendingSheetId) {
// Remove the sheetId from the URL to prevent reload loops
const url = new URL(window.location.href);
url.searchParams.delete("sheetId");
window.history.replaceState({}, document.title, url.toString());

this.loadSharedSheet(pendingSheetId);
}

// Initialize the Broadcast Channel
this.syncChannel = new BroadcastChannel("app_playback_sync");
this.syncChannel.onmessage = this.handleSyncMessage;
Expand All @@ -203,6 +219,50 @@ class App extends React.Component {
}
};

loadSharedSheet = async (sheetId) => {
this.setState({ pendingSheetId: null });
try {
toast.info("Loading Google Sheet...");
const token = await requestSheetsToken();
const logs = await importFromGoogleSheet(sheetId, token);

const logsWithTimestamp = logs.map((logEntry) => ({
...logEntry,
timestamp: logEntry.timestamp || new Date().toISOString(),
}));

await uploadCloudLogs(logsWithTimestamp, "_clipboard");

toast.success("Dataset from Google Sheet loaded. You can now Paste into any dataset.", { autoClose: false });
} catch (error) {
log(`Error loading shared sheet: ${error.message}`, error);

// If the browser blocks the popup (often happens on page load without user gesture)
if (error.message && error.message.includes("Failed to open login popup window")) {
toast.warning(
<div>
Popup blocked by browser.
<br />
<br />
<button
className="toggle-button toggle-button-active"
onClick={() => {
toast.dismiss();
this.loadSharedSheet(sheetId);
}}
style={{ padding: "4px 8px", fontSize: "14px" }}
>
Load Google Sheet
</button>
</div>,
{ autoClose: false, closeOnClick: false }
);
} else {
toast.error(`Failed to load shared sheet: ${error.message}`);
}
}
};

updateToggleState(newValue, toggleName, jsonPaths) {
this.setState((prevState) => {
const newToggleOptions = {
Expand Down Expand Up @@ -587,12 +647,16 @@ class App extends React.Component {
try {
const token = await requestSheetsToken();
const sheetUrl = await exportToGoogleSheet(index, token);
const spreadsheetId = extractSpreadsheetId(sheetUrl);
const deepLink = `${window.location.origin}${window.location.pathname}?sheetId=${spreadsheetId}`;
toast.success(
<span>
Exported to{" "}
<a href={sheetUrl} target="_blank" rel="noopener noreferrer">
Google Sheet
</a>
<br />
Shareable Fleet Debugger URL <a href={deepLink}>{deepLink}</a>
</span>,
{ autoClose: false }
);
Expand All @@ -602,6 +666,71 @@ class App extends React.Component {
}
};

const handleCopyClick = async (e) => {
e.stopPropagation();
log(`Copy dataset ${index} initiated`);
this.setState({ activeMenuIndex: null });

try {
const data = await getUploadedData(index);
if (!data || !data.rawLogs || data.rawLogs.length === 0) {
toast.warning("Dataset is empty, nothing to copy.");
return;
}

let logsToCopy = data.rawLogs;
const { logTypes } = this.state.filters;
const totalFilterCount = Object.keys(logTypes).length;
const activeFilterCount = Object.values(logTypes).filter(Boolean).length;

if (activeFilterCount !== totalFilterCount) {
if (window.confirm("Do you want to copy only the currently filtered log types?")) {
logsToCopy = data.rawLogs.filter((logEntry) => {
const apiType = getApiType(logEntry);
return logTypes[apiType] !== false;
});
}
}

const dataToSave = { ...data, rawLogs: logsToCopy };
await saveToIndexedDB(dataToSave, "_clipboard");
toast.success(`Dataset ${index + 1} copied to clipboard!`);
} catch (error) {
log(`Error copying dataset: ${error.message}`, error);
toast.error(`Error copying dataset: ${error.message}`);
}
};

const handlePasteClick = async (e) => {
e.stopPropagation();
log(`Paste to dataset ${index} initiated`);
this.setState({ activeMenuIndex: null });

try {
const clipboardData = await getUploadedData("_clipboard");
if (!clipboardData || !clipboardData.rawLogs) {
toast.warning("Clipboard is empty.");
return;
}
await saveToIndexedDB(clipboardData, index);
toast.success(`Pasted into Dataset ${index + 1}!`);

this.setState(
(prevState) => {
const newUploadedDatasets = [...prevState.uploadedDatasets];
newUploadedDatasets[index] = "Uploaded";
return { uploadedDatasets: newUploadedDatasets };
},
() => {
this.switchDataset(index);
}
);
} catch (error) {
log(`Error pasting dataset: ${error.message}`, error);
toast.error(`Error pasting dataset: ${error.message}`);
}
};

const handlePruneClick = async (e) => {
e.stopPropagation();
log(`Prune initiated for dataset ${index}`);
Expand Down Expand Up @@ -680,6 +809,25 @@ class App extends React.Component {
}

try {
if (result.pasteClipboard) {
const clipboardData = await getUploadedData("_clipboard");
if (!clipboardData || !clipboardData.rawLogs) {
toast.warning("Clipboard is empty.");
return;
}
await saveToIndexedDB(clipboardData, index);
toast.success(`Pasted into Dataset ${index + 1}!`);
this.setState(
(prevState) => {
const newUploadedDatasets = [...prevState.uploadedDatasets];
newUploadedDatasets[index] = "Uploaded";
return { uploadedDatasets: newUploadedDatasets };
},
() => this.switchDataset(index)
);
return;
}

if (result.file) {
const uploadEvent = { target: { files: [result.file] } };
await this.handleFileUpload(uploadEvent, index);
Expand All @@ -706,7 +854,9 @@ class App extends React.Component {
newUploadedDatasets[index] = "Uploaded";
return { uploadedDatasets: newUploadedDatasets };
},
() => this.switchDataset(index)
() => {
this.switchDataset(index);
}
);
}
} catch (error) {
Expand Down Expand Up @@ -748,29 +898,34 @@ class App extends React.Component {
isActive ? "dataset-button-active" : isUploaded ? "dataset-button-uploaded" : "dataset-button-empty"
}`}
>
{isUploaded ? `Dataset ${index + 1}` : `Select Dataset ${index + 1}`}

{isUploaded ? `Dataset ${index + 1}` : `Select Data ${index + 1}`}
{isUploaded && isActive && (
<span className="dataset-button-actions" onClick={toggleMenu}>
{isMenuOpen && (
<div className="dataset-button-menu">
<div className="dataset-button-menu-item export" onClick={handleSaveClick}>
Export File
</div>
<div className="dataset-button-menu-item export" onClick={handleGoogleSheetExport}>
Export GSheet
</div>
<div className="dataset-button-menu-item prune" onClick={handlePruneClick}>
Prune
</div>
<div className="dataset-button-menu-item delete" onClick={handleDeleteClick}>
Delete
</div>
</div>
)}
</span>
)}
{isMenuOpen && (
<div className="dataset-button-menu" style={{ top: "100%", right: 0 }}>
<div className="dataset-button-menu-item paste" onClick={handlePasteClick}>
Paste Dataset
</div>
<div className="dataset-button-menu-item copy" onClick={handleCopyClick}>
Copy Dataset
</div>
<div className="dataset-button-menu-item export" onClick={handleSaveClick}>
Export File
</div>
<div className="dataset-button-menu-item export" onClick={handleGoogleSheetExport}>
Export GSheet
</div>
<div className="dataset-button-menu-item prune" onClick={handlePruneClick}>
Prune
</div>
<div className="dataset-button-menu-item delete" onClick={handleDeleteClick}>
Delete
</div>
</div>
)}
</button>
</div>
);
Expand Down Expand Up @@ -858,6 +1013,7 @@ class App extends React.Component {
onLogsReceived: handleCloudLogsReceived,
onExtraLogsReceived: handleExtraLogsReceived,
onFileUpload: handleFileUpload,
onPasteClipboard: () => cleanupAndResolve({ pasteClipboard: true }),
hasExtraDataSource: HAS_EXTRA_DATA_SOURCE,
});
dialogRoot.render(datasetLoadingComponent);
Expand Down
9 changes: 7 additions & 2 deletions src/DatasetLoading.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isTokenValid, fetchLogsWithToken, useCloudLoggingLogin, buildQueryFilte
import DatasetSideLoading from "./DatasetSideLoading";
import { GOOGLE_CLIENT_ID } from "./constants";

const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload, onPasteClipboard }) => {
const getStoredValue = (key, defaultValue = "") => localStorage.getItem(`datasetLoading_${key}`) || defaultValue;

const [fetching, setFetching] = useState(false);
Expand Down Expand Up @@ -162,7 +162,12 @@ const CloudLoggingFormComponent = ({ onLogsReceived, onFileUpload }) => {
<progress className="progress-bar" />
</div>
)}
<DatasetSideLoading onLogsReceived={onLogsReceived} onFileUpload={onFileUpload} setLocalError={setLocalError}>
<DatasetSideLoading
onLogsReceived={onLogsReceived}
onFileUpload={onFileUpload}
setLocalError={setLocalError}
onPasteClipboard={onPasteClipboard}
>
<button type="button" onClick={handleFetch} disabled={fetching} className="fetch-logs-button">
{fetching ? "Fetching..." : isTokenValid() ? "Fetch Logs" : "Sign in and Fetch Logs"}
</button>
Expand Down
21 changes: 19 additions & 2 deletions src/DatasetSideLoading.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// src/DatasetSideLoading.js
import { useState } from "react";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";
import { useSheetsLogin, isSheetsTokenValid, getSheetsToken, importFromGoogleSheet } from "./GoogleSheets";
import { getUploadedData } from "./localStorage";
import { log } from "./Utils";

/**
Expand All @@ -14,10 +15,21 @@ import { log } from "./Utils";
* @param {Function} props.setLocalError Callback to set error messages in the parent.
* @param {React.ReactNode} props.children The primary fetch button(s) to show alongside sideload buttons.
*/
export const DatasetSideLoading = ({ onLogsReceived, onFileUpload, setLocalError, children }) => {
export const DatasetSideLoading = ({ onLogsReceived, onFileUpload, setLocalError, onPasteClipboard, children }) => {
const [sheetFormVisible, setSheetFormVisible] = useState(false);
const [sheetUrl, setSheetUrl] = useState(localStorage.getItem("datasetLoading_sheetUrl") || "");
const [sheetLoading, setSheetLoading] = useState(false);
const [hasClipboard, setHasClipboard] = useState(false);

useEffect(() => {
getUploadedData("_clipboard")
.then((data) => {
if (data && data.rawLogs && data.rawLogs.length > 0) {
setHasClipboard(true);
}
})
.catch(() => {});
}, []);

const handleSheetImport = (token) => {
setSheetLoading(true);
Expand Down Expand Up @@ -69,6 +81,11 @@ export const DatasetSideLoading = ({ onLogsReceived, onFileUpload, setLocalError
<>
<div className="cloud-logging-buttons">
{children}
{onPasteClipboard && hasClipboard && (
<button type="button" onClick={onPasteClipboard} className="sideload-logs-button">
Paste Dataset
</button>
)}
<button type="button" onClick={() => setSheetFormVisible(!sheetFormVisible)} className="sideload-logs-button">
Load Google Sheet
</button>
Expand Down
4 changes: 2 additions & 2 deletions src/GoogleSheets.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const API_TYPE_REGEX_MAP = [
{ name: "updateTask", regex: /updateTask/i },
];

function getApiType(logEntry) {
export function getApiType(logEntry) {
const typeField = logEntry["@type"] || logEntry.jsonpayload?.["@type"] || "";
for (const { name, regex } of API_TYPE_REGEX_MAP) {
if (regex.test(typeField)) return name;
Expand Down Expand Up @@ -266,7 +266,7 @@ export async function exportToGoogleSheet(index, token) {

// --- Import ---

function extractSpreadsheetId(input) {
export function extractSpreadsheetId(input) {
const trimmed = input.trim();
const urlMatch = trimmed.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
if (urlMatch) return urlMatch[1];
Expand Down
Loading