Files
flipper/desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx
Michel Weststrate ef6e802244 Some Client related cleanups
Summary:
Client up `client.device` (which had no code references anymore) / `client.deviceSync`. Cleaned up feature code for old SDKs (pre 2, which is 3 years old).

This makes decapitating Client a little simpler in the rest of the stack.

Reviewed By: passy

Differential Revision: D31235436

fbshipit-source-id: 919679c1830e2b9368d0787d7b363c090305edb8
2021-09-29 07:01:18 -07:00

315 lines
8.2 KiB
TypeScript

/**
* 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,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import {Glyph, Layout, styled} from '../../ui';
import {DeviceOS, theme, useTrackedCallback, useValue} from 'flipper-plugin';
import {batch} from 'react-redux';
import {useDispatch, useStore} from '../../utils/useStore';
import {
canBeDefaultDevice,
getAvailableClients,
selectClient,
selectDevice,
} from '../../reducers/connections';
import BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client';
import {State} from '../../reducers';
import {brandColors, brandIcons, colors} from '../../ui/components/colors';
import {TroubleshootingGuide} from './fb-stubs/TroubleshootingGuide';
import GK from '../../fb-stubs/GK';
const {Text} = Typography;
function getOsIcon(os?: DeviceOS) {
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);
useValue(selectedDevice?.connected, false); // subscribe to future archived state changes
const onSelectDevice = useTrackedCallback(
'select-device',
(device: BaseDevice) => {
batch(() => {
dispatch(selectDevice(device));
dispatch(selectClient(null));
});
},
[],
);
const onSelectApp = useTrackedCallback(
'select-app',
(device: BaseDevice, client: Client) => {
batch(() => {
dispatch(selectDevice(device));
dispatch(selectClient(client.id));
});
},
[],
);
const entries = computeEntries(
devices,
clients,
uninitializedClients,
onSelectDevice,
onSelectApp,
);
const client = clients.find((client) => client.id === selectedApp);
return (
<>
{entries.length ? (
<Radio.Group
value={selectedApp}
size="small"
style={{
display: 'flex',
flex: 1,
}}>
<Dropdown
trigger={['click']}
overlay={
<Menu selectedKeys={selectedApp ? [selectedApp] : []}>
{entries}
</Menu>
}>
<AppInspectButton title="Select the device / app to inspect">
<Layout.Horizontal gap center>
<AppIcon appname={client?.query.app} device={selectedDevice} />
<Layout.Container grow shrink>
<Text strong>{client?.query.app ?? ''}</Text>
<Text>{selectedDevice?.title || 'Available devices'}</Text>
</Layout.Container>
<CaretDownOutlined />
</Layout.Horizontal>
</AppInspectButton>
</Dropdown>
</Radio.Group>
) : (
<Layout.Horizontal gap center>
<ExclamationCircleOutlined style={{color: theme.warningColor}} />
<Text
type="secondary"
style={{
textTransform: 'uppercase',
fontSize: '0.8em',
color: theme.errorColor,
}}>
No devices found
</Text>
</Layout.Horizontal>
)}
<TroubleshootingGuide
showGuide={GK.get('flipper_self_sufficiency')}
devicesDetected={entries.length}
/>
</>
);
}
const AppInspectButton = styled(Button)({
background: theme.backgroundTransparentHover,
height: 52,
border: 'none',
fontWeight: 'normal',
flex: `1 1 0`,
overflow: 'hidden', // required for ellipsis
paddingLeft: theme.space.small,
paddingRight: theme.space.small,
textAlign: 'left',
'&:hover, &:focus, &:active': {
background: theme.backgroundTransparentHover,
},
'.ant-typography': {
lineHeight: '20px',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
});
function AppIcon({
appname,
device,
}: {
appname?: string;
device?: BaseDevice | null;
}) {
const invert = appname?.endsWith('Lite') ?? false;
const brandName = appname?.replace(/ Lite$/, '');
const color = brandName
? getColorByApp(brandName)
: theme.backgroundTransparentHover;
const icon = (brandName && (brandIcons as any)[brandName]) ?? device?.icon;
return (
<AppIconContainer style={{background: invert ? 'white' : color}}>
{icon && (
<Glyph
name={icon}
size={24}
variant="outline"
color={invert ? color : 'white'}
/>
)}
</AppIconContainer>
);
}
const AppIconContainer = styled.div({
borderRadius: 4,
width: 36,
height: 36,
padding: 6,
});
function computeEntries(
devices: BaseDevice[],
clients: Client[],
uninitializedClients: State['connections']['uninitializedClients'],
onSelectDevice: (device: BaseDevice) => void,
onSelectApp: (device: BaseDevice, client: Client) => void,
) {
const entries = devices
.filter(
(device) =>
// hide non default devices, unless they have a connected client or plugins
canBeDefaultDevice(device) ||
device.hasDevicePlugins ||
clients.some((c) => c.device === device),
)
.map((device) => {
const deviceEntry = (
<Menu.Item
icon={getOsIcon(device.os)}
key={device.serial}
style={{fontWeight: 'bold'}}
onClick={() => {
onSelectDevice(device);
}}>
<DeviceTitle device={device} />
</Menu.Item>
);
const clientEntries = getAvailableClients(device, clients).map(
(client) => (
<Menu.Item
key={client.id}
onClick={() => {
onSelectApp(device, client);
}}>
<Radio value={client.id}>
<ClientTitle client={client} />
</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.appName}>
{`${client.appName} (${client.deviceName})`}
</Menu.Item>
)),
]);
}
return entries.flat();
}
function DeviceTitle({device}: {device: BaseDevice}) {
const connected = useValue(device.connected);
const isImported = device.isArchived;
return (
<span>
<>{device.title} </>
{!connected || isImported ? (
<span
style={{
textTransform: 'uppercase',
fontSize: '0.6em',
color: isImported ? theme.primaryColor : theme.errorColor,
fontWeight: 'bold',
}}>
{isImported ? '(Imported)' : '(Offline)'}
</span>
) : null}
</span>
);
}
function ClientTitle({client}: {client: Client}) {
const connected = useValue(client.connected);
return (
<span>
<>{client.query.app} </>
{!connected ? (
<span
style={{
textTransform: 'uppercase',
fontSize: '0.6em',
color: theme.errorColor,
fontWeight: 'bold',
}}>
(Offline)
</span>
) : null}
</span>
);
}
function getColorByApp(app?: string | null): string {
let iconColor: string | undefined = (brandColors as any)[app!];
if (!iconColor) {
if (!app) {
// Device plugin
iconColor = colors.macOSTitleBarIconBlur;
} else {
const pluginColors = [
colors.seaFoam,
colors.teal,
colors.lime,
colors.lemon,
colors.orange,
colors.tomato,
colors.cherry,
colors.pink,
colors.grape,
];
iconColor = pluginColors[parseInt(app, 36) % pluginColors.length];
}
}
return iconColor;
}