/** * 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; }) { const iconElement = icon && cloneElement(icon, {style: {fontSize: small ? 16 : 24}}); let res = ( ); if (count !== undefined) { res = count === true ? ( {res} ) : ( {res} ); } if (title) { res = ( {res} ); } 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 ( } title="App Inspect" selected={toplevelSelection === 'appinspect'} onClick={() => { setToplevelSelection('appinspect'); }} /> } title="Plugin Manager" onClick={() => { Dialog.showModal((onHide) => ); }} /> {!isProduction() && (
)} {config.showLogin && }
); }); 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 ( <> } small />} className={submenu}> {canOpenDialog() ? ( Import Flipper file ) : null} {canFileExport() ? ( Export Flipper file ) : null} {constants.ENABLE_SHAREABLE_LINK ? ( Export shareable link ) : null} openDeeplinkDialog(store)}> Trigger deeplink {config.isFBBuild ? ( <> { 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 ) : null} setShowSettings(true)}> Settings setWelcomeVisible(true)}> Help {showSettings && ( )} 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 ( } 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 ( } 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 ( } title="Notifications" selected={toplevelSelection === 'notification'} count={activeNotifications.length} onClick={() => setToplevelSelection('notification')} /> ); } function DebugLogsButton({ toplevelSelection, setToplevelSelection, }: ToplevelProps) { const errorCount = useValue(errorCounterAtom); return ( } title="Flipper Logs" selected={toplevelSelection === 'flipperlogs'} count={errorCount} onClick={() => { setToplevelSelection('flipperlogs'); }} /> ); } function LaunchEmulatorButton() { const store = useStore(); return ( } 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 ( <> } small title="Setup Doctor" count={hasNewProblem ? true : undefined} onClick={() => setVisible(true)} /> ); } 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 ? ( { onHandleVisibleChange(false); dispatch(logout()); }}> Log Out } trigger="click" placement="right" visible={showLogout} overlayStyle={{padding: 0}} onVisibleChange={onHandleVisibleChange}> ) : ( <> } title="Log In" onClick={() => showLoginDialog()} /> ); }