Move app/src (mostly) to flipper-ui-core/src
Summary: This diff moves all UI code from app/src to app/flipper-ui-core. That is now slightly too much (e.g. node deps are not removed yet), but from here it should be easier to move things out again, as I don't want this diff to be open for too long to avoid too much merge conflicts. * But at least flipper-ui-core is Electron free :) * Killed all cross module imports as well, as they where now even more in the way * Some unit test needed some changes, most not too big (but emotion hashes got renumbered in the snapshots, feel free to ignore that) * Found some files that were actually meaningless (tsconfig in plugins, WatchTools files, that start generating compile errors, removed those Follow up work: * make flipper-ui-core configurable, and wire up flipper-server-core in Electron instead of here * remove node deps (aigoncharov) * figure out correct place to load GKs, plugins, make intern requests etc., and move to the correct module * clean up deps Reviewed By: aigoncharov Differential Revision: D32427722 fbshipit-source-id: 14fe92e1ceb15b9dcf7bece367c8ab92df927a70
This commit is contained in:
committed by
Facebook GitHub Bot
parent
54b7ce9308
commit
7e50c0466a
479
desktop/flipper-ui-core/src/sandy-chrome/LeftRail.tsx
Normal file
479
desktop/flipper-ui-core/src/sandy-chrome/LeftRail.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* 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} from 'react';
|
||||
import {Button, Divider, Badge, Tooltip, Avatar, Popover, Menu} from 'antd';
|
||||
import {
|
||||
MobileFilled,
|
||||
AppstoreOutlined,
|
||||
BellOutlined,
|
||||
FileExclamationOutlined,
|
||||
LoginOutlined,
|
||||
SettingOutlined,
|
||||
MedicineBoxOutlined,
|
||||
RocketOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {SidebarLeft, SidebarRight} from './SandyIcons';
|
||||
import {useDispatch, useStore} from '../utils/useStore';
|
||||
import {
|
||||
toggleLeftSidebarVisible,
|
||||
toggleRightSidebarVisible,
|
||||
} from '../reducers/application';
|
||||
import {
|
||||
theme,
|
||||
Layout,
|
||||
withTrackingScope,
|
||||
Dialog,
|
||||
useTrackedCallback,
|
||||
NUX,
|
||||
} 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} from '../reducers/connections';
|
||||
import {getLogger} from 'flipper-common';
|
||||
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 PluginManager from '../chrome/plugin-manager/PluginManager';
|
||||
import {showLoginDialog} from '../chrome/fb-stubs/SignInSheet';
|
||||
import SubMenu from 'antd/lib/menu/SubMenu';
|
||||
import constants from '../fb-stubs/constants';
|
||||
import {
|
||||
canFileExport,
|
||||
canOpenDialog,
|
||||
showOpenDialog,
|
||||
startFileExport,
|
||||
startLinkExport,
|
||||
} from '../utils/exportData';
|
||||
import {openDeeplinkDialog} from '../deeplink';
|
||||
import {css} from '@emotion/css';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 (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({
|
||||
toplevelSelection,
|
||||
setToplevelSelection,
|
||||
}: ToplevelProps) {
|
||||
return (
|
||||
<Layout.Container borderRight padv={12} width={48}>
|
||||
<Layout.Bottom style={{overflow: 'visible'}}>
|
||||
<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={() => {
|
||||
Dialog.showModal((onHide) => <PluginManager onHide={onHide} />);
|
||||
}}
|
||||
/>
|
||||
<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 />
|
||||
<RightSidebarToggleButton />
|
||||
<LeftSidebarToggleButton />
|
||||
<ExtrasMenu />
|
||||
{config.showLogin && <LoginButton />}
|
||||
</Layout.Container>
|
||||
</Layout.Bottom>
|
||||
</Layout.Container>
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
const MenuDividerPadded = styled(Menu.Divider)({
|
||||
marginBottom: '8px !important',
|
||||
});
|
||||
|
||||
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}>
|
||||
<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.Item
|
||||
key="triggerDeeplink"
|
||||
onClick={() => openDeeplinkDialog(store)}>
|
||||
Trigger deeplink
|
||||
</Menu.Item>
|
||||
{config.isFBBuild ? (
|
||||
<>
|
||||
<MenuDividerPadded />
|
||||
<Menu.Item
|
||||
key="feedback"
|
||||
onClick={() => {
|
||||
getLogger().track('usage', 'support-form-source', {
|
||||
source: 'sidebar',
|
||||
group: undefined,
|
||||
});
|
||||
store.dispatch(setStaticView(SupportRequestFormV2));
|
||||
}}>
|
||||
Feedback
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : null}
|
||||
<MenuDividerPadded />
|
||||
<Menu.Item key="settings" onClick={() => setShowSettings(true)}>
|
||||
Settings
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="help" onClick={() => setWelcomeVisible(true)}>
|
||||
Help
|
||||
</Menu.Item>
|
||||
</SubMenu>
|
||||
</Menu>
|
||||
</NUX>
|
||||
{showSettings && (
|
||||
<SettingsSheet platform={process.platform} onHide={onSettingsClose} />
|
||||
)}
|
||||
<WelcomeScreen
|
||||
visible={welcomeVisible}
|
||||
onClose={() => setWelcomeVisible(false)}
|
||||
showAtStartup={showWelcomeAtStartup}
|
||||
onCheck={(value) =>
|
||||
store.dispatch({
|
||||
type: 'UPDATE_SETTINGS',
|
||||
payload: {...settings, showWelcomeAtStartup: value},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 LoginButton() {
|
||||
const dispatch = useDispatch();
|
||||
const user = useStore((state) => state.user);
|
||||
const login = (user?.id ?? null) !== null;
|
||||
const profileUrl = user?.profile_picture?.uri;
|
||||
const [showLogout, setShowLogout] = useState(false);
|
||||
const onHandleVisibleChange = useCallback(
|
||||
(visible) => setShowLogout(visible),
|
||||
[],
|
||||
);
|
||||
|
||||
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={() => showLoginDialog()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user