Summary: This PR fixes the missing hover effect for the `LeftRail` component buttons, when they have included the badge. To fix the issue, I had to wrap the whole `Button` with `Badge` component (instead wrapping only around icon). However, this solution required to added `offset` property to the `Badge` which moves the indicator to the position prior change (otherwise indicator was moved to the right corner of the button). The file has been formatted after the changes with ESLint. **Edit:** I have also spotted that this change fixes the icon placement inside the button, when badge is present. Earlier, as seen below, the log icon was moved towards the top of the button: <img width="111" alt="Screenshot 2022-01-31 at 00 57 49" src="https://user-images.githubusercontent.com/719641/151723422-0ffb83ee-5806-412e-9191-f9953f78532e.png"> ## Changelog [desktop] UI: fix hover effect of LeftRail icons with badge Pull Request resolved: https://github.com/facebook/flipper/pull/3372 Test Plan: The change has been testes by running the desktop Flipper app locally from source. ## Preview (before & after) #### Before <img width="1339" alt="Screenshot 2022-01-31 at 00 24 23" src="https://user-images.githubusercontent.com/719641/151722800-a2f3dd44-aa24-4858-b43e-0620b1f2ae65.png"> #### After > I have used mocked indicator values locally to ensure that the Badges are displayed correctly. <img width="1339" alt="Screenshot 2022-01-31 at 00 26 10" src="https://user-images.githubusercontent.com/719641/151722795-745b04ac-9ee4-49a8-8217-206d8d7456e6.png"> <img width="1339" alt="Screenshot 2022-01-31 at 00 45 08" src="https://user-images.githubusercontent.com/719641/151722940-aaaf0e9b-f2d1-4245-8b2b-cfc11052b39e.png"> Reviewed By: aigoncharov Differential Revision: D33975324 Pulled By: mweststrate fbshipit-source-id: fe4773b4825b9f22e01821e45259747d319233aa
504 lines
14 KiB
TypeScript
504 lines
14 KiB
TypeScript
/**
|
|
* 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, 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';
|
|
import {getRenderHostInstance} from '../RenderHost';
|
|
import openSupportRequestForm from '../fb-stubs/openSupportRequestForm';
|
|
|
|
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({
|
|
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);
|
|
|
|
const fullState = useStore((state) => state);
|
|
|
|
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}}>
|
|
<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,
|
|
});
|
|
if (
|
|
getRenderHostInstance().GK('flipper_support_entry_point')
|
|
) {
|
|
openSupportRequestForm(fullState);
|
|
} else {
|
|
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={
|
|
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},
|
|
})
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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()}
|
|
/>
|
|
</>
|
|
);
|
|
}
|