/** * 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 config from '../fb-stubs/config'; import BaseDevice from '../devices/BaseDevice'; import Client from '../Client'; import {UninitializedClient} from '../UninitializedClient'; import {FlipperBasePlugin} from '../plugin'; import {PluginNotification} from '../reducers/notifications'; import {ActiveSheet, ACTIVE_SHEET_PLUGINS} from '../reducers/application'; import {State as Store} from '../reducers'; import { Sidebar, FlexBox, colors, brandColors, Text, Glyph, styled, FlexColumn, GK, FlipperPlugin, FlipperDevicePlugin, LoadingIndicator, } from 'flipper'; import React, {Component, PureComponent} from 'react'; import NotificationsHub from '../NotificationsHub'; import { selectPlugin, showMoreOrLessPlugins, StaticView, setStaticView, } from '../reducers/connections'; import {setActiveSheet} from '../reducers/application'; import UserAccount from './UserAccount'; import {connect} from 'react-redux'; import {BackgroundColorProperty} from 'csstype'; import { MAX_MINIMUM_PLUGINS, SHOW_REMAINING_PLUGIN_IF_LESS_THAN, } from '../Client'; import {StyledOtherComponent} from 'create-emotion-styled'; import SupportRequestFormManager from '../fb-stubs/SupportRequestFormManager'; const ListItem = styled('div')(({active}: {active?: boolean}) => ({ paddingLeft: 10, display: 'flex', alignItems: 'center', marginBottom: 2, flexShrink: 0, backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none', color: active ? colors.white : colors.macOSSidebarSectionItem, lineHeight: '25px', padding: '0 10px', '&[disabled]': { color: 'rgba(0, 0, 0, 0.5)', }, })); const SidebarHeader = styled(FlexBox)({ display: 'block', alignItems: 'center', padding: 3, color: colors.macOSSidebarSectionTitle, fontSize: 11, fontWeight: 500, marginLeft: 7, textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', flexShrink: 0, }); const PluginShape = styled(FlexBox)( ({backgroundColor}: {backgroundColor?: BackgroundColorProperty}) => ({ marginRight: 5, backgroundColor, borderRadius: 3, flexShrink: 0, width: 18, height: 18, justifyContent: 'center', alignItems: 'center', }), ); const PluginName = styled(Text)( (props: {isActive?: boolean; count?: number}) => ({ minWidth: 0, textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', flexGrow: 1, '::after': { fontSize: 12, display: props.count ? 'inline-block' : 'none', padding: '0 8px', lineHeight: '17px', height: 17, alignSelf: 'center', content: `"${props.count}"`, borderRadius: '999em', color: props.isActive ? colors.macOSTitleBarIconSelected : colors.white, backgroundColor: props.isActive ? colors.white : colors.macOSTitleBarIconSelected, fontWeight: 500, }, }), ); const Plugins = styled(FlexColumn)({ flexGrow: 1, overflow: 'auto', }); const PluginDebugger = styled(FlexBox)({ color: colors.blackAlpha50, alignItems: 'center', padding: 10, flexShrink: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }); const PluginShowMoreOrLess = styled(ListItem)({ color: colors.blue, fontSize: 10, lineHeight: '10px', paddingBottom: 5, }); function PluginIcon({ isActive, backgroundColor, name, color, }: { isActive: boolean; backgroundColor?: string; name: string; color: string; }) { return ( ); } class PluginSidebarListItem extends Component<{ onClick: () => void; isActive: boolean; plugin: typeof FlipperBasePlugin; app?: string | null | undefined; }> { render() { const {isActive, plugin} = this.props; const app = this.props.app || 'Facebook'; let iconColor: string | undefined = (brandColors as any)[app]; if (!iconColor) { const pluginColors = [ colors.seaFoam, colors.teal, colors.lime, colors.lemon, colors.orange, colors.tomato, colors.cherry, colors.pink, colors.grape, ]; iconColor = pluginColors[parseInt(app, 36) % pluginColors.length]; } return ( {plugin.title || plugin.id} ); } } const Spinner = centerInSidebar(LoadingIndicator); const ErrorIndicator = centerInSidebar(Glyph); function centerInSidebar( component: StyledOtherComponent | React.ComponentType, ) { return styled(component)({ marginTop: '10px', marginBottom: '10px', marginLeft: 'auto', marginRight: 'auto', }); } type OwnProps = {}; type StateFromProps = { numNotifications: number; windowIsFocused: boolean; selectedDevice: BaseDevice | null | undefined; staticView: StaticView; selectedPlugin: string | null | undefined; selectedApp: string | null | undefined; clients: Array; uninitializedClients: Array<{ client: UninitializedClient; deviceId?: string; errorMessage?: string; }>; devicePlugins: Map; clientPlugins: Map; }; type DispatchFromProps = { selectPlugin: (payload: { selectedPlugin: string | null; selectedApp: string | null; deepLinkPayload: string | null; }) => void; setActiveSheet: (activeSheet: ActiveSheet) => void; setStaticView: (payload: StaticView) => void; showMoreOrLessPlugins: (payload: string) => void; }; type Props = OwnProps & StateFromProps & DispatchFromProps; type State = {showSupportForm: boolean}; class MainSidebar extends PureComponent { state: State = {showSupportForm: GK.get('flipper_support_requests')}; static getDerivedStateFromProps(props: Props, state: State) { if ( !state.showSupportForm && props.staticView === SupportRequestFormManager ) { // Show SupportForm option even when GK is false and support form is shown. // That means the user has used deeplink to open support form. // Once the variable is true, it will be true for the whole session. return {showSupportForm: true}; } return state; } render() { const { selectedDevice, selectedPlugin, staticView, selectedApp, selectPlugin, setStaticView, windowIsFocused, numNotifications, } = this.props; let {clients, uninitializedClients} = this.props; clients = clients .filter( (client: Client) => selectedDevice && selectedDevice.supportsOS(client.query.os), ) .sort((a, b) => (a.query.app || '').localeCompare(b.query.app)); const byPluginNameOrId = ( a: typeof FlipperBasePlugin, b: typeof FlipperBasePlugin, ) => ((a.title || a.id) > (b.title || b.id) ? 1 : -1); return ( {!GK.get('flipper_disable_notifications') && ( selectPlugin({ selectedPlugin: 'notifications', selectedApp: null, deepLinkPayload: null, }) }> 0 ? NotificationsHub.icon || 'bell' : 'bell-null' } isActive={selectedPlugin === NotificationsHub.id} /> {NotificationsHub.title} )} {this.state.showSupportForm && ( setStaticView(SupportRequestFormManager)}> Litho Support Request )} {selectedDevice && ( {selectedDevice.title} )} {selectedDevice && Array.from(this.props.devicePlugins.values()) .filter(plugin => plugin.supportsDevice(selectedDevice)) .sort(byPluginNameOrId) .map((plugin: typeof FlipperDevicePlugin) => ( selectPlugin({ selectedPlugin: plugin.id, selectedApp: null, deepLinkPayload: null, }) } plugin={plugin} /> ))} {clients .filter( (client: Client) => (selectedDevice && client.query.device_id === selectedDevice.serial) || // Old android sdk versions don't know their device_id // Display their plugins under all selected devices until they die out client.query.device_id === 'unknown', ) .map((client: Client) => { const plugins = Array.from( this.props.clientPlugins.values(), ).filter( (p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1, ); const minShowPluginsCount = plugins.length < MAX_MINIMUM_PLUGINS + SHOW_REMAINING_PLUGIN_IF_LESS_THAN ? plugins.length : MAX_MINIMUM_PLUGINS; return ( {client.query.app} {plugins .sort((a: typeof FlipperPlugin, b: typeof FlipperPlugin) => client.byClientLRU(plugins.length, a, b), ) .slice( 0, client.showAllPlugins ? client.plugins.length : minShowPluginsCount, ) .map((plugin: typeof FlipperPlugin) => ( selectPlugin({ selectedPlugin: plugin.id, selectedApp: client.id, deepLinkPayload: null, }) } plugin={plugin} app={client.query.app} /> ))} {plugins.length > minShowPluginsCount && ( this.props.showMoreOrLessPlugins(client.id) }> {client.showAllPlugins ? 'Show less' : 'Show more'} )} ); })} {uninitializedClients.map(entry => ( {entry.client.appName} {entry.errorMessage ? ( ) : ( )} ))} this.props.setActiveSheet(ACTIVE_SHEET_PLUGINS)}>  Manage Plugins... {config.showLogin && } ); } } export default connect( ({ application: {windowIsFocused}, connections: { selectedDevice, selectedPlugin, selectedApp, clients, uninitializedClients, staticView, }, notifications: {activeNotifications, blacklistedPlugins}, plugins: {devicePlugins, clientPlugins}, }) => ({ numNotifications: (() => { const blacklist = new Set(blacklistedPlugins); return activeNotifications.filter( (n: PluginNotification) => !blacklist.has(n.pluginId), ).length; })(), windowIsFocused, selectedDevice, staticView, selectedPlugin, selectedApp, clients, uninitializedClients, devicePlugins, clientPlugins, }), { selectPlugin, setStaticView, setActiveSheet, showMoreOrLessPlugins, }, )(MainSidebar);