Add ability to favourite virtual devices
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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a0d6c9a1b8
commit
d52eeffb86
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -210,10 +260,10 @@ 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>
|
||||
@@ -221,3 +271,52 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
@@ -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"
|
||||
@@ -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>,
|
||||
@@ -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>,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user