remove LeftRail

Reviewed By: elboman

Differential Revision: D47441161

fbshipit-source-id: f0c792beb64fc2474bf6e72b4e4a69d40b699c1e
This commit is contained in:
Anton Kastritskiy
2023-07-18 03:52:34 -07:00
committed by Facebook GitHub Bot
parent a76faafa4b
commit a5631c8d9f
4 changed files with 149 additions and 605 deletions

View File

@@ -1,359 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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, {
Component,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import {
FlexColumn,
FlexRow,
Button,
Checkbox,
styled,
Input,
Link,
} from '../ui';
import {LeftRailButton} from '../sandy-chrome/LeftRail';
import * as UserFeedback from '../fb-stubs/UserFeedback';
import {FeedbackPrompt} from '../fb-stubs/UserFeedback';
import {StarOutlined} from '@ant-design/icons';
import {Popover, Rate} from 'antd';
import {useStore} from '../utils/useStore';
import {currentUser} from '../fb-stubs/user';
import {theme, useValue} from 'flipper-plugin';
import {reportPlatformFailures} from 'flipper-common';
import {getRenderHostInstance} from 'flipper-frontend-core';
type NextAction = 'select-rating' | 'leave-comment' | 'finished';
class PredefinedComment extends Component<{
comment: string;
selected: boolean;
onClick: (_: unknown) => unknown;
}> {
static Container = styled.div<{selected: boolean}>((props) => {
return {
border: '1px solid #f2f3f5',
cursor: 'pointer',
borderRadius: 24,
backgroundColor: props.selected ? '#ecf3ff' : '#f2f3f5',
marginBottom: 4,
marginRight: 4,
padding: '4px 8px',
color: props.selected ? 'rgb(56, 88, 152)' : undefined,
borderColor: props.selected ? '#3578e5' : undefined,
':hover': {
borderColor: '#3578e5',
},
};
});
render() {
return (
<PredefinedComment.Container
onClick={this.props.onClick}
selected={this.props.selected}>
{this.props.comment}
</PredefinedComment.Container>
);
}
}
const Row = styled(FlexRow)({
marginTop: 5,
marginBottom: 5,
justifyContent: 'center',
textAlign: 'center',
color: '#9a9a9a',
flexWrap: 'wrap',
});
const DismissRow = styled(Row)({
marginBottom: 0,
marginTop: 10,
});
const DismissButton = styled.span({
'&:hover': {
textDecoration: 'underline',
cursor: 'pointer',
},
});
const Spacer = styled(FlexColumn)({
flexGrow: 1,
});
function dismissRow(dismiss: () => void) {
return (
<DismissRow key="dismiss">
<Spacer />
<DismissButton onClick={dismiss}>Dismiss</DismissButton>
<Spacer />
</DismissRow>
);
}
type FeedbackComponentState = {
rating: number | null;
hoveredRating: number;
allowUserInfoSharing: boolean;
nextAction: NextAction;
predefinedComments: {[key: string]: boolean};
comment: string;
};
class FeedbackComponent extends Component<
{
submitRating: (rating: number) => void;
submitComment: (
rating: number,
comment: string,
selectedPredefinedComments: Array<string>,
allowUserInfoSharing: boolean,
) => void;
close: () => void;
dismiss: () => void;
promptData: FeedbackPrompt;
},
FeedbackComponentState
> {
state: FeedbackComponentState = {
rating: null,
hoveredRating: 0,
allowUserInfoSharing: true,
nextAction: 'select-rating' as NextAction,
predefinedComments: this.props.promptData.predefinedComments.reduce(
(acc, cv) => ({...acc, [cv]: false}),
{},
),
comment: '',
};
onSubmitRating(newRating: number) {
const nextAction = newRating <= 2 ? 'leave-comment' : 'finished';
this.setState({rating: newRating, nextAction: nextAction});
this.props.submitRating(newRating);
if (nextAction === 'finished') {
setTimeout(this.props.close, 5000);
}
}
onCommentSubmitted(comment: string) {
this.setState({nextAction: 'finished'});
const selectedPredefinedComments: Array<string> = Object.entries(
this.state.predefinedComments,
)
.map((x) => ({comment: x[0], enabled: x[1]}))
.filter((x) => x.enabled)
.map((x) => x.comment);
const currentRating = this.state.rating;
if (currentRating) {
this.props.submitComment(
currentRating,
comment,
selectedPredefinedComments,
this.state.allowUserInfoSharing,
);
} else {
console.error('Illegal state: Submitting comment with no rating set.');
}
setTimeout(this.props.close, 1000);
}
onAllowUserSharingChanged(allowed: boolean) {
this.setState({allowUserInfoSharing: allowed});
}
render() {
let body: Array<ReactElement>;
switch (this.state.nextAction) {
case 'select-rating':
body = [
<Row key="bodyText">{this.props.promptData.bodyText}</Row>,
<Row key="stars" style={{margin: 'auto'}}>
<Rate onChange={(newRating) => this.onSubmitRating(newRating)} />
</Row>,
dismissRow(this.props.dismiss),
];
break;
case 'leave-comment':
const predefinedComments = Object.entries(
this.state.predefinedComments,
).map((c: [string, unknown], idx: number) => (
<PredefinedComment
key={idx}
comment={c[0]}
selected={Boolean(c[1])}
onClick={() =>
this.setState({
predefinedComments: {
...this.state.predefinedComments,
[c[0]]: !c[1],
},
})
}
/>
));
body = [
<Row key="predefinedComments">{predefinedComments}</Row>,
<Row key="inputRow">
<Input
style={{height: 30, width: '100%'}}
placeholder={this.props.promptData.commentPlaceholder}
value={this.state.comment}
onChange={(e) => this.setState({comment: e.target.value})}
onKeyDown={(e) =>
e.key == 'Enter' && this.onCommentSubmitted(this.state.comment)
}
autoFocus
/>
</Row>,
<Row key="contactCheckbox">
<Checkbox
checked={this.state.allowUserInfoSharing}
onChange={this.onAllowUserSharingChanged.bind(this)}
/>
{'Tool owner can contact me '}
</Row>,
<Row key="submit">
<Button onClick={() => this.onCommentSubmitted(this.state.comment)}>
Submit
</Button>
</Row>,
dismissRow(this.props.dismiss),
];
break;
case 'finished':
body = [
<Row key="thanks">
Thanks for the feedback! You can now help
<Link href="https://www.internalfb.com/intern/papercuts/?application=flipper">
prioritize bugs and features for Flipper in Papercuts
</Link>
</Row>,
dismissRow(this.props.dismiss),
];
break;
default: {
console.error('Illegal state: nextAction: ' + this.state.nextAction);
return null;
}
}
return (
<FlexColumn
style={{
width: 400,
paddingLeft: 20,
paddingRight: 20,
paddingTop: 10,
paddingBottom: 10,
}}>
<Row key="heading" style={{color: theme.primaryColor, fontSize: 20}}>
{this.state.nextAction === 'finished'
? this.props.promptData.postSubmitHeading
: this.props.promptData.preSubmitHeading}
</Row>
{body}
</FlexColumn>
);
}
}
export function SandyRatingButton() {
const [promptData, setPromptData] =
useState<UserFeedback.FeedbackPrompt | null>(null);
const [isShown, setIsShown] = useState(false);
const [hasTriggered, setHasTriggered] = useState(false);
const sessionId = useStore((store) => store.application.sessionId);
const loggedIn = useValue(currentUser());
const triggerPopover = useCallback(() => {
if (!hasTriggered) {
setIsShown(true);
setHasTriggered(true);
}
}, [hasTriggered]);
useEffect(() => {
if (
getRenderHostInstance().GK('flipper_enable_star_ratiings') &&
!hasTriggered &&
loggedIn
) {
reportPlatformFailures(
UserFeedback.getPrompt().then((prompt) => {
setPromptData(prompt);
setTimeout(triggerPopover, 30000);
}),
'RatingButton:getPrompt',
).catch((e) => {
console.warn('Failed to load ratings prompt:', e);
});
}
}, [triggerPopover, hasTriggered, loggedIn]);
const onClick = () => {
const willBeShown = !isShown;
setIsShown(willBeShown);
setHasTriggered(true);
if (!willBeShown) {
UserFeedback.dismiss(sessionId);
}
};
const submitRating = (rating: number) => {
UserFeedback.submitRating(rating, sessionId);
};
const submitComment = (
rating: number,
comment: string,
selectedPredefinedComments: Array<string>,
allowUserInfoSharing: boolean,
) => {
UserFeedback.submitComment(
rating,
comment,
selectedPredefinedComments,
allowUserInfoSharing,
sessionId,
);
};
if (!promptData) {
return null;
}
if (!promptData.shouldPopup || (hasTriggered && !isShown)) {
return null;
}
return (
<Popover
visible={isShown}
content={
<FeedbackComponent
submitRating={submitRating}
submitComment={submitComment}
close={() => {
setIsShown(false);
}}
dismiss={onClick}
promptData={promptData}
/>
}
placement="right"
trigger="click">
<LeftRailButton
icon={<StarOutlined />}
title="Rate Flipper"
onClick={onClick}
small
/>
</Popover>
);
}

View File

@@ -1,238 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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, {cloneElement, useState, useCallback} from 'react';
import {Button, Divider, Badge, Tooltip, Menu} from 'antd';
import {SettingOutlined} from '@ant-design/icons';
import {useStore} from '../utils/useStore';
import {
theme,
withTrackingScope,
useTrackedCallback,
NUX,
} from 'flipper-plugin';
import SettingsSheet from '../chrome/SettingsSheet';
import WelcomeScreen from './WelcomeScreen';
import styled from '@emotion/styled';
import {setStaticView} from '../reducers/connections';
import constants from '../fb-stubs/constants';
import {
canFileExport,
canOpenDialog,
showOpenDialog,
startFileExport,
startLinkExport,
} from '../utils/exportData';
import {openDeeplinkDialog} from '../deeplink';
import {css} from '@emotion/css';
import {getRenderHostInstance} from 'flipper-frontend-core';
import {StyleGuide} from './StyleGuide';
const LeftRailButtonElem = styled(Button)<{kind?: 'small'}>(({kind}) => ({
width: kind === 'small' ? 32 : 36,
height: kind === 'small' ? 32 : 36,
padding: '5px 0',
border: 'none',
boxShadow: 'none',
}));
LeftRailButtonElem.displayName = 'LeftRailButtonElem';
export function LeftRailButton({
icon,
small,
selected,
toggled,
count,
title,
onClick,
disabled,
}: {
icon?: React.ReactElement;
small?: boolean;
toggled?: boolean;
selected?: boolean;
disabled?: boolean;
count?: number | true;
title?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
}) {
const iconElement =
icon && cloneElement(icon, {style: {fontSize: small ? 16 : 24}});
let res = (
<LeftRailButtonElem
title={title}
kind={small ? 'small' : undefined}
type={selected ? 'primary' : 'ghost'}
icon={iconElement}
onClick={onClick}
disabled={disabled}
style={{
color: toggled ? theme.primaryColor : undefined,
background: toggled ? theme.backgroundWash : undefined,
}}
/>
);
if (count !== undefined) {
res =
count === true ? (
<Badge dot offset={[-8, 8]} {...{onClick}}>
{res}
</Badge>
) : (
<Badge count={count} offset={[-6, 5]} {...{onClick}}>
{res}
</Badge>
);
}
if (title) {
res = (
<Tooltip title={title} placement="right">
{res}
</Tooltip>
);
}
return res;
}
const LeftRailDivider = styled(Divider)({
margin: `10px 0`,
width: 32,
minWidth: 32,
});
LeftRailDivider.displayName = 'LeftRailDividier';
export const LeftRail = withTrackingScope(function LeftRail() {
return <ExtrasMenu />;
});
const menu = css`
border: none;
`;
const submenu = css`
.ant-menu-submenu-title {
width: 32px;
height: 32px !important;
line-height: 32px !important;
padding: 0;
margin: 0;
}
.ant-menu-submenu-arrow {
display: none;
}
`;
function ExtrasMenu() {
const store = useStore();
const startFileExportTracked = useTrackedCallback(
'File export',
() => startFileExport(store.dispatch),
[store.dispatch],
);
const startLinkExportTracked = useTrackedCallback(
'Link export',
() => startLinkExport(store.dispatch),
[store.dispatch],
);
const startImportTracked = useTrackedCallback(
'File import',
() => showOpenDialog(store),
[store],
);
const [showSettings, setShowSettings] = useState(false);
const onSettingsClose = useCallback(() => setShowSettings(false), []);
const settings = useStore((state) => state.settingsState);
const {showWelcomeAtStartup} = settings;
const [welcomeVisible, setWelcomeVisible] = useState(showWelcomeAtStartup);
return (
<>
<NUX
title="Find import, export, deeplink, feedback, settings, and help (welcome) here"
placement="right">
<Menu
mode="vertical"
className={menu}
selectable={false}
style={{backgroundColor: theme.backgroundDefault}}>
<Menu.SubMenu
popupOffset={[10, 0]}
key="extras"
title={<LeftRailButton icon={<SettingOutlined />} small />}
className={submenu}>
{canOpenDialog() ? (
<Menu.Item key="importFlipperFile" onClick={startImportTracked}>
Import Flipper file
</Menu.Item>
) : null}
{canFileExport() ? (
<Menu.Item key="exportFile" onClick={startFileExportTracked}>
Export Flipper file
</Menu.Item>
) : null}
{constants.ENABLE_SHAREABLE_LINK ? (
<Menu.Item
key="exportShareableLink"
onClick={startLinkExportTracked}>
Export shareable link
</Menu.Item>
) : null}
<Menu.Divider />
<Menu.SubMenu title="Plugin developers">
<Menu.Item
key="styleguide"
onClick={() => {
store.dispatch(setStaticView(StyleGuide));
}}>
Flipper Style Guide
</Menu.Item>
<Menu.Item
key="triggerDeeplink"
onClick={() => openDeeplinkDialog(store)}>
Trigger deeplink
</Menu.Item>
</Menu.SubMenu>
<Menu.Divider />
<Menu.Item key="settings" onClick={() => setShowSettings(true)}>
Settings
</Menu.Item>
<Menu.Item key="help" onClick={() => setWelcomeVisible(true)}>
Help
</Menu.Item>
</Menu.SubMenu>
</Menu>
</NUX>
{showSettings && (
<SettingsSheet
platform={
getRenderHostInstance().serverConfig.environmentInfo.os.platform
}
onHide={onSettingsClose}
/>
)}
<WelcomeScreen
visible={welcomeVisible}
onClose={() => setWelcomeVisible(false)}
showAtStartup={showWelcomeAtStartup}
onCheck={(value) =>
store.dispatch({
type: 'UPDATE_SETTINGS',
payload: {...settings, showWelcomeAtStartup: value},
})
}
/>
</>
);
}

View File

@@ -30,21 +30,20 @@ import {useDispatch, useStore} from '../utils/useStore';
import config from '../fb-stubs/config';
import {isConnected, currentUser, logoutUser} from '../fb-stubs/user';
import {showLoginDialog} from '../chrome/fb-stubs/SignInSheet';
import {Avatar, Badge, Button, Modal, Popover, Tooltip} from 'antd';
import {Avatar, Badge, Button, Menu, Modal, Popover, Tooltip} from 'antd';
import {
ApiOutlined,
AppstoreAddOutlined,
BellOutlined,
BugOutlined,
CameraOutlined,
EllipsisOutlined,
FileExclamationOutlined,
LayoutOutlined,
LoginOutlined,
MedicineBoxOutlined,
MobileOutlined,
QuestionCircleOutlined,
RocketOutlined,
SettingOutlined,
VideoCameraOutlined,
WarningOutlined,
} from '@ant-design/icons';
@@ -62,10 +61,22 @@ import NetworkGraph from '../chrome/NetworkGraph';
import {errorCounterAtom} from '../chrome/ConsoleLogs';
import {filterNotifications} from './notification/notificationUtils';
import {
canFileExport,
canOpenDialog,
exportEverythingEverywhereAllAtOnce,
ExportEverythingEverywhereAllAtOnceStatus,
showOpenDialog,
startFileExport,
startLinkExport,
} from '../utils/exportData';
import UpdateIndicator from '../chrome/UpdateIndicator';
import {css} from '@emotion/css';
import constants from '../fb-stubs/constants';
import {setStaticView} from '../reducers/connections';
import {StyleGuide} from './StyleGuide';
import {openDeeplinkDialog} from '../deeplink';
import SettingsSheet from '../chrome/SettingsSheet';
import WelcomeScreen from './WelcomeScreen';
export const Navbar = withTrackingScope(function Navbar({
toplevelSelection,
@@ -126,12 +137,12 @@ export const Navbar = withTrackingScope(function Navbar({
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<ExtrasMenu />
<SetupDoctorButton />
<ExportEverythingEverywhereAllAtOnceButton />
<NavbarButton label="Help" icon={QuestionCircleOutlined} />
<NavbarButton label="More" icon={EllipsisOutlined} />
<RightSidebarToggleButton />
{config.showLogin && <LoginConnectivityButton />}
<UpdateIndicator />
</Layout.Horizontal>
</Layout.Horizontal>
);
@@ -607,3 +618,131 @@ export function LeftRailButton({
return res;
}
const menu = css`
border: none;
height: 56px;
.ant-menu-submenu-title {
hieght: 56px;
}
`;
const submenu = css`
height: 56px;
.ant-menu-submenu-title {
width: 61px !important;
height: 56px !important;
padding: 0;
margin: 0;
}
.ant-menu-submenu-arrow {
display: none;
}
`;
function ExtrasMenu() {
const store = useStore();
const startFileExportTracked = useTrackedCallback(
'File export',
() => startFileExport(store.dispatch),
[store.dispatch],
);
const startLinkExportTracked = useTrackedCallback(
'Link export',
() => startLinkExport(store.dispatch),
[store.dispatch],
);
const startImportTracked = useTrackedCallback(
'File import',
() => showOpenDialog(store),
[store],
);
const [showSettings, setShowSettings] = useState(false);
const onSettingsClose = useCallback(() => setShowSettings(false), []);
const settings = useStore((state) => state.settingsState);
const {showWelcomeAtStartup} = settings;
const [welcomeVisible, setWelcomeVisible] = useState(showWelcomeAtStartup);
return (
<>
<NUX
title="Find import, export, deeplink, feedback, settings, and help (welcome) here"
placement="right">
<Menu
mode="vertical"
className={menu}
selectable={false}
style={{backgroundColor: theme.backgroundDefault}}>
<Menu.SubMenu
popupOffset={[-50, 50]}
key="extras"
title={<NavbarButton icon={SettingOutlined} label="More" />}
className={submenu}>
{canOpenDialog() ? (
<Menu.Item key="importFlipperFile" onClick={startImportTracked}>
Import Flipper file
</Menu.Item>
) : null}
{canFileExport() ? (
<Menu.Item key="exportFile" onClick={startFileExportTracked}>
Export Flipper file
</Menu.Item>
) : null}
{constants.ENABLE_SHAREABLE_LINK ? (
<Menu.Item
key="exportShareableLink"
onClick={startLinkExportTracked}>
Export shareable link
</Menu.Item>
) : null}
<Menu.Divider />
<Menu.SubMenu title="Plugin developers">
<Menu.Item
key="styleguide"
onClick={() => {
store.dispatch(setStaticView(StyleGuide));
}}>
Flipper Style Guide
</Menu.Item>
<Menu.Item
key="triggerDeeplink"
onClick={() => openDeeplinkDialog(store)}>
Trigger deeplink
</Menu.Item>
</Menu.SubMenu>
<Menu.Divider />
<Menu.Item key="settings" onClick={() => setShowSettings(true)}>
Settings
</Menu.Item>
<Menu.Item key="help" onClick={() => setWelcomeVisible(true)}>
Help
</Menu.Item>
</Menu.SubMenu>
</Menu>
</NUX>
{showSettings && (
<SettingsSheet
platform={
getRenderHostInstance().serverConfig.environmentInfo.os.platform
}
onHide={onSettingsClose}
/>
)}
<WelcomeScreen
visible={welcomeVisible}
onClose={() => setWelcomeVisible(false)}
showAtStartup={showWelcomeAtStartup}
onCheck={(value) =>
store.dispatch({
type: 'UPDATE_SETTINGS',
payload: {...settings, showWelcomeAtStartup: value},
})
}
/>
</>
);
}

View File

@@ -21,7 +21,6 @@ import {theme} from 'flipper-plugin';
import {Logger} from 'flipper-common';
import {Navbar} from './Navbar';
import {LeftRail} from './LeftRail';
import {useStore, useDispatch} from '../utils/useStore';
import {FlipperDevTools} from '../chrome/FlipperDevTools';
import {setStaticView} from '../reducers/connections';
@@ -173,9 +172,12 @@ export function SandyApp() {
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<Layout.Left>
<Layout.Left
style={{
paddingLeft: theme.space.large,
paddingRight: theme.space.large,
}}>
<Layout.Horizontal>
<LeftRail />
<_Sidebar width={250} minWidth={220} maxWidth={800} gutter>
{leftMenuContent && (
<TrackingScope scope={toplevelSelection!}>