/** * 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, Modal, } from 'antd'; import { MobileFilled, AppstoreOutlined, BellOutlined, FileExclamationOutlined, LoginOutlined, SettingOutlined, MedicineBoxOutlined, RocketOutlined, BugOutlined, } 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 {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 constants from '../fb-stubs/constants'; import { canFileExport, canOpenDialog, exportEverythingEverywhereAllAtOnce, showOpenDialog, startFileExport, startLinkExport, ExportEverythingEverywhereAllAtOnceStatus, } from '../utils/exportData'; import {openDeeplinkDialog} from '../deeplink'; import {css} from '@emotion/css'; import {getRenderHostInstance} from 'flipper-frontend-core'; import openSupportRequestForm from '../fb-stubs/openSupportRequestForm'; import {StyleGuide} from './StyleGuide'; import {useEffect} from 'react'; 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; } `; 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} { store.dispatch(setStaticView(StyleGuide)); }}> Flipper Style Guide openDeeplinkDialog(store)}> Trigger deeplink {config.isFBBuild ? ( <> { getLogger().track('usage', 'support-form-source', { source: 'sidebar', group: undefined, }); openSupportRequestForm(fullState); }}> 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 ExportEverythingEverywhereAllAtOnceButton() { const store = useStore(); const [status, setStatus] = useState< ExportEverythingEverywhereAllAtOnceStatus | undefined >(); const [statusMessage, setStatusMessage] = useState(); const exportEverythingEverywhereAllAtOnceTracked = useTrackedCallback( 'Debug data export', () => exportEverythingEverywhereAllAtOnce(store, setStatus), [store, setStatus], ); useEffect(() => { switch (status) { case 'logs': { setStatusMessage(

Exporting Flipper logs...

); return; } case 'files': { let sheepCount = 0; const setFileExportMessage = () => { setStatusMessage( <>

Exporting Flipper debug files from all devices...

It could take a long time!

Let's count sheep while we wait: {sheepCount++}.

We'll skip it automatically if it exceeds 3 minutes.

, ); }; setFileExportMessage(); const interval = setInterval(setFileExportMessage, 3000); return () => clearInterval(interval); } case 'state': { let dinosaursCount = 0; const setStateExportMessage = () => { setStatusMessage( <>

Exporting Flipper state...

It also could take a long time!

This time we could count dinosaurs: {dinosaursCount++}.

We'll skip it automatically if it exceeds 2 minutes.

, ); }; setStateExportMessage(); const interval = setInterval(setStateExportMessage, 2000); return () => clearInterval(interval); } case 'archive': { setStatusMessage(

Creating an archive...

); return; } case 'done': { setStatusMessage(

Done!

); return; } case 'cancelled': { setStatusMessage(

Cancelled! Why? 😱🤯👏

); return; } } }, [status]); return ( <> { setStatus(undefined); }} title="Exporting everything everywhere all at once" footer={null}> {statusMessage} } title="Export Flipper debug data" onClick={() => { exportEverythingEverywhereAllAtOnceTracked(); }} small /> ); } 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()} /> ); }