Separate the concepts of archived and disconnected devices

Summary:
Minor code cleanup to avoid future confusion:

- archived: a device that was imported from a Flipper trace, and only has persisted state
- (dis)connected: a real stateful device that might or might not have an active connection

Reviewed By: nikoant

Differential Revision: D26275459

fbshipit-source-id: eba554b37c39711e367c3795ff4456329a303c22
This commit is contained in:
Michel Weststrate
2021-02-09 04:12:09 -08:00
committed by Facebook GitHub Bot
parent 1bb1cae167
commit 43c68c0e7c
19 changed files with 69 additions and 66 deletions

View File

@@ -588,7 +588,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
} }
const isArchivedDevice = !selectedDevice const isArchivedDevice = !selectedDevice
? false ? false
: selectedDevice instanceof ArchivedDevice; : selectedDevice.isArchived;
if (isArchivedDevice) { if (isArchivedDevice) {
pluginIsEnabled = true; pluginIsEnabled = true;
} }

View File

@@ -50,10 +50,12 @@ test('Devices can disconnect', async () => {
).toBe(true); ).toBe(true);
expect(device.isArchived).toBe(false); expect(device.isArchived).toBe(false);
expect(device.connected.get()).toBe(true);
device.disconnect(); device.disconnect();
expect(device.isArchived).toBe(true); expect(device.isArchived).toBe(false);
expect(device.connected.get()).toBe(false);
const instance = device.sandyPluginStates.get(deviceplugin.id)!; const instance = device.sandyPluginStates.get(deviceplugin.id)!;
expect(instance.instanceApi.isConnected).toBe(false); expect(instance.instanceApi.isConnected).toBe(false);
expect(instance).toBeTruthy(); expect(instance).toBeTruthy();
@@ -61,7 +63,8 @@ test('Devices can disconnect', async () => {
expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.destroy).toBeCalledTimes(0);
device.destroy(); device.destroy();
expect(device.isArchived).toBe(true); expect(device.isArchived).toBe(false);
expect(device.connected.get()).toBe(false);
expect(instance.instanceApi.destroy).toBeCalledTimes(1); expect(instance.instanceApi.destroy).toBeCalledTimes(1);
expect(device.sandyPluginStates.get(deviceplugin.id)).toBeUndefined(); expect(device.sandyPluginStates.get(deviceplugin.id)).toBeUndefined();
@@ -91,6 +94,7 @@ test('New device with same serial removes & cleans the old one', async () => {
const instance = device.sandyPluginStates.get(deviceplugin.id)!; const instance = device.sandyPluginStates.get(deviceplugin.id)!;
expect(device.isArchived).toBe(false); expect(device.isArchived).toBe(false);
expect(device.connected.get()).toBe(true);
expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.destroy).toBeCalledTimes(0);
expect(store.getState().connections.devices).toEqual([device]); expect(store.getState().connections.devices).toEqual([device]);
@@ -107,7 +111,8 @@ test('New device with same serial removes & cleans the old one', async () => {
}); });
device2.loadDevicePlugins(store.getState().plugins.devicePlugins); device2.loadDevicePlugins(store.getState().plugins.devicePlugins);
expect(device.isArchived).toBe(true); expect(device.isArchived).toBe(false);
expect(device.connected.get()).toBe(false);
expect(instance.instanceApi.destroy).toBeCalledTimes(1); expect(instance.instanceApi.destroy).toBeCalledTimes(1);
expect( expect(
device2.sandyPluginStates.get(deviceplugin.id)!.instanceApi.destroy, device2.sandyPluginStates.get(deviceplugin.id)!.instanceApi.destroy,

View File

@@ -12,13 +12,12 @@ import MetroDevice, {MetroReportableEvent} from '../devices/MetroDevice';
import {useStore} from '../utils/useStore'; import {useStore} from '../utils/useStore';
import {Button as AntButton} from 'antd'; import {Button as AntButton} from 'antd';
import {MenuOutlined, ReloadOutlined} from '@ant-design/icons'; import {MenuOutlined, ReloadOutlined} from '@ant-design/icons';
import {theme} from 'flipper-plugin';
type LogEntry = {};
export default function MetroButton() { export default function MetroButton() {
const device = useStore((state) => const device = useStore((state) =>
state.connections.devices.find( state.connections.devices.find(
(device) => device.os === 'Metro' && !device.isArchived, (device) => device.os === 'Metro' && device.connected.get(),
), ),
) as MetroDevice | undefined; ) as MetroDevice | undefined;
@@ -69,6 +68,7 @@ export default function MetroButton() {
sendCommand('reload'); sendCommand('reload');
}} }}
loading={progress < 1} loading={progress < 1}
style={{color: _hasBuildError ? theme.errorColor : undefined}}
/> />
<AntButton <AntButton
icon={<MenuOutlined />} icon={<MenuOutlined />}

View File

@@ -13,6 +13,8 @@ import {OS, DeviceShell} from './BaseDevice';
import {SupportFormRequestDetailsState} from '../reducers/supportForm'; import {SupportFormRequestDetailsState} from '../reducers/supportForm';
export default class ArchivedDevice extends BaseDevice { export default class ArchivedDevice extends BaseDevice {
isArchived = true;
constructor(options: { constructor(options: {
serial: string; serial: string;
deviceType: DeviceType; deviceType: DeviceType;
@@ -23,7 +25,7 @@ export default class ArchivedDevice extends BaseDevice {
supportRequestDetails?: SupportFormRequestDetailsState; supportRequestDetails?: SupportFormRequestDetailsState;
}) { }) {
super(options.serial, options.deviceType, options.title, options.os); super(options.serial, options.deviceType, options.title, options.os);
this.archivedState.set(true); this.connected.set(false);
this.source = options.source || ''; this.source = options.source || '';
this.supportRequestDetails = options.supportRequestDetails; this.supportRequestDetails = options.supportRequestDetails;
this.archivedScreenshotHandle = options.screenshotHandle; this.archivedScreenshotHandle = options.screenshotHandle;

View File

@@ -38,6 +38,8 @@ export type DeviceExport = {
}; };
export default class BaseDevice { export default class BaseDevice {
isArchived = false;
constructor( constructor(
serial: string, serial: string,
deviceType: DeviceType, deviceType: DeviceType,
@@ -72,10 +74,7 @@ export default class BaseDevice {
logListeners: Map<Symbol, DeviceLogListener> = new Map(); logListeners: Map<Symbol, DeviceLogListener> = new Map();
archivedState = createState(false); readonly connected = createState(true);
get isArchived() {
return this.archivedState.get();
}
// if imported, stores the original source location // if imported, stores the original source location
source = ''; source = '';
@@ -93,7 +92,7 @@ export default class BaseDevice {
} }
displayTitle(): string { displayTitle(): string {
return this.title; return this.connected.get() ? this.title : `${this.title} (Offline)`;
} }
async exportState( async exportState(
@@ -226,7 +225,7 @@ export default class BaseDevice {
} }
disconnect() { disconnect() {
this.archivedState.set(true); this.connected.set(false);
} }
destroy() { destroy() {

View File

@@ -53,7 +53,7 @@ export default class IOSDevice extends BaseDevice {
} }
async screenshot(): Promise<Buffer> { async screenshot(): Promise<Buffer> {
if (this.isArchived) { if (!this.connected.get()) {
return Buffer.from([]); return Buffer.from([]);
} }
const tmpImageName = uuid() + '.png'; const tmpImageName = uuid() + '.png';
@@ -192,7 +192,7 @@ export default class IOSDevice extends BaseDevice {
} }
async screenCaptureAvailable() { async screenCaptureAvailable() {
return this.deviceType === 'emulator' && !this.isArchived; return this.deviceType === 'emulator' && this.connected.get();
} }
async startScreenCapture(destination: string) { async startScreenCapture(destination: string) {

View File

@@ -9,7 +9,6 @@
import {LogLevel} from 'flipper-plugin'; import {LogLevel} from 'flipper-plugin';
import BaseDevice from './BaseDevice'; import BaseDevice from './BaseDevice';
import ArchivedDevice from './ArchivedDevice';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
// From xplat/js/metro/packages/metro/src/lib/reporting.js // From xplat/js/metro/packages/metro/src/lib/reporting.js
@@ -198,14 +197,4 @@ export default class MetroDevice extends BaseDevice {
console.warn('Cannot send command, no connection', command); console.warn('Cannot send command, no connection', command);
} }
} }
archive() {
return new ArchivedDevice({
serial: this.serial,
deviceType: this.deviceType,
title: this.title,
os: this.os,
screenshotHandle: null,
});
}
} }

View File

@@ -231,7 +231,7 @@ export default (store: Store, logger: Logger) => {
.getState() .getState()
.connections.devices.filter( .connections.devices.filter(
(device: BaseDevice) => (device: BaseDevice) =>
device.serial === androidDevice.serial && device.isArchived, device.serial === androidDevice.serial && !device.connected.get(),
) )
.map((device) => device.serial); .map((device) => device.serial);

View File

@@ -118,7 +118,7 @@ function processDevices(
(device) => (device) =>
device instanceof IOSDevice && device instanceof IOSDevice &&
device.deviceType === type && device.deviceType === type &&
!device.isArchived, device.connected.get(),
) )
.map((device) => device.serial), .map((device) => device.serial),
); );

View File

@@ -41,17 +41,8 @@ test('register, remove, re-register a metro device works correctly', () => {
expect(state.devices.length).toBe(1); expect(state.devices.length).toBe(1);
expect(state.devices[0].displayTitle()).toBe('React Native'); expect(state.devices[0].displayTitle()).toBe('React Native');
const archived = device1.archive(); device1.disconnect();
state = reducer(state, {
type: 'UNREGISTER_DEVICES',
payload: new Set([device1.serial]),
});
expect(state.devices.length).toBe(0);
state = reducer(state, {
type: 'REGISTER_DEVICE',
payload: archived,
});
expect(state.devices.length).toBe(1); expect(state.devices.length).toBe(1);
expect(state.devices[0].displayTitle()).toBe('React Native (Offline)'); expect(state.devices[0].displayTitle()).toBe('React Native (Offline)');
@@ -61,6 +52,7 @@ test('register, remove, re-register a metro device works correctly', () => {
}); });
expect(state.devices.length).toBe(1); expect(state.devices.length).toBe(1);
expect(state.devices[0].displayTitle()).toBe('React Native'); expect(state.devices[0].displayTitle()).toBe('React Native');
expect(state.devices[0]).not.toBe(device1);
}); });
test('triggering REGISTER_DEVICE before REGISTER_PLUGINS still registers device plugins', () => { test('triggering REGISTER_DEVICE before REGISTER_PLUGINS still registers device plugins', () => {

View File

@@ -23,7 +23,6 @@ import Client from '../../Client';
import {State} from '../../reducers'; import {State} from '../../reducers';
import BaseDevice from '../../devices/BaseDevice'; import BaseDevice from '../../devices/BaseDevice';
import MetroDevice from '../../devices/MetroDevice'; import MetroDevice from '../../devices/MetroDevice';
import ArchivedDevice from '../../devices/ArchivedDevice';
import {ExclamationCircleOutlined, FieldTimeOutlined} from '@ant-design/icons'; import {ExclamationCircleOutlined, FieldTimeOutlined} from '@ant-design/icons';
const {Text} = Typography; const {Text} = Typography;
@@ -56,7 +55,7 @@ export function AppInspect() {
metroDevice, metroDevice,
connections.userPreferredDevice, connections.userPreferredDevice,
]); ]);
const isDeviceArchived = useValue(activeDevice?.archivedState, false); const isDeviceConnected = useValue(activeDevice?.connected, false);
const isAppConnected = useValue(client?.connected, false); const isAppConnected = useValue(client?.connected, false);
return ( return (
@@ -69,13 +68,13 @@ export function AppInspect() {
<Layout.Container padv="small" padh="medium" gap={theme.space.large}> <Layout.Container padv="small" padh="medium" gap={theme.space.large}>
<AppSelector /> <AppSelector />
{renderStatusMessage( {renderStatusMessage(
isDeviceArchived, isDeviceConnected,
activeDevice, activeDevice,
client, client,
isAppConnected, isAppConnected,
)} )}
{!isDeviceArchived && isAppConnected && <BookmarkSection />} {isDeviceConnected && isAppConnected && <BookmarkSection />}
{!isDeviceArchived && activeDevice && ( {isDeviceConnected && activeDevice && (
<Toolbar gap> <Toolbar gap>
<MetroButton /> <MetroButton />
<ScreenCaptureButtons /> <ScreenCaptureButtons />
@@ -145,13 +144,28 @@ export function findBestDevice(
} }
function renderStatusMessage( function renderStatusMessage(
isDeviceArchived: boolean, isDeviceConnected: boolean,
activeDevice: BaseDevice | undefined, activeDevice: BaseDevice | undefined,
client: Client | undefined, client: Client | undefined,
isAppConnected: boolean, isAppConnected: boolean,
): React.ReactNode { ): React.ReactNode {
return isDeviceArchived ? ( if (!activeDevice) {
activeDevice instanceof ArchivedDevice ? ( return (
<Layout.Horizontal gap center>
<ExclamationCircleOutlined style={{color: theme.warningColor}} />
<Text
type="secondary"
style={{
textTransform: 'uppercase',
fontSize: '0.8em',
}}>
Device disconnected
</Text>
</Layout.Horizontal>
);
}
return !isDeviceConnected ? (
activeDevice.isArchived ? (
<Layout.Horizontal gap center> <Layout.Horizontal gap center>
<FieldTimeOutlined style={{color: theme.primaryColor}} /> <FieldTimeOutlined style={{color: theme.primaryColor}} />
<Text <Text
@@ -160,7 +174,7 @@ function renderStatusMessage(
textTransform: 'uppercase', textTransform: 'uppercase',
fontSize: '0.8em', fontSize: '0.8em',
}}> }}>
Device loaded from file No device selected
</Text> </Text>
</Layout.Horizontal> </Layout.Horizontal>
) : ( ) : (

View File

@@ -57,7 +57,7 @@ export function AppSelector() {
uninitializedClients, uninitializedClients,
selectedApp, selectedApp,
} = useStore((state) => state.connections); } = useStore((state) => state.connections);
useValue(selectedDevice?.archivedState, false); // subscribe to future archived state changes useValue(selectedDevice?.connected, false); // subscribe to future archived state changes
const onSelectDevice = useTrackedCallback( const onSelectDevice = useTrackedCallback(
'select-device', 'select-device',
@@ -223,8 +223,8 @@ function computeEntries(
} }
function DeviceTitle({device}: {device: BaseDevice}) { function DeviceTitle({device}: {device: BaseDevice}) {
const connected = !useValue(device.archivedState); const connected = useValue(device.connected);
const isImported = device instanceof ArchivedDevice; const isImported = device.isArchived;
return ( return (
<span> <span>
<>{device.title} </> <>{device.title} </>

View File

@@ -85,7 +85,7 @@ export const PluginList = memo(function PluginList({
connections.userStarredPlugins, connections.userStarredPlugins,
pluginsChanged, pluginsChanged,
]); ]);
const isArchived = useValue(activeDevice?.archivedState, false); const isConnected = useValue(activeDevice?.connected, false);
const annotatedDownloadablePlugins = useMemoize< const annotatedDownloadablePlugins = useMemoize<
[ [
@@ -198,7 +198,7 @@ export const PluginList = memo(function PluginList({
))} ))}
</PluginGroup> </PluginGroup>
{!isArchived && ( {isConnected && (
<PluginGroup <PluginGroup
key="metro" key="metro"
title="React Native" title="React Native"
@@ -226,7 +226,7 @@ export const PluginList = memo(function PluginList({
onClick={handleAppPluginClick} onClick={handleAppPluginClick}
tooltip={getPluginTooltip(plugin.details)} tooltip={getPluginTooltip(plugin.details)}
actions={ actions={
isArchived ? null : ( isConnected ? (
<ActionButton <ActionButton
id={plugin.id} id={plugin.id}
onClick={handleStarPlugin} onClick={handleStarPlugin}
@@ -235,12 +235,12 @@ export const PluginList = memo(function PluginList({
<MinusOutlined size={16} style={{marginRight: 0}} /> <MinusOutlined size={16} style={{marginRight: 0}} />
} }
/> />
) ) : null
} }
/> />
))} ))}
</PluginGroup> </PluginGroup>
{!isArchived && ( {isConnected && (
<PluginGroup <PluginGroup
key="disabled" key="disabled"
title="Disabled" title="Disabled"
@@ -305,7 +305,7 @@ export const PluginList = memo(function PluginList({
/> />
))} ))}
</PluginGroup> </PluginGroup>
{!isArchived && ( {isConnected && (
<PluginGroup <PluginGroup
key="unavailable" key="unavailable"
title="Unavailable plugins" title="Unavailable plugins"

View File

@@ -431,14 +431,14 @@ export async function processStore(
statusUpdate = () => {}; statusUpdate = () => {};
} }
statusUpdate('Capturing screenshot...'); statusUpdate('Capturing screenshot...');
const deviceScreenshot = device.isArchived const deviceScreenshot = device.connected.get()
? null ? await capture(device).catch((e) => {
: await capture(device).catch((e) => {
console.warn( console.warn(
'Failed to capture device screenshot when exporting. ' + e, 'Failed to capture device screenshot when exporting. ' + e,
); );
return null; return null;
}); })
: null;
const processedClients = processClients(clients, serial, statusUpdate); const processedClients = processClients(clients, serial, statusUpdate);
const processedPluginStates = processPluginStates({ const processedPluginStates = processPluginStates({
clients: processedClients, clients: processedClients,

View File

@@ -301,7 +301,7 @@ function getFavoritePlugins(
starredPlugins: undefined | string[], starredPlugins: undefined | string[],
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
): PluginDefinition[] { ): PluginDefinition[] {
if (device instanceof ArchivedDevice) { if (device.isArchived) {
if (!returnFavoredPlugins) { if (!returnFavoredPlugins) {
return []; return [];
} }

View File

@@ -27,8 +27,8 @@ export function getFileName(extension: 'png' | 'mp4'): string {
} }
export async function capture(device: BaseDevice): Promise<string> { export async function capture(device: BaseDevice): Promise<string> {
if (device.isArchived) { if (!device.connected.get()) {
console.log('Skipping screenshot for archived device'); console.log('Skipping screenshot for disconnected device');
return ''; return '';
} }
const pngPath = path.join(CAPTURE_LOCATION, getFileName('png')); const pngPath = path.join(CAPTURE_LOCATION, getFileName('png'));

View File

@@ -11,6 +11,7 @@ import {SandyPluginDefinition} from './SandyPluginDefinition';
import {BasePluginInstance, BasePluginClient} from './PluginBase'; import {BasePluginInstance, BasePluginClient} from './PluginBase';
import {FlipperLib} from './FlipperLib'; import {FlipperLib} from './FlipperLib';
import {DeviceType as PluginDeviceType} from 'flipper-plugin-lib'; import {DeviceType as PluginDeviceType} from 'flipper-plugin-lib';
import {Atom} from '../state/atom';
export type DeviceLogListener = (entry: DeviceLogEntry) => void; export type DeviceLogListener = (entry: DeviceLogEntry) => void;
@@ -67,6 +68,7 @@ export interface RealFlipperDevice {
os: string; os: string;
serial: string; serial: string;
isArchived: boolean; isArchived: boolean;
connected: Atom<boolean>;
deviceType: DeviceType; deviceType: DeviceType;
addLogListener(callback: DeviceLogListener): Symbol; addLogListener(callback: DeviceLogListener): Symbol;
removeLogListener(id: Symbol): void; removeLogListener(id: Symbol): void;

View File

@@ -136,8 +136,7 @@ export abstract class BasePluginInstance {
return realDevice.isArchived; return realDevice.isArchived;
}, },
get isConnected() { get isConnected() {
// for now same as isArchived, in the future we might distinguish between archived/imported and disconnected/offline devices return realDevice.connected.get();
return !realDevice.isArchived;
}, },
deviceType: realDevice.deviceType, deviceType: realDevice.deviceType,

View File

@@ -421,6 +421,7 @@ function createMockDevice(options?: StartPluginOptions): RealFlipperDevice {
deviceType: 'emulator', deviceType: 'emulator',
serial: 'serial-000', serial: 'serial-000',
isArchived: !!options?.isArchived, isArchived: !!options?.isArchived,
connected: createState(true),
devicePlugins: [], devicePlugins: [],
addLogListener(cb) { addLogListener(cb) {
logListeners.push(cb); logListeners.push(cb);