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 {Idler} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue';
import {ToggleButton, SmallText} from './ui';
import {ToggleButton, SmallText, Layout} from './ui';
import {SandyPluginRenderer} from 'flipper-plugin';
import {isDevicePluginDefinition} from './utils/pluginUtils';
import ArchivedDevice from './devices/ArchivedDevice';
@@ -67,7 +67,6 @@ const Waiting = styled(FlexColumn)({
width: '100%',
height: '100%',
flexGrow: 1,
background: colors.light02,
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
@@ -95,6 +94,7 @@ const ProgressBarBar = styled.div<{progress: number}>(({progress}) => ({
type OwnProps = {
logger: Logger;
isSandy?: boolean;
};
type StateFromProps = {
@@ -362,6 +362,7 @@ class PluginContainer extends PureComponent<Props, State> {
isArchivedDevice,
selectedApp,
settingsState,
isSandy,
} = this.props;
if (!activePlugin || !target || !pluginKey) {
console.warn(`No selected plugin. Rendering empty!`);
@@ -429,7 +430,17 @@ class PluginContainer extends PureComponent<Props, State> {
};
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>
<Container key="plugin">
<ErrorBoundary

View File

@@ -7,12 +7,14 @@
* @format
*/
import React from 'react';
import React, {useEffect, useMemo, useContext} from 'react';
import ReactDOM from 'react-dom';
import {ReactReduxContext} from 'react-redux';
import Sidebar from '../ui/components/Sidebar';
import {connect} from 'react-redux';
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 = {
children: any;
@@ -20,57 +22,47 @@ type OwnProps = {
minWidth?: number;
};
type StateFromProps = {
rightSidebarVisible: boolean;
rightSidebarAvailable: boolean;
};
/* eslint-disable react-hooks/rules-of-hooks */
export default function DetailSidebar({children, width, minWidth}: OwnProps) {
const reduxContext = useContext(ReactReduxContext);
const domNode = useMemo(() => document.getElementById('detailsSidebar'), []);
type DispatchFromProps = {
toggleRightSidebarAvailable: (visible?: boolean) => any;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class DetailSidebar extends React.Component<Props> {
componentDidMount() {
this.updateSidebarAvailablility();
if (!reduxContext || !domNode) {
// For unit tests, make sure to render elements inline
return <div id="detailsSidebar">{children}</div>;
}
componentDidUpdate() {
this.updateSidebarAvailablility();
}
const isSandy = useIsSandy();
const dispatch = useDispatch();
const {rightSidebarAvailable, rightSidebarVisible} = useStore((state) => {
const {rightSidebarAvailable, rightSidebarVisible} = state.application;
return {rightSidebarAvailable, rightSidebarVisible};
});
updateSidebarAvailablility() {
const available = Boolean(this.props.children);
if (available !== this.props.rightSidebarAvailable) {
this.props.toggleRightSidebarAvailable(available);
}
useEffect(
function updateSidebarAvailablility() {
const available = Boolean(children);
if (available !== rightSidebarAvailable) {
dispatch(toggleRightSidebarAvailable(available));
}
},
[children, rightSidebarAvailable, dispatch],
);
render() {
const domNode = document.getElementById('detailsSidebar');
return (
this.props.children &&
this.props.rightSidebarVisible &&
(children &&
rightSidebarVisible &&
domNode &&
ReactDOM.createPortal(
<Sidebar
minWidth={this.props.minWidth}
width={this.props.width || 300}
position="right">
{this.props.children}
minWidth={minWidth}
width={width || 300}
position="right"
gutter={isSandy}>
{isSandy ? <ContentContainer>{children}</ContentContainer> : children}
</Sidebar>,
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 UserAccount from '../UserAccount';
import SupportRequestFormV2 from '../../fb-stubs/SupportRequestFormV2';
import WatchTools from '../../fb-stubs/WatchTools';
import {
isStaticViewActive,
PluginIcon,
@@ -32,6 +31,7 @@ import {ConsoleLogs, errorCounterAtom} from '../ConsoleLogs';
import {useValue} from 'flipper-plugin';
import {colors} from '../../ui';
import GK from '../../fb-stubs/GK';
import WatchTools from '../../fb-stubs/WatchTools';
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 {styled, Layout} from '../ui';
import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd';
import {
MobileFilled,
@@ -23,7 +22,10 @@ import {
} from '@ant-design/icons';
import {SidebarLeft, SidebarRight} from './SandyIcons';
import {useDispatch, useStore} from '../utils/useStore';
import {toggleLeftSidebarVisible} from '../reducers/application';
import {
toggleLeftSidebarVisible,
toggleRightSidebarVisible,
} from '../reducers/application';
import {theme} from './theme';
import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen';
import SettingsSheet from '../chrome/SettingsSheet';
@@ -34,6 +36,8 @@ import {ToplevelProps} from './SandyApp';
import {useValue} from 'flipper-plugin';
import {logout} from '../reducers/user';
import config from '../fb-stubs/config';
import Layout from '../ui/components/Layout';
import styled from '@emotion/styled';
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
width: kind === 'small' ? 32 : 36,
@@ -52,11 +56,13 @@ function LeftRailButton({
count,
title,
onClick,
disabled,
}: {
icon?: React.ReactElement;
small?: boolean;
toggled?: boolean;
selected?: boolean; // TODO: make sure only one element can be selected
selected?: boolean;
disabled?: boolean;
count?: number | true;
title: string;
onClick?: React.MouseEventHandler<HTMLElement>;
@@ -78,6 +84,7 @@ function LeftRailButton({
type={selected ? 'primary' : 'ghost'}
icon={iconElement}
onClick={onClick}
disabled={disabled}
style={{
color: toggled ? theme.primaryColor : undefined,
background: toggled ? theme.backgroundWash : undefined,
@@ -99,9 +106,9 @@ export function LeftRail({
setToplevelSelection,
}: ToplevelProps) {
return (
<Layout.Container borderRight padv={12} padh={6} width={48}>
<Layout.Container borderRight padv={12} width={48}>
<Layout.Bottom>
<Layout.Vertical center gap={10}>
<Layout.Vertical center gap={10} padh={6}>
<LeftRailButton
icon={<MobileFilled />}
title="App Inspect"
@@ -118,7 +125,7 @@ export function LeftRail({
setToplevelSelection={setToplevelSelection}
/>
</Layout.Vertical>
<Layout.Vertical center gap={10}>
<Layout.Vertical center gap={10} padh={6}>
<SetupDoctorButton />
<WelcomeScreenButton />
<ShowSettingsButton />
@@ -127,11 +134,7 @@ export function LeftRail({
small
title="Feedback / Bug Reporter"
/>
<LeftRailButton
icon={<SidebarRight />}
small
title="Right Sidebar Toggle"
/>
<RightSidebarToggleButton />
<LeftSidebarToggleButton />
{config.showLogin && <LoginButton />}
</Layout.Vertical>
@@ -141,12 +144,11 @@ export function LeftRail({
}
function LeftSidebarToggleButton() {
const dispatch = useDispatch();
const mainMenuVisible = useStore(
(state) => state.application.leftSidebarVisible,
);
const dispatch = useDispatch();
return (
<LeftRailButton
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({
toplevelSelection,
setToplevelSelection,

View File

@@ -23,6 +23,7 @@ import {setStaticView} from '../reducers/connections';
import {toggleLeftSidebarVisible} from '../reducers/application';
import {AppInspect} from './appinspect/AppInspect';
import PluginContainer from '../PluginContainer';
import {ContentContainer} from './ContentContainer';
export type ToplevelNavItem = 'appinspect' | 'flipperlogs' | undefined;
export type ToplevelProps = {
@@ -95,25 +96,15 @@ export function SandyApp({logger}: {logger: Logger}) {
</Sidebar>
</Layout.Horizontal>
<MainContainer>
<ContentContainer>
{staticView ? (
React.createElement(staticView, {
<ContentContainer>
{React.createElement(staticView, {
logger: logger,
})
})}
</ContentContainer>
) : (
<PluginContainer logger={logger} />
<PluginContainer logger={logger} isSandy />
)}
</ContentContainer>
<Sidebar
width={300}
minWidth={220}
maxWidth={800}
gutter
position="right">
<ContentContainer style={{marginRight: theme.space.large}}>
<RightMenu />
</ContentContainer>
</Sidebar>
</MainContainer>
</Layout.Left>
</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,
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,
...rest
}) => ({
boxSizing: 'border-box',
minWidth: `0`, // ensures the Container can shrink smaller than it's largest
width,
height,
@@ -64,6 +65,7 @@ const Container = styled.div<ContainerProps>(
);
const ScrollParent = styled.div({
boxSizing: 'border-box',
flex: 1,
position: 'relative',
overflow: 'auto',
@@ -141,11 +143,7 @@ const Layout = {
let [child1, child2] = props.children;
if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>;
return (
<SandySplitContainer
{...props}
flexDirection="column"
flex1={0}
flex2={1}>
<SandySplitContainer {...props} flexDirection="column" grow={2}>
{child1}
{child2}
</SandySplitContainer>
@@ -157,11 +155,7 @@ const Layout = {
let [child1, child2] = props.children;
if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>;
return (
<SandySplitContainer
{...props}
flexDirection="column"
flex1={1}
flex2={0}>
<SandySplitContainer {...props} flexDirection="column" grow={1}>
{child1}
{child2}
</SandySplitContainer>
@@ -173,7 +167,7 @@ const Layout = {
let [child1, child2] = props.children;
if (props.scrollable) child2 = <ScrollContainer>{child2}</ScrollContainer>;
return (
<SandySplitContainer {...props} flexDirection="row" flex1={0} flex2={1}>
<SandySplitContainer {...props} flexDirection="row" grow={2}>
{child1}
{child2}
</SandySplitContainer>
@@ -185,7 +179,7 @@ const Layout = {
let [child1, child2] = props.children;
if (props.scrollable) child1 = <ScrollContainer>{child1}</ScrollContainer>;
return (
<SandySplitContainer {...props} flexDirection="row" flex1={1} flex2={0}>
<SandySplitContainer {...props} flexDirection="row" grow={1}>
{child1}
{child2}
</SandySplitContainer>
@@ -202,23 +196,25 @@ Object.keys(Layout).forEach((key) => {
});
const SandySplitContainer = styled.div<{
flex1: number;
flex2: number;
grow: 1 | 2;
center?: boolean;
flexDirection: CSSProperties['flexDirection'];
}>((props) => ({
boxSizing: 'border-box',
display: 'flex',
flex: 1,
flexDirection: props.flexDirection,
alignItems: props.center ? 'center' : 'stretch',
overflow: 'hidden',
'> :first-child': {
flexGrow: props.flex1,
flexShrink: props.flex1,
flex: props.grow === 1 ? growStyle : fixedStyle,
},
'> :last-child': {
flexGrow: props.flex2,
flexShrink: props.flex2,
flex: props.grow === 2 ? growStyle : fixedStyle,
},
}));
const fixedStyle = `0 0 auto`;
const growStyle = `1 0 0`;
export default Layout;

View File

@@ -7,20 +7,12 @@
* @format
*/
import * as React from 'react';
import * as Flipper from 'flipper';
// eslint-disable-next-line
import {act} from '@testing-library/react';
{
// These mocks are needed because seammammals still uses Flipper in its UI implementation,
// 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 origCancelIdleCallback = window.cancelIdleCallback;
// @ts-ignore