Skip to content

Commit

Permalink
Add ability to favourite virtual devices
Browse files Browse the repository at this point in the history
Summary:
With a lot of virtual devices it can be confusing to remember which one contains your builds. This allows user to favourite certain ones to avoid this

Changelog: Added ability to favourite emulators / simulators in the launch virtual devices dialog

Reviewed By: mweststrate

Differential Revision: D47724521

fbshipit-source-id: aaec56608ad6ba23634797315f6f9fd77fc8b258
  • Loading branch information
Luke De Feo authored and facebook-github-bot committed Jul 25, 2023
1 parent a0d6c9a commit d52eeff
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 62 deletions.
219 changes: 159 additions & 60 deletions desktop/flipper-ui-core/src/sandy-chrome/appinspect/LaunchEmulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
*/

import React, {useEffect, useState} from 'react';
import {Modal, Button, message, Alert, Menu, Dropdown} from 'antd';
import {Modal, Button, message, Alert, Menu, Dropdown, Typography} from 'antd';
import {
AppleOutlined,
PoweroffOutlined,
MoreOutlined,
AndroidOutlined,
HeartOutlined,
HeartFilled,
} from '@ant-design/icons';
import {Store} from '../../reducers';
import {useStore} from '../../utils/useStore';
Expand All @@ -22,12 +22,16 @@ import {
Spinner,
renderReactRoot,
withTrackingScope,
useLocalStorageState,
theme,
} from 'flipper-plugin';
import {Provider} from 'react-redux';
import {IOSDeviceParams} from 'flipper-common';
import {getRenderHostInstance} from 'flipper-frontend-core';
import SettingsSheet from '../../chrome/SettingsSheet';
import {Link} from '../../ui';
import {chain, uniq, without} from 'lodash';
import {ReactNode} from 'react-markdown';

const COLD_BOOT = 'cold-boot';

Expand Down Expand Up @@ -90,6 +94,17 @@ export const LaunchEmulatorDialog = withTrackingScope(
const [waitingForAndroid, setWaitingForAndroid] = useState(androidEnabled);
const waitingForResults = waitingForIos || waitingForAndroid;

const [favoriteVirtualDevices, setFavoriteVirtualDevices] =
useLocalStorageState<string[]>('favourite-virtual-devices', []);

const addToFavorites = (deviceName: string) => {
setFavoriteVirtualDevices(uniq([deviceName, ...favoriteVirtualDevices]));
};

const removeFromFavorites = (deviceName: string) => {
setFavoriteVirtualDevices(without(favoriteVirtualDevices, deviceName));
};

useEffect(() => {
if (!iosEnabled) {
return;
Expand Down Expand Up @@ -131,70 +146,105 @@ export const LaunchEmulatorDialog = withTrackingScope(
}

const items = [
...(androidEmulators.length > 0
? [<AndroidOutlined key="android logo" />]
: []),
...androidEmulators.map((name) => {
const launch = (coldBoot: boolean) => {
getRenderHostInstance()
.flipperServer.exec('android-launch-emulator', name, coldBoot)
.then(onClose)
.catch((e) => {
console.error('Failed to start emulator: ', e);
message.error('Failed to start emulator: ' + e);
});
};
const menu = (
<Menu
onClick={({key}) => {
switch (key) {
case COLD_BOOT: {
launch(true);
break;
}
}
}}>
<Menu.Item key={COLD_BOOT} icon={<PoweroffOutlined />}>
Cold Boot
</Menu.Item>
</Menu>
);
return (
<Dropdown.Button
key={name}
overlay={menu}
icon={<MoreOutlined />}
onClick={() => launch(false)}>
{name}
</Dropdown.Button>
);
}),
...(iosEmulators.length > 0 ? [<AppleOutlined key="ios logo" />] : []),
...iosEmulators.map((device) => (
<Button
key={device.udid}
onClick={() =>
androidEmulators.length > 0 ? (
<Title key="android-title" name="Android emulators" />
) : null,
...chain(
androidEmulators.map((name) => ({
name,
isFavorite: favoriteVirtualDevices.includes(name),
})),
)
.sortBy((item) => [!item.isFavorite, item.name])
.map(({name, isFavorite}) => {
const launch = (coldBoot: boolean) => {
getRenderHostInstance()
.flipperServer.exec('ios-launch-simulator', device.udid)
.catch((e) => {
console.warn('Failed to start simulator: ', e);
message.error('Failed to start simulator: ' + e);
})
.flipperServer.exec('android-launch-emulator', name, coldBoot)
.then(onClose)
}>
{device.name}
</Button>
)),
];
.catch((e) => {
console.error('Failed to start emulator: ', e);
message.error('Failed to start emulator: ' + e);
});
};
const menu = (
<Menu
onClick={({key}) => {
switch (key) {
case COLD_BOOT: {
launch(true);
break;
}
}
}}>
<Menu.Item key={COLD_BOOT} icon={<PoweroffOutlined />}>
Cold Boot
</Menu.Item>
</Menu>
);
return (
<VirtualDeviceRow
key={name}
addToFavorites={addToFavorites}
removeFromFavorites={removeFromFavorites}
isFavorite={isFavorite}
name={name}>
<Dropdown.Button
overlay={menu}
icon={<MoreOutlined />}
onClick={() => launch(false)}>
{name}
</Dropdown.Button>
</VirtualDeviceRow>
);
})
.value(),

iosEmulators.length > 0 ? (
<Title key="android-title" name="iOS Simulators" />
) : null,
...chain(iosEmulators)
.map((device) => ({
device,
isFavorite: favoriteVirtualDevices.includes(device.name),
}))
.sortBy((item) => [!item.isFavorite, item.device.name])
.map(({device, isFavorite}) => (
<VirtualDeviceRow
key={device.udid}
addToFavorites={addToFavorites}
removeFromFavorites={removeFromFavorites}
isFavorite={isFavorite}
name={device.name}>
<Button
type="default"
key={device.udid}
style={{width: '100%'}}
onClick={() =>
getRenderHostInstance()
.flipperServer.exec('ios-launch-simulator', device.udid)
.catch((e) => {
console.warn('Failed to start simulator: ', e);
message.error('Failed to start simulator: ' + e);
})
.then(onClose)
}>
{device.name}
</Button>
</VirtualDeviceRow>
))
.value(),
].filter((item) => item != null);

const loadingSpinner = (
<>
{waitingForResults && <Spinner />}
{waitingForResults && <Spinner key="spinner" />}
{!waitingForResults && items.length === 0 && (
<Alert
key=" alert-nodevices"
message={
<>
No emulators available. <br />
No virtual devices available.
<br />
<Link href="http://fbflipper.com/docs/getting-started/troubleshooting/general/#i-see-no-emulators-available">
Learn more
</Link>
Expand All @@ -210,14 +260,63 @@ export const LaunchEmulatorDialog = withTrackingScope(
visible
centered
onCancel={onClose}
title="Launch Emulator"
title="Launch Virtual device"
footer={null}
bodyStyle={{maxHeight: 400, height: 400, overflow: 'auto'}}>
<Layout.Container gap>
<Layout.Container gap="medium">
{items.length ? items : <></>}
{loadingSpinner}
</Layout.Container>
</Modal>
);
},
);

const FavIconStyle = {fontSize: 16, color: theme.primaryColor};

function Title({name}: {name: string}) {
return (
<Typography.Title style={{padding: 4}} level={3} key={name}>
{name}
</Typography.Title>
);
}

function VirtualDeviceRow({
isFavorite,
name,
addToFavorites,
removeFromFavorites,
children,
}: {
children: ReactNode;
isFavorite: boolean;
name: string;
addToFavorites: (deviceName: string) => void;
removeFromFavorites: (deviceName: string) => void;
}) {
return (
<Layout.Horizontal gap="medium" center grow key={name}>
{children}
{isFavorite ? (
<HeartFilled
testing-id="favorite"
aria-label="favorite"
onClick={() => {
removeFromFavorites(name);
}}
style={FavIconStyle}
/>
) : (
<HeartOutlined
testing-id="not-favorite"
aria-label="not-favorite"
onClick={() => {
addToFavorites(name);
}}
style={FavIconStyle}
/>
)}
</Layout.Horizontal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {LaunchEmulatorDialog} from '../LaunchEmulator';
import {createRootReducer} from '../../../reducers';
import {sleep} from 'flipper-plugin';
import {getRenderHostInstance} from 'flipper-frontend-core';
import {last} from 'lodash';

test('Can render and launch android apps - no emulators', async () => {
const store = createStore(createRootReducer());
Expand Down Expand Up @@ -43,11 +44,12 @@ test('Can render and launch android apps - no emulators', async () => {
</Provider>,
);

expect(await renderer.findByText(/No emulators/)).toMatchInlineSnapshot(`
expect(await renderer.findByText(/No virtual devices/))
.toMatchInlineSnapshot(`
<div
class="ant-alert-message"
>
No emulators available.
No virtual devices available.
<br />
<a
class="ant-typography"
Expand Down Expand Up @@ -126,6 +128,12 @@ test('Can render and launch android apps', async () => {

expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(`
[
<h3
class="ant-typography"
style="padding: 4px;"
>
Android emulators
</h3>,
<span>
emulator1
</span>,
Expand All @@ -152,3 +160,69 @@ test('Can render and launch android apps', async () => {
]
`);
});

test('Favouriting a virtual device brings it to the top', async () => {
const store = createStore(createRootReducer());

const exec = jest.fn().mockImplementation(async (cmd) => {
if (cmd === 'android-get-emulators') {
return ['emulator1', 'emulator2'];
}
});

getRenderHostInstance().flipperServer.exec = exec;

store.dispatch({
type: 'UPDATE_SETTINGS',
payload: {
...store.getState().settingsState,
enableAndroid: true,
},
});
const onClose = jest.fn();

const renderer = render(
<Provider store={store}>
<LaunchEmulatorDialog onClose={onClose} />
</Provider>,
);

await sleep(1); // give exec time to resolve

expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(`
[
<h3
class="ant-typography"
style="padding: 4px;"
>
Android emulators
</h3>,
<span>
emulator1
</span>,
<span>
emulator2
</span>,
]
`);

const lastFavourite = last(renderer.getAllByLabelText('not-favorite'))!;
fireEvent.click(lastFavourite);

expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(`
[
<h3
class="ant-typography"
style="padding: 4px;"
>
Android emulators
</h3>,
<span>
emulator2
</span>,
<span>
emulator1
</span>,
]
`);
});

0 comments on commit d52eeff

Please sign in to comment.