Device Dropdown
Summary: This diff adds the device switcher, filling the switcher with apps and devices. Reviewed By: cekkaewnumchai Differential Revision: D24053787 fbshipit-source-id: 4f69835a12eec90a0e5704f71c8ceed5509f61ce
This commit is contained in:
committed by
Facebook GitHub Bot
parent
79ac1ef009
commit
c38a4413dc
@@ -103,10 +103,10 @@ const demos: PreviewProps[] = [
|
|||||||
background: theme.successColor,
|
background: theme.successColor,
|
||||||
}}></Layout.Container>
|
}}></Layout.Container>
|
||||||
),
|
),
|
||||||
'bordered padded rounded': (
|
'bordered pad rounded': (
|
||||||
<Layout.Container
|
<Layout.Container
|
||||||
bordered
|
bordered
|
||||||
padded
|
pad
|
||||||
rounded
|
rounded
|
||||||
style={{background: theme.backgroundDefault, width: 200}}>
|
style={{background: theme.backgroundDefault, width: 200}}>
|
||||||
<div style={demoStyle.square}>child</div>
|
<div style={demoStyle.square}>child</div>
|
||||||
@@ -152,8 +152,8 @@ const demos: PreviewProps[] = [
|
|||||||
{aDynamicBox}
|
{aDynamicBox}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
),
|
),
|
||||||
'Using flags: padded center gap={8} (great for toolbars and such)': (
|
'Using flags: pad center gap={8} (great for toolbars and such)': (
|
||||||
<Layout.Horizontal padded center gap={8}>
|
<Layout.Horizontal pad center gap={8}>
|
||||||
{aButton}
|
{aButton}
|
||||||
{someText}
|
{someText}
|
||||||
{aBox}
|
{aBox}
|
||||||
@@ -187,8 +187,8 @@ const demos: PreviewProps[] = [
|
|||||||
{aDynamicBox}
|
{aDynamicBox}
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
),
|
),
|
||||||
'Using flags: padded center gap={8} (great for toolbars and such)': (
|
'Using flags: pad center gap (great for toolbars and such)': (
|
||||||
<Layout.Vertical padded center gap={8}>
|
<Layout.Vertical pad center gap>
|
||||||
{aButton}
|
{aButton}
|
||||||
{someText}
|
{someText}
|
||||||
{aBox}
|
{aBox}
|
||||||
@@ -207,6 +207,11 @@ const demos: PreviewProps[] = [
|
|||||||
'boolean (false)',
|
'boolean (false)',
|
||||||
'If set, the area of the second child will automatically be made scrollable.',
|
'If set, the area of the second child will automatically be made scrollable.',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'center',
|
||||||
|
'boolean (false)',
|
||||||
|
'If set, all children will use their own height, and they will be centered vertically in the layout. If not set, all children will be stretched to the height of the layout.',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
demos: {
|
demos: {
|
||||||
'Layout.Top': (
|
'Layout.Top': (
|
||||||
|
|||||||
@@ -8,18 +8,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Button, Dropdown, Menu, Radio, Input} from 'antd';
|
import {Alert, Button, Input} from 'antd';
|
||||||
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
||||||
import {
|
import {SettingOutlined, RocketOutlined} from '@ant-design/icons';
|
||||||
AppleOutlined,
|
|
||||||
AndroidOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
RocketOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import {Layout, Link} from '../../ui';
|
import {Layout, Link} from '../../ui';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
import {useStore as useReduxStore} from 'react-redux';
|
import {useStore as useReduxStore} from 'react-redux';
|
||||||
import {showEmulatorLauncher} from './LaunchEmulator';
|
import {showEmulatorLauncher} from './LaunchEmulator';
|
||||||
|
import {AppSelector} from './AppSelector';
|
||||||
|
import {useStore} from '../../utils/useStore';
|
||||||
|
|
||||||
const appTooltip = (
|
const appTooltip = (
|
||||||
<>
|
<>
|
||||||
@@ -34,6 +31,7 @@ const appTooltip = (
|
|||||||
|
|
||||||
export function AppInspect() {
|
export function AppInspect() {
|
||||||
const store = useReduxStore();
|
const store = useReduxStore();
|
||||||
|
const selectedDevice = useStore((state) => state.connections.selectedDevice);
|
||||||
return (
|
return (
|
||||||
<LeftSidebar>
|
<LeftSidebar>
|
||||||
<Layout.Top scrollable>
|
<Layout.Top scrollable>
|
||||||
@@ -42,7 +40,7 @@ export function AppInspect() {
|
|||||||
App Inspect
|
App Inspect
|
||||||
</SidebarTitle>
|
</SidebarTitle>
|
||||||
<Layout.Vertical padv="small" padh="medium" gap={theme.space.large}>
|
<Layout.Vertical padv="small" padh="medium" gap={theme.space.large}>
|
||||||
<DeviceDropdown />
|
<AppSelector />
|
||||||
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
|
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
|
||||||
<Layout.Horizontal gap>
|
<Layout.Horizontal gap>
|
||||||
<Button icon={<SettingOutlined />} type="link" />
|
<Button icon={<SettingOutlined />} type="link" />
|
||||||
@@ -58,36 +56,22 @@ export function AppInspect() {
|
|||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
<Layout.Container>Dynamic section</Layout.Container>
|
<Layout.Container padv={theme.space.small}>
|
||||||
|
{selectedDevice ? (
|
||||||
|
<PluginList />
|
||||||
|
) : (
|
||||||
|
<Alert message="No device or app selected" type="info" />
|
||||||
|
)}
|
||||||
|
</Layout.Container>
|
||||||
</Layout.Top>
|
</Layout.Top>
|
||||||
</LeftSidebar>
|
</LeftSidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeviceDropdown() {
|
function PluginList() {
|
||||||
return (
|
return (
|
||||||
<Radio.Group value={1} size="small">
|
<Layout.Container>
|
||||||
<Dropdown
|
<SidebarTitle>Plugins</SidebarTitle>
|
||||||
overlay={
|
</Layout.Container>
|
||||||
<Menu>
|
|
||||||
<Menu.Item icon={<AppleOutlined />} style={{fontWeight: 'bold'}}>
|
|
||||||
IPhone 11
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<Radio value={1}>Facebook</Radio>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<Radio value={3}>Instagram</Radio>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item icon={<AndroidOutlined />} style={{fontWeight: 'bold'}}>
|
|
||||||
Android
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
}>
|
|
||||||
<Button icon={<AppleOutlined />} style={{width: '100%'}}>
|
|
||||||
Facebook Iphone11
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Radio.Group>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
162
desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx
Normal file
162
desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {Button, Dropdown, Menu, Radio, Typography} from 'antd';
|
||||||
|
import {
|
||||||
|
AppleOutlined,
|
||||||
|
AndroidOutlined,
|
||||||
|
WindowsOutlined,
|
||||||
|
CaretDownOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import {Layout, styled} from '../../ui';
|
||||||
|
import {theme} from '../theme';
|
||||||
|
import {batch} from 'react-redux';
|
||||||
|
import {Dispatch, useDispatch, useStore} from '../../utils/useStore';
|
||||||
|
import {
|
||||||
|
canBeDefaultDevice,
|
||||||
|
getAvailableClients,
|
||||||
|
selectClient,
|
||||||
|
selectDevice,
|
||||||
|
} from '../../reducers/connections';
|
||||||
|
import BaseDevice, {OS} from '../../devices/BaseDevice';
|
||||||
|
import {getColorByApp} from '../../chrome/mainsidebar/sidebarUtils';
|
||||||
|
import Client from '../../Client';
|
||||||
|
import {State} from '../../reducers';
|
||||||
|
|
||||||
|
const {Text} = Typography;
|
||||||
|
|
||||||
|
function getOsIcon(os?: OS) {
|
||||||
|
switch (os) {
|
||||||
|
case 'iOS':
|
||||||
|
return <AppleOutlined />;
|
||||||
|
case 'Android':
|
||||||
|
return <AndroidOutlined />;
|
||||||
|
case 'Windows':
|
||||||
|
return <WindowsOutlined />;
|
||||||
|
default:
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppSelector() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const {
|
||||||
|
devices,
|
||||||
|
selectedDevice,
|
||||||
|
clients,
|
||||||
|
uninitializedClients,
|
||||||
|
selectedApp,
|
||||||
|
} = useStore((state) => state.connections);
|
||||||
|
const entries = computeEntries(
|
||||||
|
devices,
|
||||||
|
dispatch,
|
||||||
|
clients,
|
||||||
|
uninitializedClients,
|
||||||
|
);
|
||||||
|
const client = clients.find((client) => client.id === selectedApp);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group value={selectedApp} size="small">
|
||||||
|
<Dropdown
|
||||||
|
overlay={
|
||||||
|
<Menu selectedKeys={selectedApp ? [selectedApp] : []}>
|
||||||
|
{entries.flat()}
|
||||||
|
</Menu>
|
||||||
|
}>
|
||||||
|
<AppInspectButton title="Select the device / app to inspect">
|
||||||
|
<Layout.Horizontal gap center>
|
||||||
|
<AppIcon appname={client?.query.app} />
|
||||||
|
<Layout.Vertical>
|
||||||
|
<Text strong>{client?.query.app ?? ''}</Text>
|
||||||
|
<Text>
|
||||||
|
{selectedDevice?.displayTitle() || 'Available devices'}
|
||||||
|
</Text>
|
||||||
|
</Layout.Vertical>
|
||||||
|
<CaretDownOutlined />
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</AppInspectButton>
|
||||||
|
</Dropdown>
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppInspectButton = styled(Button)({
|
||||||
|
background: theme.backgroundTransparentHover,
|
||||||
|
height: 52,
|
||||||
|
border: 'none',
|
||||||
|
width: '100%',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
paddingLeft: theme.space.small,
|
||||||
|
paddingRight: theme.space.small,
|
||||||
|
textAlign: 'left',
|
||||||
|
'&:hover, &:focus, &:active': {
|
||||||
|
background: theme.backgroundTransparentHover,
|
||||||
|
},
|
||||||
|
'.ant-typography': {
|
||||||
|
lineHeight: '20px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const AppIcon = styled.div<{appname?: string}>(({appname}) => ({
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
background: appname
|
||||||
|
? getColorByApp(appname)
|
||||||
|
: theme.backgroundTransparentHover,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function computeEntries(
|
||||||
|
devices: BaseDevice[],
|
||||||
|
dispatch: Dispatch,
|
||||||
|
clients: Client[],
|
||||||
|
uninitializedClients: State['connections']['uninitializedClients'],
|
||||||
|
) {
|
||||||
|
const entries = devices.filter(canBeDefaultDevice).map((device) => {
|
||||||
|
const deviceEntry = (
|
||||||
|
<Menu.Item
|
||||||
|
icon={getOsIcon(device.os)}
|
||||||
|
key={device.serial}
|
||||||
|
style={{fontWeight: 'bold'}}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(selectDevice(device));
|
||||||
|
}}>
|
||||||
|
{device.displayTitle()}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
const clientEntries = getAvailableClients(device, clients).map((client) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={client.id}
|
||||||
|
onClick={() => {
|
||||||
|
batch(() => {
|
||||||
|
dispatch(selectDevice(device));
|
||||||
|
dispatch(selectClient(client.id));
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<Radio value={client.id}>{client.query.app}</Radio>
|
||||||
|
</Menu.Item>
|
||||||
|
));
|
||||||
|
return [deviceEntry, ...clientEntries];
|
||||||
|
});
|
||||||
|
if (uninitializedClients.length) {
|
||||||
|
entries.push([
|
||||||
|
<Menu.Item key="connecting" style={{fontWeight: 'bold'}}>
|
||||||
|
Currently connecting...
|
||||||
|
</Menu.Item>,
|
||||||
|
...uninitializedClients.map((client) => (
|
||||||
|
<Menu.Item key={'connecting' + client.client.appName}>
|
||||||
|
{client.client.appName}
|
||||||
|
</Menu.Item>
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ type ContainerProps = {
|
|||||||
borderLeft?: boolean;
|
borderLeft?: boolean;
|
||||||
bordered?: boolean;
|
bordered?: boolean;
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
padded?: boolean;
|
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
} & PaddingProps;
|
} & PaddingProps;
|
||||||
@@ -46,6 +45,7 @@ const Container = styled.div<ContainerProps>(
|
|||||||
height,
|
height,
|
||||||
...rest
|
...rest
|
||||||
}) => ({
|
}) => ({
|
||||||
|
minWidth: `0`, // ensures the Container can shrink smaller than it's largest
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -101,6 +101,7 @@ const Horizontal = styled(Container)<DistributionProps>(({gap, center}) => ({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: normalizeSpace(gap, theme.space.small),
|
gap: normalizeSpace(gap, theme.space.small),
|
||||||
alignItems: center ? 'center' : 'stretch',
|
alignItems: center ? 'center' : 'stretch',
|
||||||
|
minWidth: 'auto', // corrects 0 on Container
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Vertical = styled(Container)<DistributionProps>(({gap, center}) => ({
|
const Vertical = styled(Container)<DistributionProps>(({gap, center}) => ({
|
||||||
@@ -118,6 +119,7 @@ type SplitLayoutProps = {
|
|||||||
/**
|
/**
|
||||||
* If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched.
|
* If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched.
|
||||||
*/
|
*/
|
||||||
|
center?: boolean;
|
||||||
children: [React.ReactNode, React.ReactNode];
|
children: [React.ReactNode, React.ReactNode];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
shallowEqual,
|
shallowEqual,
|
||||||
useDispatch as useDispatchBase,
|
useDispatch as useDispatchBase,
|
||||||
} from 'react-redux';
|
} from 'react-redux';
|
||||||
import {Dispatch} from 'redux';
|
import {Dispatch as ReduxDispatch} from 'redux';
|
||||||
import {State, Actions} from '../reducers/index';
|
import {State, Actions} from '../reducers/index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,9 +27,11 @@ export function useStore<Selected>(
|
|||||||
return useSelector(selector, equalityFn);
|
return useSelector(selector, equalityFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Dispatch = ReduxDispatch<Actions>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strongly typed useDispatch wrapper for the Flipper redux store.
|
* Strongly typed useDispatch wrapper for the Flipper redux store.
|
||||||
*/
|
*/
|
||||||
export function useDispatch(): Dispatch<Actions> {
|
export function useDispatch(): Dispatch {
|
||||||
return useDispatchBase();
|
return useDispatchBase() as any;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user