Introduce AppInspect

Summary:
This diff introduces the AppInspect pane.

I didn't get very fare, and it is markup only, but while at it made a bunch of other improvement in the component lib, so figured to prematurely submit to don't make the diffs too big.

Improvements
- Separated sidebar and Layout.X, as it was to much responsibility for one component, and made customization hard. Also caused state loss when switching between resizable and not.
- Setup a basic top level selection. Maybe will move it into redux in the future, but for now it suffices.
- Introduced Layout.Horizontal and `Layout.Vertical` as alternative to Ant design's space. The reason is that the latter can't stretching children, which we use quite frequently. (that is because they use wrappers to create spacing, but since we run on Electron, we can use CSS `gap` instead, which handles that much more elegantly).
- Fixed issue where gutter handle could disappear when dragging to far.

Reviewed By: cekkaewnumchai

Differential Revision: D23867265

fbshipit-source-id: e872b7f48b868e255f2c34d45e811b8ed93d0b00
This commit is contained in:
Michel Weststrate
2020-09-28 01:40:50 -07:00
committed by Facebook GitHub Bot
parent 2cbcbd1480
commit aaabe1cc82
7 changed files with 369 additions and 110 deletions

View File

@@ -0,0 +1,98 @@
/**
* 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, Input} from 'antd';
import {shell} from 'electron';
import {LeftSidebar, SidebarTitle, InfoIcon} from './LeftSidebar';
import {
AppleOutlined,
AndroidOutlined,
SettingOutlined,
} from '@ant-design/icons';
import {Layout, styled} from '../ui';
import {theme} from './theme';
const appTooltip = (
<>
Inspect apps by selecting connected devices and emulators. Navigate and
bookmark frequent destinations in the app. Refresh, screenshot and
screenrecord is also available.
{
<Button
size="small"
type="link"
onClick={() => {
shell.openExternal(
'https://fbflipper.com/docs/getting-started/index',
);
}}>
Learn More
</Button>
}
</>
);
export function AppInspect() {
return (
<LeftSidebar>
<Layout.Top scrollable>
<>
<SidebarTitle actions={<InfoIcon>{appTooltip}</InfoIcon>}>
App Inspect
</SidebarTitle>
<TopSection fillx>
<DeviceDropdown />
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
<Layout.Horizontal gap={theme.space.small}>
<Button icon={<SettingOutlined />} type="link" />
<Button icon={<SettingOutlined />} type="link" />
<Button icon={<SettingOutlined />} type="link" />
</Layout.Horizontal>
</TopSection>
</>
<div>Dynamic section</div>
</Layout.Top>
</LeftSidebar>
);
}
const TopSection = styled(Layout.Vertical)({
boxShadow: `inset 0px -1px 0px ${theme.dividerColor}`,
padding: `8px 12px`,
gap: theme.space.middle,
});
function DeviceDropdown() {
return (
<Radio.Group value={1} size="small">
<Dropdown
overlay={
<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>
);
}

View File

@@ -27,9 +27,8 @@ import {toggleLeftSidebarVisible} from '../reducers/application';
import {theme} from './theme';
import SettingsSheet from '../chrome/SettingsSheet';
import WelcomeScreen from './WelcomeScreen';
import {isStaticViewActive} from '../chrome/mainsidebar/sidebarUtils';
import {ConsoleLogs, errorCounterAtom} from '../chrome/ConsoleLogs';
import {setStaticView} from '../reducers/connections';
import {errorCounterAtom} from '../chrome/ConsoleLogs';
import {ToplevelProps} from './SandyApp';
import {useValue} from 'flipper-plugin';
const LeftRailContainer = styled(FlexColumn)({
@@ -101,11 +100,21 @@ const LeftRailDivider = styled(Divider)({
});
LeftRailDivider.displayName = 'LeftRailDividier';
export function LeftRail() {
export function LeftRail({
toplevelSelection,
setToplevelSelection,
}: ToplevelProps) {
return (
<LeftRailContainer>
<LeftRailSection>
<LeftRailButton icon={<MobileFilled />} title="App Inspect" />
<LeftRailButton
icon={<MobileFilled />}
title="App Inspect"
selected={toplevelSelection === 'appinspect'}
onClick={() => {
setToplevelSelection('appinspect');
}}
/>
<LeftRailButton icon={<AppstoreOutlined />} title="Plugin Manager" />
<LeftRailButton
count={8}
@@ -113,7 +122,10 @@ export function LeftRail() {
title="Notifications"
/>
<LeftRailDivider />
<DebugLogsButton />
<DebugLogsButton
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
</LeftRailSection>
<LeftRailSection>
<LeftRailButton
@@ -160,20 +172,19 @@ function LeftSidebarToggleButton() {
);
}
function DebugLogsButton() {
const staticView = useStore((state) => state.connections.staticView);
const active = isStaticViewActive(staticView, ConsoleLogs);
function DebugLogsButton({
toplevelSelection,
setToplevelSelection,
}: ToplevelProps) {
const errorCount = useValue(errorCounterAtom);
const dispatch = useDispatch();
return (
<LeftRailButton
icon={<FileExclamationOutlined />}
title="Flipper Logs"
selected={active}
selected={toplevelSelection === 'flipperlogs'}
count={errorCount}
onClick={() => {
dispatch(setStaticView(ConsoleLogs));
setToplevelSelection('flipperlogs');
}}
/>
);
@@ -200,9 +211,9 @@ function ShowSettingsButton() {
function WelcomeScreenButton() {
const settings = useStore((state) => state.settingsState);
const showWelcomeScreenAtStartup = settings.showWelcomeAtStartup;
const {showWelcomeAtStartup} = settings;
const dispatch = useDispatch();
const [visible, setVisible] = useState(showWelcomeScreenAtStartup);
const [visible, setVisible] = useState(showWelcomeAtStartup);
return (
<>
@@ -215,7 +226,7 @@ function WelcomeScreenButton() {
<WelcomeScreen
visible={visible}
onClose={() => setVisible(false)}
showAtStartup={showWelcomeScreenAtStartup}
showAtStartup={showWelcomeAtStartup}
onCheck={(value) =>
dispatch({
type: 'UPDATE_SETTINGS',

View File

@@ -0,0 +1,56 @@
/**
* 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 {theme} from './theme';
import styled from '@emotion/styled';
import {Layout, FlexColumn} from '../ui';
import {Button, Tooltip} from 'antd';
import {InfoCircleOutlined} from '@ant-design/icons';
export const LeftSidebar = styled(FlexColumn)({
background: theme.backgroundDefault,
flex: 1,
padding: `10px 0`,
});
export function SidebarTitle({
children,
actions,
}: {
children: React.ReactNode;
actions?: React.ReactNode;
}) {
return (
<LeftMenuTitle>
<Layout.Right center>
{children}
<>{actions}</>
</Layout.Right>
</LeftMenuTitle>
);
}
const LeftMenuTitle = styled.div({
width: '100%',
fontFamily: 'SF Pro Text',
padding: `0px 12px`,
lineHeight: '16px',
fontSize: '12px',
textTransform: 'uppercase',
});
export const InfoIcon: React.FC<{}> = ({children}) => (
<Tooltip placement="bottom" title={children} mouseEnterDelay={0.5}>
<Button
size="small"
type="link"
icon={<InfoCircleOutlined color={theme.textColorSecondary} />}></Button>
</Tooltip>
);

View File

@@ -7,10 +7,10 @@
* @format
*/
import React, {useEffect} from 'react';
import React, {useEffect, useState, useCallback} from 'react';
import {styled} from 'flipper';
import {DatePicker, Space} from 'antd';
import {Layout, FlexRow} from '../ui';
import {Layout, FlexRow, Sidebar} from '../ui';
import {theme} from './theme';
import {Logger} from '../fb-interfaces/Logger';
@@ -18,39 +18,87 @@ import {LeftRail} from './LeftRail';
import {TemporarilyTitlebar} from './TemporarilyTitlebar';
import TypographyExample from './TypographyExample';
import {registerStartupTime} from '../App';
import {useStore} from '../utils/useStore';
import {useStore, useDispatch} from '../utils/useStore';
import {SandyContext} from './SandyContext';
import {ConsoleLogs} from '../chrome/ConsoleLogs';
import {setStaticView} from '../reducers/connections';
import {toggleLeftSidebarVisible} from '../reducers/application';
import {AppInspect} from './AppInspect';
export type ToplevelNavItem = 'appinspect' | 'flipperlogs' | undefined;
export type ToplevelProps = {
toplevelSelection: ToplevelNavItem;
setToplevelSelection: (_newSelection: ToplevelNavItem) => void;
};
export function SandyApp({logger}: {logger: Logger}) {
const dispatch = useDispatch();
const leftSidebarVisible = useStore(
(state) => state.application.leftSidebarVisible,
);
const staticView = useStore((state) => state.connections.staticView);
/**
* top level navigation uses two pieces of state, selection stored here, and selection that is based on what is stored in the reducer (which might be influenced by redux action dispatches to different means).
* The logic here is to sync both, but without modifying the navigation related reducers to not break classic Flipper.
* It is possible to simplify this in the future.
*/
const [toplevelSelection, setStoredToplevelSelection] = useState<
ToplevelNavItem
>('appinspect');
// Handle toplevel nav clicks from LeftRail
const setToplevelSelection = useCallback(
(newSelection: ToplevelNavItem) => {
// toggle sidebar visibility if needed
const hasLeftSidebar = newSelection === 'appinspect';
if (hasLeftSidebar) {
if (newSelection === toplevelSelection) {
dispatch(toggleLeftSidebarVisible());
} else {
dispatch(toggleLeftSidebarVisible(true));
}
}
switch (newSelection) {
case 'flipperlogs':
dispatch(setStaticView(ConsoleLogs));
break;
default:
}
setStoredToplevelSelection(newSelection);
},
[dispatch, toplevelSelection],
);
useEffect(() => {
registerStartupTime(logger);
// don't warn about logger, even with a new logger we don't want to re-register
// eslint-disable-next-line
}, []);
const mainMenuVisible = useStore(
(state) =>
state.application.leftSidebarVisible && !state.connections.staticView,
);
const staticView = useStore((state) => state.connections.staticView);
const leftMenuContent =
leftSidebarVisible && toplevelSelection === 'appinspect' ? (
<AppInspect />
) : null;
return (
<SandyContext.Provider value={true}>
<Layout.Top>
<TemporarilyTitlebar />
<Layout.Left
initialSize={mainMenuVisible ? 348 : undefined}
minSize={200}>
<LeftMenu collapsed={!mainMenuVisible}>
<LeftRail />
{mainMenuVisible && (
<div style={{background: theme.backgroundDefault, width: '100%'}}>
LeftMenu
</div>
)}
</LeftMenu>
<Layout.Left>
<LeftSidebarContainer>
<LeftRail
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<Sidebar width={348} minWidth={220} maxWidth={800} gutter>
{leftMenuContent && (
<LeftMenuContainer>{leftMenuContent}</LeftMenuContainer>
)}
</Sidebar>
</LeftSidebarContainer>
<MainContainer>
<Layout.Right initialSize={300} minSize={200}>
<Layout.Right>
<MainContentWrapper>
<ContentContainer>
{staticView ? (
@@ -62,11 +110,18 @@ export function SandyApp({logger}: {logger: Logger}) {
)}
</ContentContainer>
</MainContentWrapper>
<MainContentWrapper>
<ContentContainer>
<RightMenu />
</ContentContainer>
</MainContentWrapper>
<Sidebar
width={300}
minWidth={220}
maxWidth={800}
gutter
position="right">
<MainContentWrapper>
<ContentContainer>
<RightMenu />
</ContentContainer>
</MainContentWrapper>
</Sidebar>
</Layout.Right>
</MainContainer>
</Layout.Left>
@@ -75,13 +130,19 @@ export function SandyApp({logger}: {logger: Logger}) {
);
}
const LeftMenu = styled(FlexRow)<{collapsed: boolean}>(({collapsed}) => ({
background: theme.backgroundWash,
boxShadow: collapsed ? undefined : `1px 0px 0px ${theme.dividerColor}`,
paddingRight: collapsed ? theme.space.middle : 0,
const LeftMenuContainer = styled.div({
background: theme.backgroundDefault,
paddingRight: 1, // to see the boxShadow
boxShadow: 'inset -1px 0px 0px rgba(0, 0, 0, 0.1)',
height: '100%',
width: '100%',
}));
});
const LeftSidebarContainer = styled(FlexRow)({
background: theme.backgroundWash,
height: '100%',
width: '100%',
});
const MainContainer = styled('div')({
display: 'flex',
@@ -97,7 +158,7 @@ export const ContentContainer = styled('div')({
padding: 0,
background: theme.backgroundDefault,
border: `1px solid ${theme.dividerColor}`,
borderRadius: theme.space.small,
borderRadius: theme.containerBorderRadius,
boxShadow: `0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.05)`,
});

View File

@@ -25,8 +25,10 @@ export const theme = {
backgroundTransparentHover: 'var(--flipper-background-transparent-hover)',
dividerColor: 'var(--flipper-divider-color)',
borderRadius: 'var(--flipper-border-radius)',
containerBorderRadius: 8,
space: {
// from Space component in Ant
tiny: 4,
small: 8,
middle: 16,
large: 24,

View File

@@ -9,20 +9,16 @@
import React from 'react';
import styled from '@emotion/styled';
import {Sidebar} from '..';
type Props = {
type SplitLayoutProps = {
/**
* If set, the dynamically sized pane will get scrollbars when needed
*/
scrollable?: boolean;
/**
* If set, the 'fixed' child will no longer be sized based on it's own dimensions,
* but rather it will be possible to resize it
* If set, items will be centered over the orthogonal direction, if false (the default) items will be stretched.
*/
initialSize?: number;
minSize?: number;
center?: boolean;
children: [React.ReactNode, React.ReactNode];
};
@@ -42,18 +38,21 @@ const ScrollContainer = styled('div')<{scrollable: boolean}>(
);
ScrollContainer.displayName = 'Layout:ScrollContainer';
const Container = styled('div')<{horizontal: boolean}>(({horizontal}) => ({
display: 'flex',
flex: 'auto',
flexDirection: horizontal ? 'row' : 'column',
height: '100%',
width: '100%',
overflow: 'hidden',
}));
const Container = styled('div')<{horizontal: boolean; center?: boolean}>(
({horizontal, center}) => ({
display: 'flex',
flex: 'auto',
flexDirection: horizontal ? 'row' : 'column',
height: '100%',
width: '100%',
overflow: 'hidden',
alignItems: center ? 'center' : undefined,
}),
);
Container.displayName = 'Layout:Container';
function renderLayout(
{children, scrollable, initialSize, minSize}: Props,
{children, scrollable, center}: SplitLayoutProps,
horizontal: boolean,
reverse: boolean,
) {
@@ -62,44 +61,45 @@ function renderLayout(
}
const fixedChild = reverse ? children[1] : children[0];
const fixedElement =
initialSize === undefined ? (
<FixedContainer>{fixedChild}</FixedContainer>
) : horizontal ? (
<Sidebar
position={reverse ? 'right' : 'left'}
width={initialSize}
minWidth={minSize}
gutter>
{fixedChild}
</Sidebar>
) : (
<Sidebar
position={reverse ? 'bottom' : 'top'}
height={initialSize}
minHeight={minSize}
gutter>
{fixedChild}
</Sidebar>
);
const fixedElement = <FixedContainer>{fixedChild}</FixedContainer>;
const dynamicElement = (
<ScrollContainer scrollable={!!scrollable}>
{reverse ? children[0] : children[1]}
</ScrollContainer>
);
return reverse ? (
<Container horizontal={horizontal}>
<Container horizontal={horizontal} center={center}>
{dynamicElement}
{fixedElement}
</Container>
) : (
<Container horizontal={horizontal}>
<Container horizontal={horizontal} center={center}>
{fixedElement}
{dynamicElement}
</Container>
);
}
type DistributionProps = {
/**
* Gab between individual items
*/
gap?: number;
/**
* If set, items will be aligned in the center, if false (the default) items will be stretched.
*/
center?: boolean;
/**
* If set, the layout will fill out to maximum width
*/
fillx?: boolean;
/**
* If set, the layout will fill out to maximum height
*/
filly?: boolean;
};
/**
* The Layout component divides all available screenspace over two components:
* A fixed top (or left) component, and all remaining space to a bottom component.
@@ -111,7 +111,11 @@ function renderLayout(
*
* Use Layout.Top / Right / Bottom / Left to indicate where the fixed element should live.
*/
const Layout: Record<'Left' | 'Right' | 'Top' | 'Bottom', React.FC<Props>> = {
const Layout: Record<
'Left' | 'Right' | 'Top' | 'Bottom',
React.FC<SplitLayoutProps>
> &
Record<'Horizontal' | 'Vertical', React.FC<DistributionProps>> = {
Top(props) {
return renderLayout(props, false, false);
},
@@ -124,11 +128,26 @@ const Layout: Record<'Left' | 'Right' | 'Top' | 'Bottom', React.FC<Props>> = {
Right(props) {
return renderLayout(props, true, true);
},
Horizontal: styled.div<DistributionProps>(({gap, center, fillx, filly}) => ({
display: 'flex',
flexDirection: 'row',
gap,
alignItems: center ? 'center' : 'stretch',
width: fillx ? '100%' : undefined,
height: filly ? '100%' : undefined,
})),
Vertical: styled.div<DistributionProps>(({gap, center, fillx, filly}) => ({
display: 'flex',
flexDirection: 'column',
gap,
alignItems: center ? 'center' : 'stretch',
width: fillx ? '100%' : undefined,
height: filly ? '100%' : undefined,
})),
};
Layout.Top.displayName = 'Layout.Top';
Layout.Left.displayName = 'Layout.Left';
Layout.Bottom.displayName = 'Layout.Bottom';
Layout.Right.displayName = 'Layout.Right';
Object.keys(Layout).forEach((key) => {
(Layout as any)[key].displayName = `Layout.${key}`;
});
export default Layout;

View File

@@ -175,23 +175,31 @@ export default class Sidebar extends Component<SidebarProps, SidebarState> {
}
const horizontal = position === 'left' || position === 'right';
const gutterWidth = gutter ? theme.space.middle : 0;
if (horizontal) {
width = width == null ? 200 : width;
minWidth = minWidth == null ? 100 : minWidth;
minWidth = (minWidth == null ? 100 : minWidth) + gutterWidth;
maxWidth = maxWidth == null ? 600 : maxWidth;
} else {
height = height == null ? 200 : height;
minHeight = minHeight == null ? 100 : minHeight;
maxHeight = maxHeight == null ? 600 : maxHeight;
}
return (
<SidebarInteractiveContainer
className={this.props.className}
minWidth={minWidth}
maxWidth={maxWidth}
width={horizontal ? (onResize ? width : this.state.width) : undefined}
width={
horizontal
? !children
? gutterWidth
: onResize
? width
: this.state.width
: undefined
}
minHeight={minHeight}
maxHeight={maxHeight}
height={!horizontal ? (onResize ? height : this.state.height) : '100%'}
@@ -222,32 +230,36 @@ const GutterWrapper = ({
}) => {
return position === 'right' ? (
<FlexRow grow>
<VerticalGutter />
<VerticalGutter enabled={!!children} />
{children}
</FlexRow>
) : (
<FlexRow grow>
{children}
<VerticalGutter />
<VerticalGutter enabled={!!children} />
</FlexRow>
); // TODO: support top / bottom
};
const VerticalGutterContainer = styled('div')({
width: theme.space.middle,
height: '100%',
color: theme.textColorPlaceholder,
fontSize: '16px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: theme.backgroundWash,
':hover': {
background: theme.dividerColor,
},
});
const VerticalGutter = () => (
<VerticalGutterContainer>
const VerticalGutterContainer = styled('div')<{enabled: boolean}>(
({enabled}) => ({
width: theme.space.middle,
minWidth: theme.space.middle,
height: '100%',
cursor: enabled ? undefined : 'default', // hide cursor from interactive container
color: enabled ? theme.textColorPlaceholder : theme.backgroundWash,
fontSize: '16px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: theme.backgroundWash,
':hover': {
background: enabled ? theme.dividerColor : undefined,
},
}),
);
const VerticalGutter = ({enabled}: {enabled: boolean}) => (
<VerticalGutterContainer enabled={enabled}>
<MoreOutlined />
</VerticalGutterContainer>
);