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 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 {
|
import {
|
||||||
AppleOutlined,
|
|
||||||
PoweroffOutlined,
|
PoweroffOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
AndroidOutlined,
|
HeartOutlined,
|
||||||
|
HeartFilled,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {Store} from '../../reducers';
|
import {Store} from '../../reducers';
|
||||||
import {useStore} from '../../utils/useStore';
|
import {useStore} from '../../utils/useStore';
|
||||||
@@ -22,12 +22,16 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
renderReactRoot,
|
renderReactRoot,
|
||||||
withTrackingScope,
|
withTrackingScope,
|
||||||
|
useLocalStorageState,
|
||||||
|
theme,
|
||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {Provider} from 'react-redux';
|
import {Provider} from 'react-redux';
|
||||||
import {IOSDeviceParams} from 'flipper-common';
|
import {IOSDeviceParams} from 'flipper-common';
|
||||||
import {getRenderHostInstance} from 'flipper-frontend-core';
|
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||||
import SettingsSheet from '../../chrome/SettingsSheet';
|
import SettingsSheet from '../../chrome/SettingsSheet';
|
||||||
import {Link} from '../../ui';
|
import {Link} from '../../ui';
|
||||||
|
import {chain, uniq, without} from 'lodash';
|
||||||
|
import {ReactNode} from 'react-markdown';
|
||||||
|
|
||||||
const COLD_BOOT = 'cold-boot';
|
const COLD_BOOT = 'cold-boot';
|
||||||
|
|
||||||
@@ -90,6 +94,17 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
|||||||
const [waitingForAndroid, setWaitingForAndroid] = useState(androidEnabled);
|
const [waitingForAndroid, setWaitingForAndroid] = useState(androidEnabled);
|
||||||
const waitingForResults = waitingForIos || waitingForAndroid;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!iosEnabled) {
|
if (!iosEnabled) {
|
||||||
return;
|
return;
|
||||||
@@ -131,70 +146,105 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
...(androidEmulators.length > 0
|
androidEmulators.length > 0 ? (
|
||||||
? [<AndroidOutlined key="android logo" />]
|
<Title key="android-title" name="Android emulators" />
|
||||||
: []),
|
) : null,
|
||||||
...androidEmulators.map((name) => {
|
...chain(
|
||||||
const launch = (coldBoot: boolean) => {
|
androidEmulators.map((name) => ({
|
||||||
getRenderHostInstance()
|
name,
|
||||||
.flipperServer.exec('android-launch-emulator', name, coldBoot)
|
isFavorite: favoriteVirtualDevices.includes(name),
|
||||||
.then(onClose)
|
})),
|
||||||
.catch((e) => {
|
)
|
||||||
console.error('Failed to start emulator: ', e);
|
.sortBy((item) => [!item.isFavorite, item.name])
|
||||||
message.error('Failed to start emulator: ' + e);
|
.map(({name, isFavorite}) => {
|
||||||
});
|
const launch = (coldBoot: boolean) => {
|
||||||
};
|
|
||||||
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={() =>
|
|
||||||
getRenderHostInstance()
|
getRenderHostInstance()
|
||||||
.flipperServer.exec('ios-launch-simulator', device.udid)
|
.flipperServer.exec('android-launch-emulator', name, coldBoot)
|
||||||
.catch((e) => {
|
|
||||||
console.warn('Failed to start simulator: ', e);
|
|
||||||
message.error('Failed to start simulator: ' + e);
|
|
||||||
})
|
|
||||||
.then(onClose)
|
.then(onClose)
|
||||||
}>
|
.catch((e) => {
|
||||||
{device.name}
|
console.error('Failed to start emulator: ', e);
|
||||||
</Button>
|
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 = (
|
const loadingSpinner = (
|
||||||
<>
|
<>
|
||||||
{waitingForResults && <Spinner />}
|
{waitingForResults && <Spinner key="spinner" />}
|
||||||
{!waitingForResults && items.length === 0 && (
|
{!waitingForResults && items.length === 0 && (
|
||||||
<Alert
|
<Alert
|
||||||
|
key=" alert-nodevices"
|
||||||
message={
|
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">
|
<Link href="http://fbflipper.com/docs/getting-started/troubleshooting/general/#i-see-no-emulators-available">
|
||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
@@ -210,10 +260,10 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
|||||||
visible
|
visible
|
||||||
centered
|
centered
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
title="Launch Emulator"
|
title="Launch Virtual device"
|
||||||
footer={null}
|
footer={null}
|
||||||
bodyStyle={{maxHeight: 400, height: 400, overflow: 'auto'}}>
|
bodyStyle={{maxHeight: 400, height: 400, overflow: 'auto'}}>
|
||||||
<Layout.Container gap>
|
<Layout.Container gap="medium">
|
||||||
{items.length ? items : <></>}
|
{items.length ? items : <></>}
|
||||||
{loadingSpinner}
|
{loadingSpinner}
|
||||||
</Layout.Container>
|
</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 {createRootReducer} from '../../../reducers';
|
||||||
import {sleep} from 'flipper-plugin';
|
import {sleep} from 'flipper-plugin';
|
||||||
import {getRenderHostInstance} from 'flipper-frontend-core';
|
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||||
|
import {last} from 'lodash';
|
||||||
|
|
||||||
test('Can render and launch android apps - no emulators', async () => {
|
test('Can render and launch android apps - no emulators', async () => {
|
||||||
const store = createStore(createRootReducer());
|
const store = createStore(createRootReducer());
|
||||||
@@ -43,11 +44,12 @@ test('Can render and launch android apps - no emulators', async () => {
|
|||||||
</Provider>,
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await renderer.findByText(/No emulators/)).toMatchInlineSnapshot(`
|
expect(await renderer.findByText(/No virtual devices/))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
<div
|
<div
|
||||||
class="ant-alert-message"
|
class="ant-alert-message"
|
||||||
>
|
>
|
||||||
No emulators available.
|
No virtual devices available.
|
||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
class="ant-typography"
|
class="ant-typography"
|
||||||
@@ -126,6 +128,12 @@ test('Can render and launch android apps', async () => {
|
|||||||
|
|
||||||
expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(`
|
expect(await renderer.findAllByText(/emulator/)).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
|
<h3
|
||||||
|
class="ant-typography"
|
||||||
|
style="padding: 4px;"
|
||||||
|
>
|
||||||
|
Android emulators
|
||||||
|
</h3>,
|
||||||
<span>
|
<span>
|
||||||
emulator1
|
emulator1
|
||||||
</span>,
|
</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