Files
flipper/desktop/app/src/sandy-chrome/LeftRail.tsx
Anton Nikolaev fff489091e Paste oauth token using deeplink
Reviewed By: passy

Differential Revision: D28790868

fbshipit-source-id: 6a22809444462af15915fa4f06e76e64ebb7f590
2021-06-02 11:30:37 -07:00

435 lines
11 KiB
TypeScript

/**
* 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, {
cloneElement,
useState,
useCallback,
useMemo,
useEffect,
} from 'react';
import {Button, Divider, Badge, Tooltip, Avatar, Popover} from 'antd';
import {
MobileFilled,
AppstoreOutlined,
BellOutlined,
FileExclamationOutlined,
LoginOutlined,
BugOutlined,
SettingOutlined,
QuestionCircleOutlined,
MedicineBoxOutlined,
RocketOutlined,
} from '@ant-design/icons';
import {SidebarLeft, SidebarRight} from './SandyIcons';
import {useDispatch, useStore} from '../utils/useStore';
import {
ACTIVE_SHEET_PLUGINS,
ACTIVE_SHEET_SIGN_IN,
setActiveSheet,
toggleLeftSidebarVisible,
toggleRightSidebarVisible,
} from '../reducers/application';
import {theme, Layout, withTrackingScope} from 'flipper-plugin';
import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen';
import SettingsSheet from '../chrome/SettingsSheet';
import WelcomeScreen from './WelcomeScreen';
import {errorCounterAtom} from '../chrome/ConsoleLogs';
import {ToplevelProps} from './SandyApp';
import {useValue} from 'flipper-plugin';
import {logout} from '../reducers/user';
import config from '../fb-stubs/config';
import styled from '@emotion/styled';
import {showEmulatorLauncher} from './appinspect/LaunchEmulator';
import SupportRequestFormV2 from '../fb-stubs/SupportRequestFormV2';
import {setStaticView, StaticView} from '../reducers/connections';
import {getInstance} from '../fb-stubs/Logger';
import {getUser} from '../fb-stubs/user';
import {SandyRatingButton} from '../chrome/RatingButton';
import {filterNotifications} from './notification/notificationUtils';
import {useMemoize} from 'flipper-plugin';
import isProduction from '../utils/isProduction';
import NetworkGraph from '../chrome/NetworkGraph';
import FpsGraph from '../chrome/FpsGraph';
import UpdateIndicator from '../chrome/UpdateIndicator';
import {UserNotSignedInError, UserUnauthorizedError} from '../utils/errors';
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>;
}) {
let iconElement =
icon && cloneElement(icon, {style: {fontSize: small ? 16 : 24}});
if (count !== undefined) {
iconElement =
count === true ? (
<Badge dot>{iconElement}</Badge>
) : (
<Badge count={count}>{iconElement}</Badge>
);
}
return (
<Tooltip title={title} placement="right">
<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,
}}
/>
</Tooltip>
);
}
const LeftRailDivider = styled(Divider)({
margin: `10px 0`,
width: 32,
minWidth: 32,
});
LeftRailDivider.displayName = 'LeftRailDividier';
export const LeftRail = withTrackingScope(function LeftRail({
toplevelSelection,
setToplevelSelection,
}: ToplevelProps) {
const dispatch = useDispatch();
return (
<Layout.Container borderRight padv={12} width={48}>
<Layout.Bottom>
<Layout.Container center gap={10} padh={6}>
<LeftRailButton
icon={<MobileFilled />}
title="App Inspect"
selected={toplevelSelection === 'appinspect'}
onClick={() => {
setToplevelSelection('appinspect');
}}
/>
<LeftRailButton
icon={<AppstoreOutlined />}
title="Plugin Manager"
onClick={() => {
dispatch(setActiveSheet(ACTIVE_SHEET_PLUGINS));
}}
/>
<NotificationButton
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<LeftRailDivider />
<DebugLogsButton
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
</Layout.Container>
<Layout.Container center gap={10} padh={6}>
{!isProduction() && (
<div>
<FpsGraph />
<NetworkGraph />
</div>
)}
<UpdateIndicator />
<SandyRatingButton />
<LaunchEmulatorButton />
<SetupDoctorButton />
<WelcomeScreenButton />
<ShowSettingsButton />
<SupportFormButton />
<RightSidebarToggleButton />
<LeftSidebarToggleButton />
{config.showLogin && <LoginButton />}
</Layout.Container>
</Layout.Bottom>
</Layout.Container>
);
});
function LeftSidebarToggleButton() {
const dispatch = useDispatch();
const mainMenuVisible = useStore(
(state) => state.application.leftSidebarVisible,
);
return (
<LeftRailButton
icon={<SidebarLeft />}
small
title="Left Sidebar Toggle"
toggled={mainMenuVisible}
onClick={() => {
dispatch(toggleLeftSidebarVisible());
}}
/>
);
}
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 NotificationButton({
toplevelSelection,
setToplevelSelection,
}: ToplevelProps) {
const notifications = useStore((state) => state.notifications);
const activeNotifications = useMemoize(filterNotifications, [
notifications.activeNotifications,
notifications.blocklistedPlugins,
notifications.blocklistedCategories,
]);
return (
<LeftRailButton
icon={<BellOutlined />}
title="Notifications"
selected={toplevelSelection === 'notification'}
count={activeNotifications.length}
onClick={() => setToplevelSelection('notification')}
/>
);
}
function DebugLogsButton({
toplevelSelection,
setToplevelSelection,
}: ToplevelProps) {
const errorCount = useValue(errorCounterAtom);
return (
<LeftRailButton
icon={<FileExclamationOutlined />}
title="Flipper Logs"
selected={toplevelSelection === 'flipperlogs'}
count={errorCount}
onClick={() => {
setToplevelSelection('flipperlogs');
}}
/>
);
}
function LaunchEmulatorButton() {
const store = useStore();
return (
<LeftRailButton
icon={<RocketOutlined />}
title="Start Emulator / Simulator"
onClick={() => {
showEmulatorLauncher(store);
}}
small
/>
);
}
function SetupDoctorButton() {
const [visible, setVisible] = useState(false);
const result = useStore(
(state) => state.healthchecks.healthcheckReport.result,
);
const hasNewProblem = useMemo(() => checkHasNewProblem(result), [result]);
const onClose = useCallback(() => setVisible(false), []);
return (
<>
<LeftRailButton
icon={<MedicineBoxOutlined />}
small
title="Setup Doctor"
count={hasNewProblem ? true : undefined}
onClick={() => setVisible(true)}
/>
<SetupDoctorScreen visible={visible} onClose={onClose} />
</>
);
}
function ShowSettingsButton() {
const [showSettings, setShowSettings] = useState(false);
const onClose = useCallback(() => setShowSettings(false), []);
return (
<>
<LeftRailButton
icon={<SettingOutlined />}
small
title="Settings"
onClick={() => setShowSettings(true)}
selected={showSettings}
/>
{showSettings && (
<SettingsSheet platform={process.platform} onHide={onClose} />
)}
</>
);
}
function SupportFormButton() {
const dispatch = useDispatch();
const staticView = useStore((state) => state.connections.staticView);
return config.isFBBuild ? (
<LeftRailButton
icon={<BugOutlined />}
small
title="Feedback / Bug Reporter"
selected={isStaticViewActive(staticView, SupportRequestFormV2)}
onClick={() => {
getInstance().track('usage', 'support-form-source', {
source: 'sidebar',
group: undefined,
});
dispatch(setStaticView(SupportRequestFormV2));
}}
/>
) : null;
}
function WelcomeScreenButton() {
const settings = useStore((state) => state.settingsState);
const {showWelcomeAtStartup} = settings;
const dispatch = useDispatch();
const [visible, setVisible] = useState(showWelcomeAtStartup);
return (
<>
<LeftRailButton
icon={<QuestionCircleOutlined />}
small
title="Help / Start Screen"
onClick={() => setVisible(true)}
/>
<WelcomeScreen
visible={visible}
onClose={() => setVisible(false)}
showAtStartup={showWelcomeAtStartup}
onCheck={(value) =>
dispatch({
type: 'UPDATE_SETTINGS',
payload: {...settings, showWelcomeAtStartup: value},
})
}
/>
</>
);
}
function LoginButton() {
const dispatch = useDispatch();
const user = useStore((state) => state.user);
const login = (user?.id ?? null) !== null;
const profileUrl = user?.profile_picture?.uri;
const showLogin = useCallback(() => {
dispatch(setActiveSheet(ACTIVE_SHEET_SIGN_IN));
}, [dispatch]);
const [showLogout, setShowLogout] = useState(false);
const onHandleVisibleChange = useCallback(
(visible) => setShowLogout(visible),
[],
);
useEffect(() => {
if (config.showLogin) {
getUser().catch((error) => {
if (
error instanceof UserUnauthorizedError ||
error instanceof UserNotSignedInError
) {
showLogin();
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return login ? (
<Popover
content={
<Button
block
style={{backgroundColor: theme.backgroundDefault}}
onClick={() => {
onHandleVisibleChange(false);
dispatch(logout());
}}>
Log Out
</Button>
}
trigger="click"
placement="right"
visible={showLogout}
overlayStyle={{padding: 0}}
onVisibleChange={onHandleVisibleChange}>
<Layout.Container padv={theme.inlinePaddingV}>
<Avatar size="small" src={profileUrl} />
</Layout.Container>
</Popover>
) : (
<>
<LeftRailButton
icon={<LoginOutlined />}
title="Log In"
onClick={showLogin}
/>
</>
);
}
function isStaticViewActive(
current: StaticView,
selected: StaticView,
): boolean {
return Boolean(current && selected && current === selected);
}