diff --git a/desktop/app/src/sandy-chrome/AppInspect.tsx b/desktop/app/src/sandy-chrome/AppInspect.tsx new file mode 100644 index 000000000..20a47d723 --- /dev/null +++ b/desktop/app/src/sandy-chrome/AppInspect.tsx @@ -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. + { + + } + +); + +export function AppInspect() { + return ( + + + <> + {appTooltip}}> + App Inspect + + + + } defaultValue="mysite" /> + + + + + ); +} diff --git a/desktop/app/src/sandy-chrome/LeftRail.tsx b/desktop/app/src/sandy-chrome/LeftRail.tsx index a834c3b53..2df3e3146 100644 --- a/desktop/app/src/sandy-chrome/LeftRail.tsx +++ b/desktop/app/src/sandy-chrome/LeftRail.tsx @@ -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 ( - } title="App Inspect" /> + } + title="App Inspect" + selected={toplevelSelection === 'appinspect'} + onClick={() => { + setToplevelSelection('appinspect'); + }} + /> } title="Plugin Manager" /> - + state.connections.staticView); - const active = isStaticViewActive(staticView, ConsoleLogs); +function DebugLogsButton({ + toplevelSelection, + setToplevelSelection, +}: ToplevelProps) { const errorCount = useValue(errorCounterAtom); - const dispatch = useDispatch(); - return ( } 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() { setVisible(false)} - showAtStartup={showWelcomeScreenAtStartup} + showAtStartup={showWelcomeAtStartup} onCheck={(value) => dispatch({ type: 'UPDATE_SETTINGS', diff --git a/desktop/app/src/sandy-chrome/LeftSidebar.tsx b/desktop/app/src/sandy-chrome/LeftSidebar.tsx new file mode 100644 index 000000000..51adc925d --- /dev/null +++ b/desktop/app/src/sandy-chrome/LeftSidebar.tsx @@ -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 ( + + + {children} + <>{actions} + + + ); +} + +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}) => ( + + + +); diff --git a/desktop/app/src/sandy-chrome/SandyApp.tsx b/desktop/app/src/sandy-chrome/SandyApp.tsx index 09e0676d6..a4b86bd4b 100644 --- a/desktop/app/src/sandy-chrome/SandyApp.tsx +++ b/desktop/app/src/sandy-chrome/SandyApp.tsx @@ -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' ? ( + + ) : null; return ( - - - - {mainMenuVisible && ( -
- LeftMenu -
- )} -
+ + + + + {leftMenuContent && ( + {leftMenuContent} + )} + + - + {staticView ? ( @@ -62,11 +110,18 @@ export function SandyApp({logger}: {logger: Logger}) { )} - - - - - + + + + + + + @@ -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)`, }); diff --git a/desktop/app/src/sandy-chrome/theme.tsx b/desktop/app/src/sandy-chrome/theme.tsx index 2673b82bf..8981897d4 100644 --- a/desktop/app/src/sandy-chrome/theme.tsx +++ b/desktop/app/src/sandy-chrome/theme.tsx @@ -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, diff --git a/desktop/app/src/ui/components/Layout.tsx b/desktop/app/src/ui/components/Layout.tsx index 9e76803aa..e434de754 100644 --- a/desktop/app/src/ui/components/Layout.tsx +++ b/desktop/app/src/ui/components/Layout.tsx @@ -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 ? ( - {fixedChild} - ) : horizontal ? ( - - {fixedChild} - - ) : ( - - {fixedChild} - - ); + const fixedElement = {fixedChild}; + const dynamicElement = ( {reverse ? children[0] : children[1]} ); return reverse ? ( - + {dynamicElement} {fixedElement} ) : ( - + {fixedElement} {dynamicElement} ); } +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> = { +const Layout: Record< + 'Left' | 'Right' | 'Top' | 'Bottom', + React.FC +> & + Record<'Horizontal' | 'Vertical', React.FC> = { Top(props) { return renderLayout(props, false, false); }, @@ -124,11 +128,26 @@ const Layout: Record<'Left' | 'Right' | 'Top' | 'Bottom', React.FC> = { Right(props) { return renderLayout(props, true, true); }, + Horizontal: styled.div(({gap, center, fillx, filly}) => ({ + display: 'flex', + flexDirection: 'row', + gap, + alignItems: center ? 'center' : 'stretch', + width: fillx ? '100%' : undefined, + height: filly ? '100%' : undefined, + })), + Vertical: styled.div(({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; diff --git a/desktop/app/src/ui/components/Sidebar.tsx b/desktop/app/src/ui/components/Sidebar.tsx index fbe20b624..feae37f9a 100644 --- a/desktop/app/src/ui/components/Sidebar.tsx +++ b/desktop/app/src/ui/components/Sidebar.tsx @@ -175,23 +175,31 @@ export default class Sidebar extends Component { } 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 ( { return position === 'right' ? ( - + {children} ) : ( {children} - + ); // 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 = () => ( - +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}) => ( + );