Render sidebar

Summary:
Restore sidebar functionality for Sandy plugins

Also needed to fix some circular dependency issues as fallout.

Reviewed By: cekkaewnumchai

Differential Revision: D24362215

fbshipit-source-id: 0a09ac35ba981322ae0793edc3aa79ffddf2ce73
This commit is contained in:
Michel Weststrate
2020-10-20 03:22:15 -07:00
committed by Facebook GitHub Bot
parent ba5f067320
commit a2fac737f6
8 changed files with 134 additions and 120 deletions

View File

@@ -45,7 +45,7 @@ import {activateMenuItems} from './MenuBar';
import {Message} from './reducers/pluginMessageQueue'; import {Message} from './reducers/pluginMessageQueue';
import {Idler} from './utils/Idler'; import {Idler} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue'; import {processMessageQueue} from './utils/messageQueue';
import {ToggleButton, SmallText} from './ui'; import {ToggleButton, SmallText, Layout} from './ui';
import {SandyPluginRenderer} from 'flipper-plugin'; import {SandyPluginRenderer} from 'flipper-plugin';
import {isDevicePluginDefinition} from './utils/pluginUtils'; import {isDevicePluginDefinition} from './utils/pluginUtils';
import ArchivedDevice from './devices/ArchivedDevice'; import ArchivedDevice from './devices/ArchivedDevice';
@@ -67,7 +67,6 @@ const Waiting = styled(FlexColumn)({
width: '100%', width: '100%',
height: '100%', height: '100%',
flexGrow: 1, flexGrow: 1,
background: colors.light02,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
textAlign: 'center', textAlign: 'center',
@@ -95,6 +94,7 @@ const ProgressBarBar = styled.div<{progress: number}>(({progress}) => ({
type OwnProps = { type OwnProps = {
logger: Logger; logger: Logger;
isSandy?: boolean;
}; };
type StateFromProps = { type StateFromProps = {
@@ -362,6 +362,7 @@ class PluginContainer extends PureComponent<Props, State> {
isArchivedDevice, isArchivedDevice,
selectedApp, selectedApp,
settingsState, settingsState,
isSandy,
} = this.props; } = this.props;
if (!activePlugin || !target || !pluginKey) { if (!activePlugin || !target || !pluginKey) {
console.warn(`No selected plugin. Rendering empty!`); console.warn(`No selected plugin. Rendering empty!`);
@@ -429,7 +430,17 @@ class PluginContainer extends PureComponent<Props, State> {
}; };
pluginElement = React.createElement(activePlugin, props); pluginElement = React.createElement(activePlugin, props);
} }
return ( return isSandy ? (
<Layout.Right>
<ErrorBoundary
heading={`Plugin "${
activePlugin.title || 'Unknown'
}" encountered an error during render`}>
{pluginElement}
</ErrorBoundary>
<SidebarContainer id="detailsSidebar" />
</Layout.Right>
) : (
<React.Fragment> <React.Fragment>
<Container key="plugin"> <Container key="plugin">
<ErrorBoundary <ErrorBoundary

View File

@@ -7,12 +7,14 @@
* @format * @format
*/ */
import React from 'react'; import React, {useEffect, useMemo, useContext} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {ReactReduxContext} from 'react-redux';
import Sidebar from '../ui/components/Sidebar'; import Sidebar from '../ui/components/Sidebar';
import {connect} from 'react-redux';
import {toggleRightSidebarAvailable} from '../reducers/application'; import {toggleRightSidebarAvailable} from '../reducers/application';
import {State as Store} from '../reducers'; import {useDispatch, useStore} from '../utils/useStore';
import {useIsSandy} from '../sandy-chrome/SandyContext';
import {ContentContainer} from '../sandy-chrome/ContentContainer';
type OwnProps = { type OwnProps = {
children: any; children: any;
@@ -20,57 +22,47 @@ type OwnProps = {
minWidth?: number; minWidth?: number;
}; };
type StateFromProps = { /* eslint-disable react-hooks/rules-of-hooks */
rightSidebarVisible: boolean; export default function DetailSidebar({children, width, minWidth}: OwnProps) {
rightSidebarAvailable: boolean; const reduxContext = useContext(ReactReduxContext);
}; const domNode = useMemo(() => document.getElementById('detailsSidebar'), []);
type DispatchFromProps = { if (!reduxContext || !domNode) {
toggleRightSidebarAvailable: (visible?: boolean) => any; // For unit tests, make sure to render elements inline
}; return <div id="detailsSidebar">{children}</div>;
type Props = OwnProps & StateFromProps & DispatchFromProps;
class DetailSidebar extends React.Component<Props> {
componentDidMount() {
this.updateSidebarAvailablility();
} }
componentDidUpdate() { const isSandy = useIsSandy();
this.updateSidebarAvailablility(); const dispatch = useDispatch();
} const {rightSidebarAvailable, rightSidebarVisible} = useStore((state) => {
const {rightSidebarAvailable, rightSidebarVisible} = state.application;
return {rightSidebarAvailable, rightSidebarVisible};
});
updateSidebarAvailablility() { useEffect(
const available = Boolean(this.props.children); function updateSidebarAvailablility() {
if (available !== this.props.rightSidebarAvailable) { const available = Boolean(children);
this.props.toggleRightSidebarAvailable(available); if (available !== rightSidebarAvailable) {
} dispatch(toggleRightSidebarAvailable(available));
} }
},
[children, rightSidebarAvailable, dispatch],
);
render() { return (
const domNode = document.getElementById('detailsSidebar'); (children &&
return ( rightSidebarVisible &&
this.props.children &&
this.props.rightSidebarVisible &&
domNode && domNode &&
ReactDOM.createPortal( ReactDOM.createPortal(
<Sidebar <Sidebar
minWidth={this.props.minWidth} minWidth={minWidth}
width={this.props.width || 300} width={width || 300}
position="right"> position="right"
{this.props.children} gutter={isSandy}>
{isSandy ? <ContentContainer>{children}</ContentContainer> : children}
</Sidebar>, </Sidebar>,
domNode, domNode,
) )) ||
); null
} );
} }
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({application: {rightSidebarVisible, rightSidebarAvailable}}) => ({
rightSidebarVisible,
rightSidebarAvailable,
}),
{
toggleRightSidebarAvailable,
},
)(DetailSidebar);

View File

@@ -19,7 +19,6 @@ import {StaticView, setStaticView} from '../../reducers/connections';
import {setActiveSheet} from '../../reducers/application'; import {setActiveSheet} from '../../reducers/application';
import UserAccount from '../UserAccount'; import UserAccount from '../UserAccount';
import SupportRequestFormV2 from '../../fb-stubs/SupportRequestFormV2'; import SupportRequestFormV2 from '../../fb-stubs/SupportRequestFormV2';
import WatchTools from '../../fb-stubs/WatchTools';
import { import {
isStaticViewActive, isStaticViewActive,
PluginIcon, PluginIcon,
@@ -32,6 +31,7 @@ import {ConsoleLogs, errorCounterAtom} from '../ConsoleLogs';
import {useValue} from 'flipper-plugin'; import {useValue} from 'flipper-plugin';
import {colors} from '../../ui'; import {colors} from '../../ui';
import GK from '../../fb-stubs/GK'; import GK from '../../fb-stubs/GK';
import WatchTools from '../../fb-stubs/WatchTools';
type OwnProps = {}; type OwnProps = {};

View File

@@ -0,0 +1,19 @@
/**
* 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 {Layout, styled} from '../ui';
import {theme} from './theme';
export const ContentContainer = styled(Layout.Container)({
overflow: 'hidden',
background: theme.backgroundDefault,
border: `1px solid ${theme.dividerColor}`,
borderRadius: theme.containerBorderRadius,
boxShadow: `0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.05)`,
});

View File

@@ -8,7 +8,6 @@
*/ */
import React, {cloneElement, useState, useCallback, useMemo} from 'react'; import React, {cloneElement, useState, useCallback, useMemo} from 'react';
import {styled, Layout} from '../ui';
import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd'; import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd';
import { import {
MobileFilled, MobileFilled,
@@ -23,7 +22,10 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import {SidebarLeft, SidebarRight} from './SandyIcons'; import {SidebarLeft, SidebarRight} from './SandyIcons';
import {useDispatch, useStore} from '../utils/useStore'; import {useDispatch, useStore} from '../utils/useStore';
import {toggleLeftSidebarVisible} from '../reducers/application'; import {
toggleLeftSidebarVisible,
toggleRightSidebarVisible,
} from '../reducers/application';
import {theme} from './theme'; import {theme} from './theme';
import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen'; import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen';
import SettingsSheet from '../chrome/SettingsSheet'; import SettingsSheet from '../chrome/SettingsSheet';
@@ -34,6 +36,8 @@ import {ToplevelProps} from './SandyApp';
import {useValue} from 'flipper-plugin'; import {useValue} from 'flipper-plugin';
import {logout} from '../reducers/user'; import {logout} from '../reducers/user';
import config from '../fb-stubs/config'; import config from '../fb-stubs/config';
import Layout from '../ui/components/Layout';
import styled from '@emotion/styled';
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({ const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
width: kind === 'small' ? 32 : 36, width: kind === 'small' ? 32 : 36,
@@ -52,11 +56,13 @@ function LeftRailButton({
count, count,
title, title,
onClick, onClick,
disabled,
}: { }: {
icon?: React.ReactElement; icon?: React.ReactElement;
small?: boolean; small?: boolean;
toggled?: boolean; toggled?: boolean;
selected?: boolean; // TODO: make sure only one element can be selected selected?: boolean;
disabled?: boolean;
count?: number | true; count?: number | true;
title: string; title: string;
onClick?: React.MouseEventHandler<HTMLElement>; onClick?: React.MouseEventHandler<HTMLElement>;
@@ -78,6 +84,7 @@ function LeftRailButton({
type={selected ? 'primary' : 'ghost'} type={selected ? 'primary' : 'ghost'}
icon={iconElement} icon={iconElement}
onClick={onClick} onClick={onClick}
disabled={disabled}
style={{ style={{
color: toggled ? theme.primaryColor : undefined, color: toggled ? theme.primaryColor : undefined,
background: toggled ? theme.backgroundWash : undefined, background: toggled ? theme.backgroundWash : undefined,
@@ -99,9 +106,9 @@ export function LeftRail({
setToplevelSelection, setToplevelSelection,
}: ToplevelProps) { }: ToplevelProps) {
return ( return (
<Layout.Container borderRight padv={12} padh={6} width={48}> <Layout.Container borderRight padv={12} width={48}>
<Layout.Bottom> <Layout.Bottom>
<Layout.Vertical center gap={10}> <Layout.Vertical center gap={10} padh={6}>
<LeftRailButton <LeftRailButton
icon={<MobileFilled />} icon={<MobileFilled />}
title="App Inspect" title="App Inspect"
@@ -118,7 +125,7 @@ export function LeftRail({
setToplevelSelection={setToplevelSelection} setToplevelSelection={setToplevelSelection}
/> />
</Layout.Vertical> </Layout.Vertical>
<Layout.Vertical center gap={10}> <Layout.Vertical center gap={10} padh={6}>
<SetupDoctorButton /> <SetupDoctorButton />
<WelcomeScreenButton /> <WelcomeScreenButton />
<ShowSettingsButton /> <ShowSettingsButton />
@@ -127,11 +134,7 @@ export function LeftRail({
small small
title="Feedback / Bug Reporter" title="Feedback / Bug Reporter"
/> />
<LeftRailButton <RightSidebarToggleButton />
icon={<SidebarRight />}
small
title="Right Sidebar Toggle"
/>
<LeftSidebarToggleButton /> <LeftSidebarToggleButton />
{config.showLogin && <LoginButton />} {config.showLogin && <LoginButton />}
</Layout.Vertical> </Layout.Vertical>
@@ -141,12 +144,11 @@ export function LeftRail({
} }
function LeftSidebarToggleButton() { function LeftSidebarToggleButton() {
const dispatch = useDispatch();
const mainMenuVisible = useStore( const mainMenuVisible = useStore(
(state) => state.application.leftSidebarVisible, (state) => state.application.leftSidebarVisible,
); );
const dispatch = useDispatch();
return ( return (
<LeftRailButton <LeftRailButton
icon={<SidebarLeft />} icon={<SidebarLeft />}
@@ -160,6 +162,29 @@ function LeftSidebarToggleButton() {
); );
} }
function RightSidebarToggleButton() {
const dispatch = useDispatch();
const rightSidebarAvailable = useStore(
(state) => state.application.rightSidebarAvailable,
);
const rightSidebarVisible = useStore(
(state) => state.application.rightSidebarVisible,
);
return (
<LeftRailButton
icon={<SidebarRight />}
small
title="Right Sidebar Toggle"
toggled={rightSidebarVisible}
disabled={!rightSidebarAvailable}
onClick={() => {
dispatch(toggleRightSidebarVisible());
}}
/>
);
}
function DebugLogsButton({ function DebugLogsButton({
toplevelSelection, toplevelSelection,
setToplevelSelection, setToplevelSelection,

View File

@@ -23,6 +23,7 @@ import {setStaticView} from '../reducers/connections';
import {toggleLeftSidebarVisible} from '../reducers/application'; import {toggleLeftSidebarVisible} from '../reducers/application';
import {AppInspect} from './appinspect/AppInspect'; import {AppInspect} from './appinspect/AppInspect';
import PluginContainer from '../PluginContainer'; import PluginContainer from '../PluginContainer';
import {ContentContainer} from './ContentContainer';
export type ToplevelNavItem = 'appinspect' | 'flipperlogs' | undefined; export type ToplevelNavItem = 'appinspect' | 'flipperlogs' | undefined;
export type ToplevelProps = { export type ToplevelProps = {
@@ -95,25 +96,15 @@ export function SandyApp({logger}: {logger: Logger}) {
</Sidebar> </Sidebar>
</Layout.Horizontal> </Layout.Horizontal>
<MainContainer> <MainContainer>
<ContentContainer> {staticView ? (
{staticView ? ( <ContentContainer>
React.createElement(staticView, { {React.createElement(staticView, {
logger: logger, logger: logger,
}) })}
) : (
<PluginContainer logger={logger} />
)}
</ContentContainer>
<Sidebar
width={300}
minWidth={220}
maxWidth={800}
gutter
position="right">
<ContentContainer style={{marginRight: theme.space.large}}>
<RightMenu />
</ContentContainer> </ContentContainer>
</Sidebar> ) : (
<PluginContainer logger={logger} isSandy />
)}
</MainContainer> </MainContainer>
</Layout.Left> </Layout.Left>
</Layout.Top> </Layout.Top>
@@ -121,19 +112,7 @@ export function SandyApp({logger}: {logger: Logger}) {
); );
} }
const MainContainer = styled(Layout.Right)({ const MainContainer = styled(Layout.Container)({
background: theme.backgroundWash, background: theme.backgroundWash,
padding: `${theme.space.large}px ${theme.space.large}px ${theme.space.large}px 0`,
}); });
export const ContentContainer = styled(Layout.Container)({
background: theme.backgroundDefault,
border: `1px solid ${theme.dividerColor}`,
borderRadius: theme.containerBorderRadius,
boxShadow: `0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 1px rgba(0, 0, 0, 0.05)`,
marginTop: theme.space.large,
marginBottom: theme.space.large,
});
function RightMenu() {
return <div>RightMenu</div>;
}

View File

@@ -45,6 +45,7 @@ const Container = styled.div<ContainerProps>(
height, height,
...rest ...rest
}) => ({ }) => ({
boxSizing: 'border-box',
minWidth: `0`, // ensures the Container can shrink smaller than it's largest minWidth: `0`, // ensures the Container can shrink smaller than it's largest
width, width,
height, height,
@@ -64,6 +65,7 @@ const Container = styled.div<ContainerProps>(
); );
const ScrollParent = styled.div({ const ScrollParent = styled.div({
boxSizing: 'border-box',
flex: 1, flex: 1,
position: 'relative', position: 'relative',
overflow: 'auto', overflow: 'auto',
@@ -141,11 +143,7 @@ const Layout = {
let [child1, child2] = props.children; let [child1, child2] = props.children;
if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>; if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>;
return ( return (
<SandySplitContainer <SandySplitContainer {...props} flexDirection="column" grow={2}>
{...props}
flexDirection="column"
flex1={0}
flex2={1}>
{child1} {child1}
{child2} {child2}
</SandySplitContainer> </SandySplitContainer>
@@ -157,11 +155,7 @@ const Layout = {
let [child1, child2] = props.children; let [child1, child2] = props.children;
if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>; if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>;
return ( return (
<SandySplitContainer <SandySplitContainer {...props} flexDirection="column" grow={1}>
{...props}
flexDirection="column"
flex1={1}
flex2={0}>
{child1} {child1}
{child2} {child2}
</SandySplitContainer> </SandySplitContainer>
@@ -173,7 +167,7 @@ const Layout = {
let [child1, child2] = props.children; let [child1, child2] = props.children;
if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>; if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>;
return ( return (
<SandySplitContainer {...props} flexDirection="row" flex1={0} flex2={1}> <SandySplitContainer {...props} flexDirection="row" grow={2}>
{child1} {child1}
{child2} {child2}
</SandySplitContainer> </SandySplitContainer>
@@ -185,7 +179,7 @@ const Layout = {
let [child1, child2] = props.children; let [child1, child2] = props.children;
if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>; if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>;
return ( return (
<SandySplitContainer {...props} flexDirection="row" flex1={1} flex2={0}> <SandySplitContainer {...props} flexDirection="row" grow={1}>
{child1} {child1}
{child2} {child2}
</SandySplitContainer> </SandySplitContainer>
@@ -202,23 +196,25 @@ Object.keys(Layout).forEach((key) => {
}); });
const SandySplitContainer = styled.div<{ const SandySplitContainer = styled.div<{
flex1: number; grow: 1 | 2;
flex2: number;
center?: boolean; center?: boolean;
flexDirection: CSSProperties['flexDirection']; flexDirection: CSSProperties['flexDirection'];
}>((props) => ({ }>((props) => ({
boxSizing: 'border-box',
display: 'flex', display: 'flex',
flex: 1, flex: 1,
flexDirection: props.flexDirection, flexDirection: props.flexDirection,
alignItems: props.center ? 'center' : 'stretch', alignItems: props.center ? 'center' : 'stretch',
overflow: 'hidden',
'> :first-child': { '> :first-child': {
flexGrow: props.flex1, flex: props.grow === 1 ? growStyle : fixedStyle,
flexShrink: props.flex1,
}, },
'> :last-child': { '> :last-child': {
flexGrow: props.flex2, flex: props.grow === 2 ? growStyle : fixedStyle,
flexShrink: props.flex2,
}, },
})); }));
const fixedStyle = `0 0 auto`;
const growStyle = `1 0 0`;
export default Layout; export default Layout;

View File

@@ -7,20 +7,12 @@
* @format * @format
*/ */
import * as React from 'react';
import * as Flipper from 'flipper';
// eslint-disable-next-line // eslint-disable-next-line
import {act} from '@testing-library/react'; import {act} from '@testing-library/react';
{ {
// These mocks are needed because seammammals still uses Flipper in its UI implementation, // These mocks are needed because seammammals still uses Flipper in its UI implementation,
// so we need to mock some things // so we need to mock some things
// @ts-ignore
jest.spyOn(Flipper.DetailSidebar, 'type').mockImplementation((props) => {
return <div className="DetailsSidebar">{props.children}</div>;
});
const origRequestIdleCallback = window.requestIdleCallback; const origRequestIdleCallback = window.requestIdleCallback;
const origCancelIdleCallback = window.cancelIdleCallback; const origCancelIdleCallback = window.cancelIdleCallback;
// @ts-ignore // @ts-ignore