Add a modal with status updates for the universal export

Summary:
Design doc: https://docs.google.com/document/d/1HLCFl46RfqG0o1mSt8SWrwf_HMfOCRg_oENioc1rkvQ/edit#

Exporting all files form a device and export Flipper's own state could take a long time. We need to keep our users updated on the status.

Reviewed By: passy

Differential Revision: D40551661

fbshipit-source-id: d5c94fb99d4bc8b4495ce463915b77c475548f01
This commit is contained in:
Andrey Goncharov
2022-10-25 05:31:48 -07:00
committed by Facebook GitHub Bot
parent 96aa0ac02b
commit 80f947212b
2 changed files with 125 additions and 14 deletions

View File

@@ -8,7 +8,16 @@
*/ */
import React, {cloneElement, useState, useCallback, useMemo} from 'react'; import React, {cloneElement, useState, useCallback, useMemo} from 'react';
import {Button, Divider, Badge, Tooltip, Avatar, Popover, Menu} from 'antd'; import {
Button,
Divider,
Badge,
Tooltip,
Avatar,
Popover,
Menu,
Modal,
} from 'antd';
import { import {
MobileFilled, MobileFilled,
AppstoreOutlined, AppstoreOutlined,
@@ -64,12 +73,14 @@ import {
startFileExport, startFileExport,
startLinkExport, startLinkExport,
startFlipperLogsExport, startFlipperLogsExport,
ExportEverythingEverywhereAllAtOnceStatus,
} from '../utils/exportData'; } from '../utils/exportData';
import {openDeeplinkDialog} from '../deeplink'; import {openDeeplinkDialog} from '../deeplink';
import {css} from '@emotion/css'; import {css} from '@emotion/css';
import {getRenderHostInstance} from 'flipper-frontend-core'; import {getRenderHostInstance} from 'flipper-frontend-core';
import openSupportRequestForm from '../fb-stubs/openSupportRequestForm'; import openSupportRequestForm from '../fb-stubs/openSupportRequestForm';
import {StyleGuide} from './StyleGuide'; import {StyleGuide} from './StyleGuide';
import {useEffect} from 'react';
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({ const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
width: kind === 'small' ? 32 : 36, width: kind === 'small' ? 32 : 36,
@@ -435,22 +446,97 @@ function DebugLogsButton({
function ExportEverythingEverywhereAllAtOnceButton() { function ExportEverythingEverywhereAllAtOnceButton() {
const store = useStore(); const store = useStore();
const [status, setStatus] = useState<
ExportEverythingEverywhereAllAtOnceStatus | undefined
>();
const [statusMessage, setStatusMessage] = useState<JSX.Element | undefined>();
const exportEverythingEverywhereAllAtOnceTracked = useTrackedCallback( const exportEverythingEverywhereAllAtOnceTracked = useTrackedCallback(
'Debug data export', 'Debug data export',
() => exportEverythingEverywhereAllAtOnce(store), () => exportEverythingEverywhereAllAtOnce(store, setStatus),
[store], [store, setStatus],
); );
useEffect(() => {
switch (status) {
case 'logs': {
setStatusMessage(<p>Exporting Flipper logs...</p>);
return;
}
case 'files': {
let sheepCount = 0;
const setFileExportMessage = () => {
setStatusMessage(
<>
<p>Exporting Flipper debug files from all devices...</p>
<p>It could take a long time!</p>
<p>Let's count sheep while we wait: {sheepCount++}.</p>
<p>
Scream for help if the sheep count reaches 42, but not earlier.
</p>
</>,
);
};
setFileExportMessage();
const interval = setInterval(setFileExportMessage, 3000);
return () => clearInterval(interval);
}
case 'state': {
let dinosaursCount = 0;
const setStateExportMessage = () => {
setStatusMessage(
<>
<p>Exporting Flipper state...</p>
<p>It also could take a long time!</p>
<p>This time we could count dinosaurs: {dinosaursCount++}.</p>
<p>You already know what to do when the counter reaches 42.</p>
</>,
);
};
setStateExportMessage();
const interval = setInterval(setStateExportMessage, 1000);
return () => clearInterval(interval);
}
case 'archive': {
setStatusMessage(<p>Creating an archive...</p>);
return;
}
case 'done': {
setStatusMessage(<p>Done!</p>);
return;
}
case 'cancelled': {
setStatusMessage(<p>Cancelled! Why? 😱🤯👏</p>);
return;
}
}
}, [status]);
return ( return (
<LeftRailButton <>
icon={<BugOutlined />} <Modal
title="Export Flipper debug data" visible={!!status}
onClick={() => { centered
exportEverythingEverywhereAllAtOnceTracked(); onCancel={() => {
}} setStatus(undefined);
small }}
/> title="Exporting everything everywhere all at once"
footer={null}>
{statusMessage}
</Modal>
<LeftRailButton
icon={<BugOutlined />}
title="Export Flipper debug data"
onClick={() => {
exportEverythingEverywhereAllAtOnceTracked();
}}
small
/>
</>
); );
} }

View File

@@ -616,16 +616,27 @@ export async function startFlipperLogsExport() {
} }
async function startDeviceFlipperFolderExport() { async function startDeviceFlipperFolderExport() {
return await getRenderHostInstance().flipperServer.exec('fetch-debug-data'); return await getRenderHostInstance().flipperServer.exec(
{timeout: 3 * 60 * 1000},
'fetch-debug-data',
);
} }
export type ExportEverythingEverywhereAllAtOnceStatus =
| 'logs'
| 'files'
| 'state'
| 'archive'
| 'done'
| 'cancelled';
export async function exportEverythingEverywhereAllAtOnce( export async function exportEverythingEverywhereAllAtOnce(
store: MiddlewareAPI, store: MiddlewareAPI,
onStatusUpdate?: (status: ExportEverythingEverywhereAllAtOnceStatus) => void,
) { ) {
// TODO: Show a progress dialog
const zip = new JSZip(); const zip = new JSZip();
// Step 1: Export Flipper logs // Step 1: Export Flipper logs
onStatusUpdate?.('logs');
const serializedLogs = exportLogs const serializedLogs = exportLogs
.map((item) => JSON.stringify(item)) .map((item) => JSON.stringify(item))
.join('\n'); .join('\n');
@@ -633,6 +644,7 @@ export async function exportEverythingEverywhereAllAtOnce(
zip.file('flipper_logs.txt', serializedLogs); zip.file('flipper_logs.txt', serializedLogs);
// Step 2: Export device logs // Step 2: Export device logs
onStatusUpdate?.('files');
const flipperFolderContent = await startDeviceFlipperFolderExport(); const flipperFolderContent = await startDeviceFlipperFolderExport();
const deviceFlipperFolder = zip.folder('device_flipper_folder')!; const deviceFlipperFolder = zip.folder('device_flipper_folder')!;
@@ -661,6 +673,7 @@ export async function exportEverythingEverywhereAllAtOnce(
}); });
// Step 3: Export Flipper State // Step 3: Export Flipper State
onStatusUpdate?.('state');
const exportablePlugins = getExportablePlugins(store.getState()); const exportablePlugins = getExportablePlugins(store.getState());
// TODO: no need to put this in the store, // TODO: no need to put this in the store,
// need to be cleaned up later in combination with SupportForm // need to be cleaned up later in combination with SupportForm
@@ -669,9 +682,21 @@ export async function exportEverythingEverywhereAllAtOnce(
zip.file('flipper_export', serializedString); zip.file('flipper_export', serializedString);
onStatusUpdate?.('archive');
const archiveData = await zip.generateAsync({type: 'uint8array'}); const archiveData = await zip.generateAsync({type: 'uint8array'});
await getRenderHostInstance().exportFileBinary?.(archiveData); const exportedFilePath = await getRenderHostInstance().exportFileBinary?.(
archiveData,
{
defaultPath: 'flipper_EEAaO_export',
},
);
if (exportedFilePath) {
onStatusUpdate?.('done');
} else {
onStatusUpdate?.('cancelled');
}
} }
export async function startFileExport(dispatch: Store['dispatch']) { export async function startFileExport(dispatch: Store['dispatch']) {