Remove non-Sandy UI

Summary:
This removes the Non-Sandy UI from the Flipper codebase. It is a pretty rough scan for unused components, over time when converting more advanced components to Ant design probably even more code can be removed.

Partially used `npx ts-purge` to reveal never imported source files.

Changelog: It is no longer possible to opt out of the new Sandy UI

Reviewed By: jknoxville

Differential Revision: D25825282

fbshipit-source-id: 9041dbc7e03bce0760c9a0a34f1877851b5f06cf
This commit is contained in:
Michel Weststrate
2021-01-25 04:45:28 -08:00
committed by Facebook GitHub Bot
parent ba74b074c2
commit 12e59afdc6
44 changed files with 343 additions and 3386 deletions

View File

@@ -14,7 +14,6 @@ import {
FlipperDevicePlugin,
} from './plugin';
import BaseDevice, {OS} from './devices/BaseDevice';
import {LegacyApp} from './chrome/LegacyApp';
import {Logger} from './fb-interfaces/Logger';
import {Store} from './reducers/index';
import {setPluginState} from './reducers/pluginStates';
@@ -124,7 +123,6 @@ export interface FlipperClientConnection<D, M> {
}
export default class Client extends EventEmitter {
app: LegacyApp | undefined;
connected: boolean;
id: string;
query: ClientQuery;

View File

@@ -1,45 +0,0 @@
/**
* 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 from 'react';
import {useEffect} from 'react';
import LegacyApp from './LegacyApp';
import fbConfig from '../fb-stubs/config';
import {isFBEmployee} from '../utils/fbEmployee';
import {Logger} from '../fb-interfaces/Logger';
import isSandyEnabled from '../utils/isSandyEnabled';
import {SandyApp} from '../sandy-chrome/SandyApp';
import {notification} from 'antd';
import isProduction from '../utils/isProduction';
type Props = {logger: Logger};
export default function App(props: Props) {
useEffect(() => {
if (fbConfig.warnFBEmployees && isProduction()) {
isFBEmployee().then((isEmployee) => {
if (isEmployee) {
notification.warning({
placement: 'bottomLeft',
message: 'Please use Flipper@FB',
description: (
<>
You are using the open-source version of Flipper. Install the
internal build from Managed Software Center to get access to
more plugins.
</>
),
duration: null,
});
}
});
}
}, []);
return isSandyEnabled() ? <SandyApp /> : <LegacyApp logger={props.logger} />;
}

View File

@@ -16,7 +16,6 @@ import type {Styles} from 'console-feed/lib/definitions/Styles';
import {createState, useValue} from 'flipper-plugin';
import {useLocalStorage} from '../utils/useLocalStorage';
import {theme} from 'flipper-plugin';
import {useIsSandy} from '../sandy-chrome/SandyContext';
import {useIsDarkMode} from '../utils/useIsDarkMode';
const MAX_LOG_ITEMS = 1000;
@@ -65,7 +64,6 @@ const allLogLevels: Methods[] = [
const defaultLogLevels: Methods[] = ['warn', 'error', 'table', 'assert'];
export function ConsoleLogs() {
const isSandy = useIsSandy();
const isDarkMode = useIsDarkMode();
const logs = useValue(logsAtom);
const [logLevels, setLogLevels] = useLocalStorage<Methods[]>(
@@ -90,7 +88,7 @@ export function ConsoleLogs() {
);
}, [logLevels, setLogLevels]);
const styles = useMemo(() => buildTheme(isSandy), [isSandy]);
const styles = useMemo(buildTheme, []);
return (
<Layout.Top>
@@ -106,7 +104,7 @@ export function ConsoleLogs() {
<Console
logs={logs}
filter={logLevels}
variant={isDarkMode || !isSandy ? 'dark' : 'light'}
variant={isDarkMode ? 'dark' : 'light'}
styles={styles}
/>
</Layout.ScrollContainer>
@@ -114,15 +112,7 @@ export function ConsoleLogs() {
);
}
function buildTheme(isSandy: boolean): Styles {
if (!isSandy) {
const bg = '#333';
return {
BASE_BACKGROUND_COLOR: bg,
BASE_COLOR: 'white',
LOG_BACKGROUND: bg,
};
}
function buildTheme(): Styles {
return {
// See: https://github.com/samdenty/console-feed/blob/master/src/definitions/Styles.d.ts
BASE_BACKGROUND_COLOR: 'transparent',

View File

@@ -12,7 +12,6 @@ import ReactDOM from 'react-dom';
import Sidebar from '../ui/components/Sidebar';
import {toggleRightSidebarAvailable} from '../reducers/application';
import {useDispatch, useStore} from '../utils/useStore';
import {useIsSandy} from '../sandy-chrome/SandyContext';
import {ContentContainer} from '../sandy-chrome/ContentContainer';
import {Layout} from '../ui';
@@ -33,7 +32,6 @@ export default function DetailSidebar({children, width, minWidth}: OwnProps) {
return <div>{children}</div>;
}
const isSandy = useIsSandy();
const dispatch = useDispatch();
const {rightSidebarAvailable, rightSidebarVisible} = useStore((state) => {
const {rightSidebarAvailable, rightSidebarVisible} = state.application;
@@ -72,16 +70,10 @@ export default function DetailSidebar({children, width, minWidth}: OwnProps) {
minWidth={minWidth}
width={width || 300}
position="right"
gutter={isSandy}>
{isSandy ? (
gutter>
<ContentContainer>
<Layout.ScrollContainer vertical>
{children}
</Layout.ScrollContainer>
<Layout.ScrollContainer vertical>{children}</Layout.ScrollContainer>
</ContentContainer>
) : (
children
)}
</Sidebar>,
domNode,
)) ||

View File

@@ -1,199 +0,0 @@
/**
* 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 {Button, styled} from '../ui';
import {connect, ReactReduxContext} from 'react-redux';
import {selectDevice, preferDevice} from '../reducers/connections';
import {
setActiveSheet,
ActiveSheet,
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
} from '../reducers/application';
import {showOpenDialog} from '../utils/exportData';
import BaseDevice from '../devices/BaseDevice';
import React, {Component} from 'react';
import {State} from '../reducers';
import GK from '../fb-stubs/GK';
import {launchEmulator} from '../devices/AndroidDevice';
type StateFromProps = {
selectedDevice: BaseDevice | null | undefined;
androidEmulators: Array<string>;
devices: Array<BaseDevice>;
};
type DispatchFromProps = {
selectDevice: (device: BaseDevice) => void;
preferDevice: (device: string) => void;
setActiveSheet: (sheet: ActiveSheet) => void;
};
type OwnProps = {};
type Props = OwnProps & StateFromProps & DispatchFromProps;
const DropdownButton = styled(Button)({
fontSize: 11,
});
class DevicesButton extends Component<Props> {
launchEmulator = async (name: string) => {
await launchEmulator(name);
this.props.preferDevice(name);
};
render() {
const {
devices,
androidEmulators,
selectedDevice,
selectDevice,
} = this.props;
let buttonLabel = 'No device selected';
let icon = 'minus-circle';
if (selectedDevice && selectedDevice.isArchived) {
buttonLabel = `${selectedDevice.displayTitle() || 'Unknown device'}`;
icon = 'box';
} else if (selectedDevice && selectedDevice.deviceType === 'physical') {
buttonLabel = selectedDevice.displayTitle() || 'Unknown device';
icon = 'mobile';
} else if (selectedDevice && selectedDevice.deviceType === 'emulator') {
buttonLabel = selectedDevice.displayTitle() || 'Unknown emulator';
icon = 'desktop';
}
const dropdown: any[] = [];
// Physical devices
const connectedDevices = [
{
label: 'Connected Devices',
enabled: false,
},
...devices
.filter((device) => device.deviceType === 'physical')
.map((device: BaseDevice) => ({
click: () => selectDevice(device),
checked: device === selectedDevice,
label: `📱 ${device.displayTitle()}`,
type: 'checkbox',
})),
];
if (connectedDevices.length > 1) {
dropdown.push(...connectedDevices);
}
// Emulators
const runningEmulators = [
{
label: 'Running Emulators',
enabled: false,
},
...devices
.filter((device) => device.deviceType === 'emulator')
.map((device: BaseDevice) => ({
click: () => selectDevice(device),
checked: device === selectedDevice,
label: device.displayTitle(),
type: 'checkbox',
})),
];
if (runningEmulators.length > 1) {
dropdown.push(...runningEmulators);
}
// Archived
const importedFiles = [
{
label: 'Disconnected Devices',
enabled: false,
},
...devices
.filter((device) => device.isArchived)
.map((device: BaseDevice) => ({
click: () => selectDevice(device),
checked: device === selectedDevice,
label: `📦 ${device.displayTitle()}`,
type: 'checkbox',
})),
];
if (importedFiles.length > 1) {
dropdown.push(...importedFiles);
}
// Launch JS emulator
if (GK.get('flipper_js_client_emulator')) {
dropdown.push(
{type: 'separator' as 'separator'},
{
label: 'Launch JS Web App',
click: () =>
this.props.setActiveSheet(ACTIVE_SHEET_JS_EMULATOR_LAUNCHER),
},
);
}
// Launch Android emulators
if (androidEmulators.length > 0) {
const emulators = Array.from(androidEmulators)
.filter(
(name: string) =>
devices.findIndex(
(device: BaseDevice) =>
device.title === name && !device.isArchived,
) === -1,
)
.map((name: string) => ({
label: name,
click: () => this.launchEmulator(name),
}));
if (emulators.length > 0) {
dropdown.push(
{type: 'separator' as 'separator'},
{
label: 'Launch Android emulators',
enabled: false,
},
...emulators,
);
}
}
if (dropdown.length > 0) {
dropdown.push({type: 'separator' as 'separator'});
}
return (
<ReactReduxContext.Consumer>
{({store}) => {
dropdown.push({
label: 'Open File...',
click: () => {
showOpenDialog(store);
},
});
return (
<DropdownButton compact={true} icon={icon} dropdown={dropdown}>
{buttonLabel}
</DropdownButton>
);
}}
</ReactReduxContext.Consumer>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, State>(
({connections: {devices, androidEmulators, selectedDevice}}) => ({
devices,
androidEmulators,
selectedDevice,
}),
{
selectDevice,
preferDevice,
setActiveSheet,
},
)(DevicesButton);

View File

@@ -1,185 +0,0 @@
/**
* 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 {styled, colors} from '../ui';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {
setActiveSheet,
ActiveSheet,
ACTIVE_SHEET_DOCTOR,
ACTIVE_SHEET_SETTINGS,
} from '../reducers/application';
import {State as Store} from '../reducers/index';
import {ButtonGroup, Button} from '../ui';
import {FlexColumn, FlexRow} from '../ui';
import runHealthchecks, {
HealthcheckSettings,
HealthcheckEventsHandler,
} from '../utils/runHealthchecks';
import {
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
HealthcheckReport,
HealthcheckResult,
} from '../reducers/healthchecks';
import {reportUsage} from '../utils/metrics';
type StateFromProps = {
healthcheckReport: HealthcheckReport;
} & HealthcheckSettings;
type DispatchFromProps = {
setActiveSheet: (payload: ActiveSheet) => void;
} & HealthcheckEventsHandler;
type State = {visible: boolean; message: string; showSettingsButton: boolean};
type Props = DispatchFromProps & StateFromProps;
class DoctorBar extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
visible: false,
message: '',
showSettingsButton: false,
};
}
componentDidMount() {
this.showMessageIfChecksFailed();
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
const failedCategories = Object.values(
props.healthcheckReport.categories,
).filter((cat) => hasProblems(cat.result));
if (failedCategories.length == 1) {
const failedCat = failedCategories[0];
if (failedCat.key === 'ios' || failedCat.key === 'android') {
return {
...state,
message: `Doctor has discovered problems with your ${failedCat.label} setup. If you are not interested in ${failedCat.label} development you can disable it in Settings.`,
showSettingsButton: true,
};
}
}
if (failedCategories.length) {
return {
...state,
message: 'Doctor has discovered problems with your installation.',
showSettingsButton: false,
};
}
return null;
}
async showMessageIfChecksFailed() {
await runHealthchecks(this.props);
const result = this.props.healthcheckReport.result;
if (hasProblems(result)) {
if (result.isAcknowledged) {
reportUsage('doctor:warning:suppressed');
} else {
this.setVisible(true);
reportUsage('doctor:warning:shown');
}
}
}
render() {
return (
this.state.visible && (
<Container>
<WarningContainer>
<FlexRow style={{flexDirection: 'row-reverse'}}>
<ButtonSection>
<ButtonGroup>
<Button
onClick={() => {
reportUsage('doctor:report:opened:fromWarningBar');
this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR);
this.setVisible(false);
}}>
Show Problems
</Button>
{this.state.showSettingsButton && (
<Button
onClick={() => {
reportUsage('settings:opened:fromWarningBar');
this.props.setActiveSheet(ACTIVE_SHEET_SETTINGS);
}}>
Show Settings
</Button>
)}
<Button onClick={() => this.setVisible(false)}>
Dismiss
</Button>
</ButtonGroup>
</ButtonSection>
<FlexColumn style={{flexGrow: 1}}>
{this.state.message}
</FlexColumn>
</FlexRow>
</WarningContainer>
</Container>
)
);
}
setVisible(visible: boolean) {
this.setState((prevState) => {
return {
...prevState,
visible,
};
});
}
}
export default connect<StateFromProps, DispatchFromProps, {}, Store>(
({settingsState, healthchecks: {healthcheckReport}}) => ({
healthcheckReport,
settings: settingsState,
}),
{
setActiveSheet,
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
},
)(DoctorBar);
const Container = styled.div({
boxShadow: '2px 2px 2px #ccc',
userSelect: 'text',
});
const WarningContainer = styled.div({
backgroundColor: colors.orange,
color: '#fff',
maxHeight: '600px',
overflowY: 'auto',
overflowX: 'hidden',
transition: 'max-height 0.3s ease',
'&.collapsed': {
maxHeight: '0px',
},
padding: '4px 12px',
borderBottom: '1px solid ' + colors.orangeDark3,
verticalAlign: 'middle',
lineHeight: '28px',
});
const ButtonSection = styled(FlexColumn)({
marginLeft: '8px',
flexShrink: 0,
flexGrow: 0,
});
function hasProblems(result: HealthcheckResult) {
return result.status === 'WARNING' || result.status === 'FAILED';
}

View File

@@ -222,7 +222,7 @@ function hasNewProblems(result: HealthcheckResult) {
return hasProblems(result) && !result.isAcknowledged;
}
export type State = {
type State = {
acknowledgeCheckboxVisible: boolean;
acknowledgeOnClose?: boolean;
selectedCheckKey?: string;

View File

@@ -7,16 +7,7 @@
* @format
*/
import {
FlexColumn,
Button,
styled,
Text,
FlexRow,
Spacer,
Input,
Label,
} from '../ui';
import {Button} from '../ui';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
@@ -26,31 +17,8 @@ import {Settings} from '../reducers/settings';
import {Collapse, Form, Input as AntInput} from 'antd';
import {Html5Outlined} from '@ant-design/icons';
const Container = styled(FlexColumn)({
padding: 20,
width: 800,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
const textareaStyle = {
margin: 0,
marginBottom: 10,
};
const TitleInput = styled(Input)({
...textareaStyle,
height: 30,
});
type OwnProps = {
onHide: () => void;
useSandy?: boolean;
};
type StateFromProps = {
@@ -96,7 +64,7 @@ class JSEmulatorLauncherSheet extends Component<Props, State> {
render() {
const {url, height, width} = this.state;
return this.props.useSandy ? (
return (
<Form labelCol={{span: 4}}>
<Form.Item label="Url">
<AntInput value={url} onChange={this.onUrlChange} />
@@ -113,27 +81,6 @@ class JSEmulatorLauncherSheet extends Component<Props, State> {
</Button>
</Form.Item>
</Form>
) : (
<Container>
<Title>Launch Web App</Title>
<Label>Url</Label>
<TitleInput value={url} onChange={this.onUrlChange} />
<Label>Height</Label>
<TitleInput value={height} onChange={this.onHeightChange} />
<Label>Width</Label>
<TitleInput value={width} onChange={this.onWidthChange} />
<br />
<FlexRow>
<Spacer />
<Button compact padded onClick={this.props.onHide}>
Cancel
</Button>
<Button type="primary" compact padded onClick={this.applyChanges}>
Launch
</Button>
</FlexRow>
</Container>
);
}
}
@@ -154,7 +101,7 @@ export function JSEmulatorLauncherSheetSandy({onClose}: {onClose(): void}) {
extra={<Html5Outlined />}
header="Launch JS Web App"
key="launch-js-web-app">
<Launcher onHide={onClose} useSandy />
<Launcher onHide={onClose} />
</Collapse.Panel>
</Collapse>
);

View File

@@ -1,138 +0,0 @@
/**
* 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 from 'react';
import {FlexRow, styled, Layout} from '../ui';
import {connect} from 'react-redux';
import TitleBar from './TitleBar';
import MainSidebar2 from './mainsidebar/MainSidebar2';
import DoctorBar from './DoctorBar';
import PluginContainer from '../PluginContainer';
import {ipcRenderer, remote} from 'electron';
import {
ACTIVE_SHEET_CHANGELOG_RECENT_ONLY,
setActiveSheet,
} from '../reducers/application';
import {Logger} from '../fb-interfaces/Logger';
import {State as Store} from '../reducers/index';
import {StaticView} from '../reducers/connections';
import StatusBar from './StatusBar';
import {hasNewChangesToShow} from './ChangelogSheet';
import QPL, {QuickLogActionType, FLIPPER_QPL_EVENTS} from '../fb-stubs/QPL';
import {SheetRenderer} from './SheetRenderer';
const version = remote.app.getVersion();
type OwnProps = {
logger: Logger;
};
type StateFromProps = {
leftSidebarVisible: boolean;
staticView: StaticView;
};
type DispatchProps = {
setActiveSheet: typeof setActiveSheet;
};
/**
* This wrapper is only needed for hacky plugins that place contents out of
* contents, like hermes debugger
*/
const PluginContent = styled(FlexRow)({
width: '100%',
height: '100%',
position: 'relative',
});
PluginContent.displayName = 'App:PluginContent';
type Props = StateFromProps & OwnProps & DispatchProps;
export function registerStartupTime(logger: Logger) {
// track time since launch
const [s, ns] = process.hrtime();
const launchEndTime = s * 1e3 + ns / 1e6;
ipcRenderer.on('getLaunchTime', (_: any, launchStartTime: number) => {
logger.track('performance', 'launchTime', launchEndTime - launchStartTime);
QPL.markerStart(FLIPPER_QPL_EVENTS.STARTUP, 0, launchStartTime);
QPL.markerEnd(
FLIPPER_QPL_EVENTS.STARTUP,
QuickLogActionType.SUCCESS,
0,
launchEndTime,
);
});
ipcRenderer.send('getLaunchTime');
ipcRenderer.send('componentDidMount');
}
export class LegacyApp extends React.Component<Props> {
componentDidMount() {
registerStartupTime(this.props.logger);
if (hasNewChangesToShow(window.localStorage)) {
this.props.setActiveSheet(ACTIVE_SHEET_CHANGELOG_RECENT_ONLY);
}
}
render() {
return (
<Layout.Top>
<Layout.Top>
<>
<TitleBar version={version} />
<DoctorBar />
</>
<>
<SheetRenderer logger={this.props.logger} />
</>
</Layout.Top>
<Layout.Bottom>
<Layout.Left>
{this.props.leftSidebarVisible && <MainSidebar2 />}
<PluginContent>
{this.props.staticView != null ? (
React.createElement(this.props.staticView, {
logger: this.props.logger,
})
) : (
<PluginContainer logger={this.props.logger} />
)}
<div>
<div
id="flipper-out-of-contents-container"
style={{
display: 'none',
position: 'absolute',
right: 0,
bottom: 0,
left: 0,
top: 0,
}}
/>
</div>
</PluginContent>
</Layout.Left>
<StatusBar />
</Layout.Bottom>
</Layout.Top>
);
}
}
export default connect<StateFromProps, DispatchProps, OwnProps, Store>(
({application: {leftSidebarVisible}, connections: {staticView}}) => ({
leftSidebarVisible,
staticView,
}),
{
setActiveSheet,
},
)(LegacyApp);

View File

@@ -1,136 +0,0 @@
/**
* 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, {useCallback, useEffect} from 'react';
import {platform} from 'os';
import {useValue} from 'flipper-plugin';
import {Button, styled} from '../ui';
import {useStore} from '../utils/useStore';
import {useMemoize} from '../utils/useMemoize';
import {State} from '../reducers';
// eslint-disable-next-line flipper/no-relative-imports-across-packages
import type {NavigationPlugin} from '../../../plugins/navigation/index';
// eslint-disable-next-line flipper/no-relative-imports-across-packages
import type {Bookmark} from '../../../plugins/navigation/types';
const DropdownButton = styled(Button)({
fontSize: 11,
});
const shortenText = (text: string, MAX_CHARACTERS = 30): string => {
if (text.length <= MAX_CHARACTERS) {
return text;
} else {
return text.split('').slice(0, MAX_CHARACTERS).join('') + '...';
}
};
const NAVIGATION_PLUGIN_ID = 'Navigation';
export function LocationsButton() {
const navPlugin = useStore(navPluginStateSelector);
return navPlugin ? (
<ActiveLocationsButton navPlugin={navPlugin} />
) : (
<DropdownButton compact={true}>(none)</DropdownButton>
);
}
function ActiveLocationsButton({navPlugin}: {navPlugin: NavigationPlugin}) {
const currentURI = useValue(navPlugin.currentURI);
const bookmarks = useValue(navPlugin.bookmarks);
const keyDown = useCallback(
(e: KeyboardEvent) => {
if (
((platform() === 'darwin' && e.metaKey) ||
(platform() !== 'darwin' && e.ctrlKey)) &&
/^\d$/.test(e.key) &&
bookmarks.size >= parseInt(e.key, 10)
) {
navPlugin.navigateTo(
Array.from(bookmarks.values())[parseInt(e.key, 10) - 1].uri,
);
}
},
[bookmarks, navPlugin],
);
useEffect(() => {
document.addEventListener('keydown', keyDown);
return () => {
document.removeEventListener('keydown', keyDown);
};
}, [keyDown]);
const dropdown = useMemoize(computeBookmarkDropdown, [
navPlugin,
bookmarks,
currentURI,
]);
return (
<DropdownButton compact={true} dropdown={dropdown}>
{(currentURI && shortenText(currentURI)) || '(none)'}
</DropdownButton>
);
}
export function navPluginStateSelector(state: State) {
const {selectedApp, clients} = state.connections;
if (!selectedApp) return undefined;
const client = clients.find((client) => client.id === selectedApp);
if (!client) return undefined;
return client.sandyPluginStates.get(NAVIGATION_PLUGIN_ID)?.instanceApi as
| undefined
| NavigationPlugin;
}
function computeBookmarkDropdown(
navPlugin: NavigationPlugin,
bookmarks: Map<string, Bookmark>,
currentURI: string,
) {
const dropdown: Electron.MenuItemConstructorOptions[] = [
{
label: 'Bookmarks',
enabled: false,
},
...Array.from(bookmarks.values()).map((bookmark, i) => {
return {
click: () => {
navPlugin.navigateTo(bookmark.uri);
},
accelerator: i < 9 ? `CmdOrCtrl+${i + 1}` : undefined,
label: shortenText(
(bookmark.commonName ? bookmark.commonName + ' - ' : '') +
bookmark.uri,
100,
),
};
}),
];
if (currentURI) {
dropdown.push(
{type: 'separator'},
{
label: 'Bookmark Current Location',
click: () => {
navPlugin.addBookmark({
uri: currentURI,
commonName: null,
});
},
},
);
}
return dropdown;
}

View File

@@ -8,53 +8,14 @@
*/
import React, {useCallback, useEffect, useState} from 'react';
import {Button, ButtonGroup, colors} from '../ui';
import MetroDevice, {MetroReportableEvent} from '../devices/MetroDevice';
import styled from '@emotion/styled';
import {useStore} from '../utils/useStore';
import {Button as AntButton} from 'antd';
import {MenuOutlined, ReloadOutlined} from '@ant-design/icons';
type LogEntry = {};
export type PersistedState = {
logs: LogEntry[];
};
function ProgressBar({
progress,
width,
color,
}: {
progress: number;
width: number;
color: string;
}) {
return (
<ProgressBarContainer width={width} color={color}>
<ProgressBarBar progress={progress} color={color} />
</ProgressBarContainer>
);
}
const ProgressBarContainer = styled.div<{width: number; color: string}>(
({width, color}) => ({
border: `1px solid ${color}`,
borderRadius: 4,
height: 6,
width: width,
}),
);
const ProgressBarBar = styled.div<{progress: number; color: string}>(
({progress, color}) => ({
background: color,
width: `${Math.min(100, Math.round(progress * 100))}%`,
height: 4,
}),
);
export default function MetroButton({useSandy}: {useSandy?: boolean}) {
export default function MetroButton() {
const device = useStore((state) =>
state.connections.devices.find(
(device) => device.os === 'Metro' && !device.isArchived,
@@ -68,7 +29,7 @@ export default function MetroButton({useSandy}: {useSandy?: boolean}) {
[device],
);
const [progress, setProgress] = useState(1);
const [hasBuildError, setHasBuildError] = useState(false);
const [_hasBuildError, setHasBuildError] = useState(false);
useEffect(() => {
if (!device) {
@@ -98,7 +59,7 @@ export default function MetroButton({useSandy}: {useSandy?: boolean}) {
return null;
}
return useSandy ? (
return (
<>
<AntButton
icon={<ReloadOutlined />}
@@ -118,31 +79,5 @@ export default function MetroButton({useSandy}: {useSandy?: boolean}) {
}}
/>
</>
) : (
<ButtonGroup>
<Button
title="Reload React Native App"
icon="arrows-circle"
compact
onClick={() => {
sendCommand('reload');
}}>
{progress < 1 ? (
<ProgressBar
progress={100 * progress}
color={hasBuildError ? colors.red : colors.cyan}
width={20}
/>
) : null}
</Button>
<Button
title="Open the React Native Dev Menu on the device"
icon="navicon"
compact
onClick={() => {
sendCommand('devMenu');
}}
/>
</ButtonGroup>
);
}

View File

@@ -1,199 +0,0 @@
/**
* 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 {
FlexColumn,
FlexRow,
FlexBox,
Text,
Button,
styled,
colors,
} from '../ui';
import React, {PureComponent} from 'react';
const Anchor = styled.img({
zIndex: 6,
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translate(-50%, calc(100% + 2px))',
});
const PopoverContainer = styled(FlexColumn)({
backgroundColor: colors.white,
borderRadius: 7,
border: '1px solid rgba(0,0,0,0.3)',
boxShadow: '0 2px 10px 0 rgba(0,0,0,0.3)',
position: 'absolute',
zIndex: 5,
minWidth: 240,
bottom: 0,
marginTop: 15,
left: '50%',
transform: 'translate(-50%, calc(100% + 15px))',
overflow: 'hidden',
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
height: 13,
top: -13,
width: 26,
backgroundColor: colors.white,
},
});
const Heading = styled(Text)({
display: 'block',
backgroundColor: colors.white,
color: colors.light30,
fontSize: 11,
fontWeight: 600,
lineHeight: '21px',
padding: '4px 8px 0',
});
const PopoverItem = styled(FlexRow)({
alignItems: 'center',
borderBottom: `1px solid ${colors.light05}`,
height: 50,
'&:last-child': {
borderBottom: 'none',
},
});
const ItemTitle = styled(Text)({
display: 'block',
fontSize: 14,
fontWeight: 400,
lineHeight: '120%',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
marginBottom: 1,
});
const ItemSubtitle = styled(Text)({
display: 'block',
fontWeight: 400,
fontSize: 11,
color: colors.light30,
lineHeight: '14px',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
});
const ItemImage = styled(FlexBox)({
alignItems: 'center',
justifyContent: 'center',
width: 40,
flexShrink: 0,
});
const ItemContent = styled.div({
minWidth: 0,
paddingRight: 5,
flexGrow: 1,
});
const Section = styled.div({
borderBottom: `1px solid ${colors.light05}`,
'&:last-child': {
borderBottom: 'none',
},
});
const Action = styled(Button)({
border: `1px solid ${colors.macOSTitleBarButtonBorder}`,
background: 'transparent',
color: colors.macOSTitleBarIconSelected,
marginRight: 8,
lineHeight: '22px',
'&:hover': {
background: 'transparent',
},
'&:active': {
background: 'transparent',
border: `1px solid ${colors.macOSTitleBarButtonBorder}`,
},
});
type Props = {
sections: Array<{
title: string;
items: Array<{
title: string;
subtitle: string;
onClick: (() => void) | null | undefined;
icon: Element | null | undefined;
}>;
}>;
onDismiss: Function;
};
export default class Popover extends PureComponent<Props> {
_ref?: Element | null;
componentDidMount() {
window.document.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
window.document.addEventListener('click', this.handleClick);
}
handleClick = (e: MouseEvent) => {
if (this._ref && !this._ref.contains(e.target as HTMLElement)) {
this.props.onDismiss();
}
};
_setRef = (ref: Element | null) => {
this._ref = ref;
};
render() {
return (
<>
<Anchor src="./anchor.svg" key="anchor" />
<PopoverContainer ref={this._setRef} key="popup">
{this.props.sections.map((section) => {
if (section.items.length > 0) {
return (
<Section key={section.title}>
<Heading>{section.title}</Heading>
{section.items.map((item) => (
<PopoverItem key={item.title}>
<ItemImage>{item.icon}</ItemImage>
<ItemContent>
<ItemTitle>{item.title}</ItemTitle>
<ItemSubtitle>{item.subtitle}</ItemSubtitle>
</ItemContent>
{item.onClick && (
<Action onClick={item.onClick} compact={true}>
Run
</Action>
)}
</PopoverItem>
))}
</Section>
);
} else {
return null;
}
})}
</PopoverContainer>
</>
);
}
}

View File

@@ -30,8 +30,6 @@ import {LeftRailButton} from '../sandy-chrome/LeftRail';
import GK from '../fb-stubs/GK';
import * as UserFeedback from '../fb-stubs/UserFeedback';
import {FeedbackPrompt} from '../fb-stubs/UserFeedback';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
import {StarOutlined} from '@ant-design/icons';
import {Popover, Rate} from 'antd';
import {useStore} from '../utils/useStore';
@@ -458,7 +456,3 @@ export function SandyRatingButton() {
</Popover>
);
}
export default connect<{sessionId: string | null}, null, {}, Store>(
({application: {sessionId}}) => ({sessionId}),
)(RatingButton);

View File

@@ -8,7 +8,6 @@
*/
import {Button as AntButton, message} from 'antd';
import {Button, ButtonGroup} from '../ui';
import React, {useState, useEffect, useCallback} from 'react';
import path from 'path';
import open from 'open';
@@ -16,7 +15,7 @@ import {capture, CAPTURE_LOCATION, getFileName} from '../utils/screenshot';
import {CameraOutlined, VideoCameraOutlined} from '@ant-design/icons';
import {useStore} from '../utils/useStore';
export async function openFile(path: string | null) {
async function openFile(path: string | null) {
if (!path) {
return;
}
@@ -28,7 +27,7 @@ export async function openFile(path: string | null) {
}
}
export default function ScreenCaptureButtons({useSandy}: {useSandy?: boolean}) {
export default function ScreenCaptureButtons() {
const selectedDevice = useStore((state) => state.connections.selectedDevice);
const [isTakingScreenshot, setIsTakingScreenshot] = useState(false);
const [isRecordingAvailable, setIsRecordingAvailable] = useState(false);
@@ -84,7 +83,7 @@ export default function ScreenCaptureButtons({useSandy}: {useSandy?: boolean}) {
}
}, [selectedDevice, isRecording]);
return useSandy ? (
return (
<>
<AntButton
icon={<CameraOutlined />}
@@ -103,24 +102,5 @@ export default function ScreenCaptureButtons({useSandy}: {useSandy?: boolean}) {
danger={isRecording}
/>
</>
) : (
<ButtonGroup>
<Button
compact={true}
onClick={handleScreenshot}
icon="camera"
title="Take Screenshot"
disabled={!selectedDevice}
/>
<Button
compact={true}
onClick={handleRecording}
icon={isRecording ? 'stop-playback' : 'camcorder'}
pulse={isRecording}
selected={isRecording}
title="Make Screen Recording"
disabled={!selectedDevice || !isRecordingAvailable}
/>
</ButtonGroup>
);
}

View File

@@ -7,7 +7,7 @@
* @format
*/
import {FlexColumn, Button, styled, Text, FlexRow, Spacer} from '../ui';
import {FlexColumn, Button} from '../ui';
import React, {Component, useContext} from 'react';
import {updateSettings, Action} from '../reducers/settings';
import {
@@ -29,22 +29,10 @@ import {reportUsage} from '../utils/metrics';
import {Modal, message} from 'antd';
import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin';
const Container = styled(FlexColumn)({
padding: 20,
width: 800,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
type OwnProps = {
useSandy?: boolean;
onHide: () => void;
platform: NodeJS.Platform;
noModal?: boolean;
};
type StateFromProps = {
@@ -114,23 +102,6 @@ class SettingsSheet extends Component<Props, State> {
);
}
renderNativeContainer(
contents: React.ReactElement,
footer: React.ReactElement,
) {
return (
<Container>
<Title>Settings</Title>
{contents}
<br />
<FlexRow>
<Spacer />
{footer}
</FlexRow>
</Container>
);
}
render() {
const {
enableAndroid,
@@ -140,12 +111,9 @@ class SettingsSheet extends Component<Props, State> {
enablePrefetching,
idbPath,
reactNative,
disableSandy,
darkMode,
} = this.state.updatedSettings;
const {useSandy} = this.props;
const settingsPristine =
isEqual(this.props.settings, this.state.updatedSettings) &&
isEqual(this.props.launcherSettings, this.state.updatedLauncherSettings);
@@ -264,29 +232,6 @@ class SettingsSheet extends Component<Props, State> {
});
}}
/>
<ToggledSection
label="Disable Sandy UI"
toggled={this.state.updatedSettings.disableSandy}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
disableSandy: v,
},
forcedRestartSettings: {
...this.state.forcedRestartSettings,
disableSandy: v,
},
});
}}>
{' '}
<ConfigText
content={
'If disabled, Flipper will fall back to the classic design.'
}
/>
</ToggledSection>
{!disableSandy && (
<ToggledSection
label="Enable dark theme (experimental)"
toggled={darkMode}
@@ -299,7 +244,6 @@ class SettingsSheet extends Component<Props, State> {
}));
}}
/>
)}
<ToggledSection
label="React Native keyboard shortcuts"
toggled={reactNative.shortcuts.enabled}
@@ -388,9 +332,14 @@ class SettingsSheet extends Component<Props, State> {
</>
);
return useSandy
? this.renderSandyContainer(contents, footer)
: this.renderNativeContainer(contents, footer);
return this.props.noModal ? (
<>
{contents}
{footer}
</>
) : (
this.renderSandyContainer(contents, footer)
);
}
}

View File

@@ -7,17 +7,7 @@
* @format
*/
import {
FlexColumn,
Button,
styled,
Text,
FlexRow,
Spacer,
Input,
Link,
colors,
} from '../ui';
import {FlexColumn, Button, styled, Text, Input, Link, colors} from '../ui';
import React, {Component} from 'react';
import {writeKeychain, getUser} from '../fb-stubs/user';
import {login} from '../reducers/user';
@@ -29,11 +19,6 @@ import {reportPlatformFailures} from '../utils/metrics';
import {Modal} from 'antd';
import {TrackingScope} from 'flipper-plugin';
const Container = styled(FlexColumn)({
padding: 20,
width: 500,
});
const Title = styled(Text)({
marginBottom: 6,
fontWeight: 600,
@@ -49,7 +34,6 @@ const TokenInput = styled(Input)({
});
type OwnProps = {
useSandy?: boolean;
onHide: () => any;
};
@@ -150,22 +134,6 @@ class SignInSheet extends Component<Props, State> {
);
}
renderNativeContainer(
contents: React.ReactElement,
footer: React.ReactElement,
) {
return (
<Container>
{contents}
<br />
<FlexRow>
<Spacer />
{footer}
</FlexRow>
</Container>
);
}
render() {
const content = (
<>
@@ -212,9 +180,7 @@ class SignInSheet extends Component<Props, State> {
</>
);
return this.props.useSandy
? this.renderSandyContainer(content, footer)
: this.renderNativeContainer(content, footer);
return this.renderSandyContainer(content, footer);
}
}

View File

@@ -1,54 +0,0 @@
/**
* 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 {colors} from '../ui/components/colors';
import {styled} from '../ui';
import {connect} from 'react-redux';
import {State} from '../reducers';
import React, {ReactElement} from 'react';
import Text from '../ui/components/Text';
const StatusBarContainer = styled(Text)({
backgroundColor: colors.macOSTitleBarBackgroundBlur,
borderTop: '1px solid #b3b3b3',
lineHeight: '26px',
padding: '0 10px',
textOverflow: 'ellipsis',
overflow: 'hidden',
textAlign: 'center',
});
type Props = {
statusMessage: React.ReactNode | string | null;
};
export function statusBarView(props: Props): ReactElement | null {
const {statusMessage} = props;
if (statusMessage) {
return (
<StatusBarContainer whiteSpace="nowrap">
{statusMessage}
</StatusBarContainer>
);
} else {
return null;
}
}
export default connect<Props, void, {}, State>((state: State) => {
const {
application: {statusMessages},
} = state;
if (statusMessages.length > 0) {
return {statusMessage: statusMessages[statusMessages.length - 1]};
}
return {
statusMessage: null,
};
})(statusBarView);

View File

@@ -1,247 +0,0 @@
/**
* 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 {
ActiveSheet,
LauncherMsg,
ShareType,
setActiveSheet,
toggleLeftSidebarVisible,
toggleRightSidebarVisible,
ACTIVE_SHEET_SETTINGS,
ACTIVE_SHEET_DOCTOR,
} from '../reducers/application';
import {
colors,
Button,
ButtonGroup,
ButtonGroupChain,
FlexRow,
Spacer,
styled,
Text,
LoadingIndicator,
} from '../ui';
import {connect} from 'react-redux';
import RatingButton from './RatingButton';
import DevicesButton from './DevicesButton';
import {LocationsButton} from './LocationsButton';
import ScreenCaptureButtons from './ScreenCaptureButtons';
import UpdateIndicator from './UpdateIndicator';
import config from '../fb-stubs/config';
import {clipboard} from 'electron';
import React from 'react';
import {State} from '../reducers';
import {reportUsage} from '../utils/metrics';
import MetroButton from './MetroButton';
import {navPluginStateSelector} from './LocationsButton';
import {getVersionString} from '../utils/versionString';
const AppTitleBar = styled(FlexRow)<{focused?: boolean}>(({focused}) => ({
userSelect: 'none',
background: focused
? `linear-gradient(to bottom, ${colors.macOSTitleBarBackgroundTop} 0%, ${colors.macOSTitleBarBackgroundBottom} 100%)`
: colors.macOSTitleBarBackgroundBlur,
borderBottom: `1px solid ${
focused ? colors.macOSTitleBarBorder : colors.macOSTitleBarBorderBlur
}`,
height: 38,
flexShrink: 0,
width: '100%',
alignItems: 'center',
paddingLeft: 80,
paddingRight: 10,
justifyContent: 'space-between',
WebkitAppRegion: 'drag',
zIndex: 6,
}));
type OwnProps = {
version: string;
};
type DispatchFromProps = {
toggleLeftSidebarVisible: (visible?: boolean) => void;
toggleRightSidebarVisible: (visible?: boolean) => void;
setActiveSheet: (sheet: ActiveSheet) => void;
};
type StateFromProps = {
windowIsFocused: boolean;
leftSidebarVisible: boolean;
rightSidebarVisible: boolean;
rightSidebarAvailable: boolean;
downloadingImportData: boolean;
launcherMsg: LauncherMsg;
share: ShareType | null | undefined;
navPluginIsActive: boolean;
};
const VersionText = styled(Text)({
color: colors.light50,
marginLeft: 4,
marginTop: 2,
cursor: 'pointer',
display: 'block',
padding: '4px 10px',
'&:hover': {
backgroundColor: `rgba(0,0,0,0.05)`,
borderRadius: '999em',
},
});
export class Version extends React.Component<
{children: string},
{copied: boolean}
> {
state = {
copied: false,
};
_onClick = () => {
clipboard.writeText(this.props.children);
this.setState({copied: true});
setTimeout(() => this.setState({copied: false}), 1000);
};
render() {
return (
<VersionText onClick={this._onClick}>
{this.state.copied ? 'Copied' : this.props.children}
</VersionText>
);
}
}
const Importing = styled(FlexRow)({
color: colors.light50,
alignItems: 'center',
marginLeft: 10,
});
function statusMessageComponent(
downloadingImportData: boolean,
statusComponent?: React.ReactNode | undefined,
) {
if (downloadingImportData) {
return (
<Importing>
<LoadingIndicator size={16} />
&nbsp;Importing data...
</Importing>
);
}
if (statusComponent) {
return statusComponent;
}
return;
}
type Props = OwnProps & DispatchFromProps & StateFromProps;
class TitleBar extends React.Component<Props, StateFromProps> {
render() {
const {navPluginIsActive, share} = this.props;
return (
<AppTitleBar focused={this.props.windowIsFocused} className="toolbar">
{navPluginIsActive ? (
<ButtonGroupChain iconSize={12}>
<DevicesButton />
<LocationsButton />
</ButtonGroupChain>
) : (
<DevicesButton />
)}
<MetroButton />
<ScreenCaptureButtons />
{statusMessageComponent(
this.props.downloadingImportData,
share != null ? share.statusComponent : undefined,
)}
<Spacer />
{config.showFlipperRating ? <RatingButton /> : null}
<Version>{getVersionString()}</Version>
<UpdateIndicator />
<Button
icon="settings"
title="Settings"
compact={true}
onClick={() => {
this.props.setActiveSheet(ACTIVE_SHEET_SETTINGS);
reportUsage('settings:opened:fromTitleBar');
}}
/>
<Button
icon="first-aid"
title="Doctor"
compact={true}
onClick={() => {
this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR);
reportUsage('doctor:report:opened:fromTitleBar');
}}
/>
<ButtonGroup>
<Button
compact={true}
selected={this.props.leftSidebarVisible}
onClick={() => this.props.toggleLeftSidebarVisible()}
icon="icons/sidebar_left.svg"
iconSize={20}
title="Toggle Plugins"
/>
<Button
compact={true}
selected={this.props.rightSidebarVisible}
onClick={() => this.props.toggleRightSidebarVisible()}
icon="icons/sidebar_right.svg"
iconSize={20}
title="Toggle Details"
disabled={!this.props.rightSidebarAvailable}
/>
</ButtonGroup>
</AppTitleBar>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, State>(
(state) => {
const {
application: {
windowIsFocused,
leftSidebarVisible,
rightSidebarVisible,
rightSidebarAvailable,
downloadingImportData,
launcherMsg,
share,
},
} = state;
const navPluginIsActive = !!navPluginStateSelector(state);
return {
windowIsFocused,
leftSidebarVisible,
rightSidebarVisible,
rightSidebarAvailable,
downloadingImportData,
launcherMsg,
share,
navPluginIsActive,
};
},
{
setActiveSheet,
toggleLeftSidebarVisible,
toggleRightSidebarVisible,
},
)(TitleBar);

View File

@@ -1,137 +0,0 @@
/**
* 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 {User, USER_UNAUTHORIZED, USER_NOT_SIGNEDIN} from '../reducers/user';
import {ActiveSheet} from '../reducers/application';
import {styled, FlexRow, Glyph, Text, colors} from '../ui';
import {logout} from '../reducers/user';
import {setActiveSheet, ACTIVE_SHEET_SIGN_IN} from '../reducers/application';
import {connect} from 'react-redux';
import electron from 'electron';
import {findDOMNode} from 'react-dom';
import React, {PureComponent} from 'react';
import {getUser} from '../fb-stubs/user';
import config from '../fb-stubs/config';
import {State as Store} from '../reducers';
const Container = styled(FlexRow)({
alignItems: 'center',
padding: '5px 10px',
borderTop: `1px solid ${colors.blackAlpha10}`,
fontWeight: 500,
flexShrink: 0,
minHeight: 36,
color: colors.blackAlpha80,
});
const ProfilePic = styled.img({
borderRadius: '999em',
flexShrink: 0,
width: 24,
marginRight: 6,
});
const UserName = styled(Text)({
flexGrow: 1,
whiteSpace: 'nowrap',
overflow: 'hidden',
marginRight: 6,
textOverflow: 'ellipsis',
});
type OwnProps = {};
type DispatchFromProps = {
logout: () => void;
setActiveSheet: (activeSheet: ActiveSheet) => void;
};
type StateFromProps = {
user: User;
};
type Props = OwnProps & DispatchFromProps & StateFromProps;
class UserAccount extends PureComponent<Props> {
_ref: Element | null | undefined;
setRef = (ref: HTMLDivElement | null) => {
const element = findDOMNode(ref);
if (element instanceof HTMLElement) {
this._ref = element;
}
};
showDetails = () => {
const menuTemplate: Array<Electron.MenuItemConstructorOptions> = [
{
label: 'Sign Out',
click: this.props.logout,
},
];
const menu = electron.remote.Menu.buildFromTemplate(menuTemplate);
const {bottom = null, left = null} = this._ref
? this._ref.getBoundingClientRect()
: {};
menu.popup({
window: electron.remote.getCurrentWindow(),
// @ts-ignore async is not part of public api in electron menu popup
async: true,
x: left || 10,
y: (bottom || 10) + 8,
});
};
openLogin = () => this.props.setActiveSheet(ACTIVE_SHEET_SIGN_IN);
componentDidMount() {
if (config.showLogin) {
getUser().catch((error) => {
if (error === USER_UNAUTHORIZED || error === USER_NOT_SIGNEDIN) {
this.openLogin();
}
});
}
}
render() {
const {user} = this.props;
const name = user ? user.name : null;
return name ? (
<Container ref={this.setRef} onClick={this.showDetails}>
<ProfilePic
src={user.profile_picture ? user.profile_picture.uri : undefined}
/>
<UserName>{this.props.user.name}</UserName>
<Glyph name="chevron-down" size={10} variant="outline" />
</Container>
) : (
<Container onClick={this.openLogin}>
<Glyph
name="profile-circle"
size={16}
variant="outline"
color={colors.blackAlpha50}
/>
&nbsp;Sign In...
</Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({user}) => ({
user,
}),
{
logout,
setActiveSheet,
},
)(UserAccount);

View File

@@ -1,199 +0,0 @@
/**
* 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 {styled} from '../ui/index';
import FlexColumn from '../ui/components/FlexColumn';
import FlexRow from '../ui/components/FlexRow';
import Text from '../ui/components/FlexRow';
import Glyph from '../ui/components/Glyph';
import {colors, brandColors} from '../ui/components/colors';
import isProduction from '../utils/isProduction';
import constants from '../fb-stubs/constants';
import {shell, remote} from 'electron';
import {PureComponent} from 'react';
import React from 'react';
import {Logger, Tracked, TrackingScope} from 'flipper-plugin';
const Container = styled(FlexColumn)({
height: '100%',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.light02,
});
const Welcome = styled(FlexColumn)<{isMounted?: boolean}>(({isMounted}) => ({
width: 460,
background: colors.white,
borderRadius: 10,
boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
overflow: 'hidden',
opacity: isMounted ? 1 : 0,
transform: `translateY(${isMounted ? 0 : 20}px)`,
transition: '0.6s all ease-out',
}));
const Title = styled(Text)({
fontSize: 24,
fontWeight: 300,
textAlign: 'center',
color: colors.light50,
marginBottom: 16,
flexDirection: 'column',
});
const Version = styled(Text)({
textAlign: 'center',
fontSize: 11,
fontWeight: 300,
color: colors.light30,
marginBottom: 60,
flexDirection: 'column',
});
const Item = styled(FlexRow)({
padding: 10,
cursor: 'pointer',
alignItems: 'center',
borderTop: `1px solid ${colors.light10}`,
'&:hover, &:focus, &:active': {
backgroundColor: colors.light02,
textDecoration: 'none',
},
});
const ItemTitle = styled(Text)({
color: colors.light50,
fontSize: 15,
});
const ItemSubTitle = styled(Text)({
color: colors.light30,
fontSize: 11,
marginTop: 2,
});
const Icon = styled(Glyph)({
marginRight: 11,
marginLeft: 6,
});
const Logo = styled.img({
width: 128,
height: 128,
alignSelf: 'center',
marginTop: 50,
marginBottom: 20,
});
type Props = {
logger: Logger;
};
type State = {
isMounted: boolean;
};
export default class WelcomeScreen extends PureComponent<Props, State> {
state = {
isMounted: false,
};
timer: NodeJS.Timeout | null | undefined;
componentDidMount() {
// waiting sometime before showing the welcome screen to allow Flipper to
// connect to devices, if there are any
this.timer = setTimeout(
() =>
this.setState({
isMounted: true,
}),
2000,
);
}
componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer);
}
}
render() {
return (
<Container>
<TrackingScope scope="welcomescreen">
<Welcome isMounted={this.state.isMounted}>
<Logo src="./icon.png" />
<Title>Welcome to Flipper</Title>
<Version>
{isProduction() && remote
? `Version ${remote.app.getVersion()}`
: 'Development Mode'}
</Version>
<Tracked>
<Item
onClick={() => {
shell &&
shell.openExternal(
'https://fbflipper.com/docs/features/index',
);
}}>
<Icon size={20} name="rocket" color={brandColors.Flipper} />
<FlexColumn>
<ItemTitle>Using Flipper</ItemTitle>
<ItemSubTitle>
Learn how Flipper can help you debug your App
</ItemSubTitle>
</FlexColumn>
</Item>
<Item
onClick={() =>
shell &&
shell.openExternal(
'https://fbflipper.com/docs/tutorial/intro',
)
}>
<Icon size={20} name="magic-wand" color={brandColors.Flipper} />
<FlexColumn>
<ItemTitle>Create your own plugin</ItemTitle>
<ItemSubTitle>Get started with these pointers</ItemSubTitle>
</FlexColumn>
</Item>
<Item
onClick={() =>
shell &&
shell.openExternal(
'https://fbflipper.com/docs/getting-started/index',
)
}>
<Icon size={20} name="tools" color={brandColors.Flipper} />
<FlexColumn>
<ItemTitle>Add Flipper support to your app</ItemTitle>
<ItemSubTitle>Get started with these pointers</ItemSubTitle>
</FlexColumn>
</Item>
<Item
onClick={() =>
shell && shell.openExternal(constants.FEEDBACK_GROUP_LINK)
}>
<Icon size={20} name="posts" color={brandColors.Flipper} />
<FlexColumn>
<ItemTitle>Contributing and Feedback</ItemTitle>
<ItemSubTitle>
Report issues and help us improve Flipper
</ItemSubTitle>
</FlexColumn>
</Item>
</Tracked>
</Welcome>
</TrackingScope>
</Container>
);
}
}

View File

@@ -1,20 +0,0 @@
/**
* 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 {statusBarView} from '../StatusBar';
test('statusBarView returns null for empty status messages', () => {
const view = statusBarView({statusMessage: null});
expect(view).toBeNull();
});
test('statusBarView returns non null view when the list of messages is non empty', () => {
const view = statusBarView({statusMessage: 'Last Message'});
expect(view).toBeDefined();
});

View File

@@ -114,30 +114,31 @@ exports[`SettingsSheet snapshot with nothing enabled 1`] = `
<div
className="css-12n892b"
>
<div
className="css-xing9h-StyledButton enfqd41"
<button
className="ant-btn ant-btn-default"
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="button"
>
<span>
Close
</div>
</span>
</button>
</div>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="css-ia8nd7-StyledButton enfqd41"
<button
className="ant-btn ant-btn-primary"
disabled={true}
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="primary"
type="button"
>
<span>
Submit
</div>
</span>
</button>
</div>
</div>
</div>
@@ -259,30 +260,31 @@ exports[`SettingsSheet snapshot with one plugin enabled 1`] = `
<div
className="css-12n892b"
>
<div
className="css-xing9h-StyledButton enfqd41"
<button
className="ant-btn ant-btn-default"
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="button"
>
<span>
Close
</div>
</span>
</button>
</div>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="css-xing9h-StyledButton enfqd41"
<button
className="ant-btn ant-btn-primary"
disabled={false}
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="primary"
type="button"
>
<span>
Submit
</div>
</span>
</button>
</div>
</div>
</div>

View File

@@ -29,23 +29,24 @@ exports[`ShareSheetPendingDialog is rendered with status update 1`] = `
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<div
className="css-xing9h-StyledButton enfqd41"
<button
className="ant-btn ant-btn-default"
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="button"
>
<span>
Cancel
</div>
<div
className="css-xing9h-StyledButton enfqd41"
</span>
</button>
<button
className="ant-btn ant-btn-primary"
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="primary"
type="button"
>
<span>
Run In Background
</div>
</span>
</button>
</div>
</div>
`;
@@ -79,23 +80,24 @@ exports[`ShareSheetPendingDialog is rendered without status update 1`] = `
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<div
className="css-xing9h-StyledButton enfqd41"
<button
className="ant-btn ant-btn-default"
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="button"
>
<span>
Cancel
</div>
<div
className="css-xing9h-StyledButton enfqd41"
</span>
</button>
<button
className="ant-btn ant-btn-primary"
onClick={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
type="primary"
type="button"
>
<span>
Run In Background
</div>
</span>
</button>
</div>
</div>
`;

View File

@@ -1,625 +0,0 @@
/**
* 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 BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client';
import {UninitializedClient} from '../../UninitializedClient';
import {sortPluginsByName} from '../../utils/pluginUtils';
import {PluginNotification} from '../../reducers/notifications';
import {ActiveSheet} from '../../reducers/application';
import {State as Store} from '../../reducers';
import {
Sidebar,
colors,
Glyph,
styled,
SmallText,
Info,
HBox,
LoadingIndicator,
} from '../../ui';
import React, {
PureComponent,
Fragment,
memo,
useCallback,
useState,
useEffect,
useRef,
} from 'react';
import NotificationScreen from '../NotificationScreen';
import {
selectPlugin,
starPlugin as starPluginAction,
StaticView,
setStaticView,
getAvailableClients,
canBeDefaultDevice,
} from '../../reducers/connections';
import {setActiveSheet} from '../../reducers/application';
import {connect} from 'react-redux';
import SupportRequestDetails from '../../fb-stubs/SupportRequestDetails';
import MainSidebarUtilsSection from './MainSidebarUtilsSection';
import {
ListItem,
PluginName,
Plugins,
CategoryName,
PluginIcon,
PluginSidebarListItem,
NoDevices,
getColorByApp,
getFavoritePlugins,
isStaticViewActive,
} from './sidebarUtils';
import {useLocalStorage} from '../../utils/useLocalStorage';
import {PluginDefinition, ClientPluginMap, DevicePluginMap} from '../../plugin';
import GK from '../../fb-stubs/GK';
import ArchivedDevice from '../../devices/ArchivedDevice';
type FlipperPlugins = PluginDefinition[];
type PluginsByCategoryType = [string, FlipperPlugins][];
type SectionLevel = 1 | 2 | 3;
const ShowMoreButton = styled('div')<{collapsed: boolean}>(({collapsed}) => ({
border: `1px solid ${colors.macOSTitleBarIconBlur}`,
width: 16,
height: 16,
borderRadius: 16,
marginLeft: 'auto',
marginRight: 'auto',
lineHeight: '12px',
transform: collapsed ? 'rotate(0deg)' : 'rotate(-180deg)',
transition: `transform 0.3s ease`,
}));
const SidebarSectionButton = styled('button')<{
level: SectionLevel;
color: string;
collapsed: boolean;
}>(({level, color}) => ({
fontWeight: level === 3 ? 'normal' : 'bold',
borderRadius: 0,
border: 'none',
background: level === 1 ? colors.sectionHeaderBorder : 'transparent',
textAlign: level === 3 ? 'center' : 'left',
width: '100%',
fontSize: level === 3 ? 11 : 14,
color,
padding: `${level === 3 ? 0 : 8}px 10px 8px 9px`,
textTransform: 'capitalize',
fontVariantCaps: level === 2 ? 'all-small-caps' : 'normal',
}));
const SidebarSectionBody = styled('div')<{
level: SectionLevel;
collapsed: boolean;
}>(({collapsed, level}) => ({
userSelect: 'none',
flexShrink: 0,
overflow: 'hidden',
maxHeight: collapsed ? 0 : 2000, // might need increase if too many plugins...
transition: collapsed
? 'max-height 0.3s ease-out'
: 'max-height 0.5s ease-in',
borderBottom:
level === 2 ? `1px solid ${colors.sectionHeaderBorder}` : undefined,
}));
const SidebarSectionButtonGlyph = styled(Glyph)<{collapsed: boolean}>(
({collapsed}) => ({
transform: collapsed ? 'rotate(90deg)' : 'rotate(180deg)',
transition: `transform 0.3s ease`,
}),
);
const SidebarSection: React.FC<{
defaultCollapsed?: boolean;
title: string | React.ReactNode | ((collapsed: boolean) => React.ReactNode);
level: SectionLevel;
color?: string;
storageKey: string;
}> = ({children, title, level, color, defaultCollapsed, storageKey}) => {
const hasMounted = useRef(false);
const [collapsed, setCollapsed] = useLocalStorage(
storageKey,
!!defaultCollapsed,
);
color = color || colors.macOSTitleBarIconActive;
useEffect(() => {
// if default collapsed changed to false after mounting, propagate that
if (hasMounted.current && !defaultCollapsed && collapsed) {
setCollapsed(!collapsed);
}
hasMounted.current = true;
}, [defaultCollapsed]);
return (
<>
<SidebarSectionButton
onClick={() => setCollapsed((s) => !s)}
level={level}
color={color}
collapsed={collapsed}>
<HBox grow="left">
{typeof title === 'function' ? title(collapsed) : title}
{level < 3 && children && (
<SidebarSectionButtonGlyph
name="chevron-up"
size={12}
color={color}
collapsed={collapsed}
/>
)}
</HBox>
</SidebarSectionButton>
<SidebarSectionBody level={level} collapsed={collapsed}>
{children}
</SidebarSectionBody>
</>
);
};
type OwnProps = {};
type StateFromProps = {
numNotifications: number;
windowIsFocused: boolean;
devices: BaseDevice[];
selectedDevice: BaseDevice | null | undefined;
staticView: StaticView;
selectedPlugin: string | null | undefined;
selectedApp: string | null | undefined;
userStarredPlugins: Store['connections']['userStarredPlugins'];
clients: Array<Client>;
uninitializedClients: Array<{
client: UninitializedClient;
deviceId?: string;
errorMessage?: string;
}>;
devicePlugins: DevicePluginMap;
clientPlugins: ClientPluginMap;
};
type SelectPlugin = (payload: {
selectedPlugin: string | null;
selectedApp?: string | null;
deepLinkPayload: unknown;
selectedDevice: BaseDevice;
}) => void;
type DispatchFromProps = {
selectPlugin: SelectPlugin;
setActiveSheet: (activeSheet: ActiveSheet) => void;
setStaticView: (payload: StaticView) => void;
starPlugin: typeof starPluginAction;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
type State = {
showWatchDebugRoot: boolean;
showAllPlugins: boolean;
};
class MainSidebar2 extends PureComponent<Props, State> {
state: State = {
showWatchDebugRoot: GK.get('watch_team_flipper_clientless_access'),
showAllPlugins: false,
};
render() {
const {devices} = this.props;
return (
<Sidebar position="left" width={250} backgroundColor={colors.light02}>
<Plugins>
{devices.length ? (
devices.map((device) => this.renderDevice(device))
) : (
<NoDevices />
)}
{this.renderUnitializedClients()}
</Plugins>
<MainSidebarUtilsSection />
</Sidebar>
);
}
renderDevice(device: BaseDevice) {
const {
selectedPlugin,
selectPlugin,
clientPlugins,
starPlugin,
userStarredPlugins,
selectedApp,
selectedDevice,
} = this.props;
const clients = getAvailableClients(device, this.props.clients);
const devicePluginsItems = device.devicePlugins
.map((pluginName) => this.props.devicePlugins.get(pluginName)!)
.sort(sortPluginsByName)
.map((plugin) => (
<PluginSidebarListItem
key={plugin.id}
isActive={plugin.id === selectedPlugin && selectedDevice === device}
onClick={() =>
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: null,
deepLinkPayload: null,
selectedDevice: device,
})
}
plugin={plugin}
/>
));
const wrapDevicePlugins =
clients.length > 0 && device.devicePlugins.length > 1 && !device.source;
return (
<SidebarSection
title={device.displayTitle()}
key={device.serial}
storageKey={device.serial}
level={1}
defaultCollapsed={!canBeDefaultDevice(device)}>
{this.showArchivedDeviceDetails(device)}
{wrapDevicePlugins ? (
<SidebarSection
level={2}
title="Device Plugins"
storageKey={device.serial + ':device-plugins'}
defaultCollapsed={false}>
{devicePluginsItems}
</SidebarSection>
) : (
<div style={{marginTop: 6}}>{devicePluginsItems}</div>
)}
{clients.map((client) => (
<PluginList
device={device}
key={client.id}
client={client}
clientPlugins={clientPlugins}
starPlugin={starPlugin}
userStarredPlugins={userStarredPlugins}
selectedPlugin={selectedPlugin}
selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
))}
</SidebarSection>
);
}
renderUnitializedClients() {
const {uninitializedClients} = this.props;
return uninitializedClients.length > 0 ? (
<SidebarSection
title="Connecting..."
key="unitializedClients"
level={1}
storageKey="unitializedClients">
{uninitializedClients.map((entry) => (
<SidebarSection
color={getColorByApp(entry.client.appName)}
storageKey={'unitializedClients:' + JSON.stringify(entry.client)}
key={JSON.stringify(entry.client)}
title={
<HBox grow="left">
{entry.client.appName}
{entry.errorMessage ? (
<Glyph name={'mobile-cross'} size={16} />
) : (
<LoadingIndicator size={16} />
)}
</HBox>
}
level={2}></SidebarSection>
))}
</SidebarSection>
) : null;
}
showArchivedDeviceDetails(device: BaseDevice) {
if (!device.isArchived || !device.source) {
return null;
}
const {staticView, setStaticView} = this.props;
const supportRequestDetailsactive = isStaticViewActive(
staticView,
SupportRequestDetails,
);
return (
<>
<ListItem style={{marginTop: 8}}>
<Info type="warning" small>
{device.source ? 'Imported device' : 'Archived device'}
</Info>
</ListItem>
{(device as ArchivedDevice).supportRequestDetails && (
<ListItem
active={supportRequestDetailsactive}
onClick={() => setStaticView(SupportRequestDetails)}>
<PluginIcon
color={colors.light50}
name={'app-dailies'}
isActive={supportRequestDetailsactive}
/>
<PluginName isActive={supportRequestDetailsactive}>
Support Request Details
</PluginName>
</ListItem>
)}
</>
);
}
renderNotificationsEntry() {
if (GK.get('flipper_disable_notifications')) {
return null;
}
const active = isStaticViewActive(
this.props.staticView,
NotificationScreen,
);
return (
<ListItem
active={active}
onClick={() => this.props.setStaticView(NotificationScreen)}
style={{
borderTop: `1px solid ${colors.blackAlpha10}`,
}}>
<PluginIcon
color={colors.light50}
name={this.props.numNotifications > 0 ? 'bell' : 'bell-null'}
isActive={active}
/>
<PluginName count={this.props.numNotifications} isActive={active}>
Notifications
</PluginName>
</ListItem>
);
}
}
function groupPluginsByCategory(
plugins: FlipperPlugins,
): PluginsByCategoryType {
const sortedPlugins = plugins.slice().sort(sortPluginsByName);
const byCategory: {[cat: string]: FlipperPlugins} = {};
const res: PluginsByCategoryType = [];
sortedPlugins.forEach((plugin) => {
const category = plugin.category || '';
(byCategory[category] || (byCategory[category] = [])).push(plugin);
});
// Sort categories
Object.keys(byCategory)
.sort()
.forEach((category) => {
res.push([category, byCategory[category]]);
});
return res;
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
application: {windowIsFocused},
connections: {
devices,
selectedDevice,
selectedPlugin,
selectedApp,
userStarredPlugins,
clients,
uninitializedClients,
staticView,
},
notifications: {activeNotifications, blocklistedPlugins},
plugins: {devicePlugins, clientPlugins},
}) => ({
numNotifications: (() => {
const blocklist = new Set(blocklistedPlugins);
return activeNotifications.filter(
(n: PluginNotification) => !blocklist.has(n.pluginId),
).length;
})(),
windowIsFocused,
devices,
selectedDevice,
staticView,
selectedPlugin,
selectedApp,
userStarredPlugins,
clients,
uninitializedClients,
devicePlugins,
clientPlugins,
}),
{
selectPlugin,
setStaticView,
setActiveSheet,
starPlugin: starPluginAction,
},
)(MainSidebar2);
const PluginList = memo(function PluginList({
client,
device,
clientPlugins,
starPlugin,
userStarredPlugins,
selectedPlugin,
selectedApp,
selectPlugin,
}: {
client: Client;
device: BaseDevice;
clientPlugins: ClientPluginMap;
starPlugin: typeof starPluginAction;
userStarredPlugins: Store['connections']['userStarredPlugins'];
selectedPlugin?: null | string;
selectPlugin: SelectPlugin;
selectedApp?: null | string;
}) {
// client is a mutable structure, so we need the event emitter to detect the addition of plugins....
const [, setPluginsChanged] = useState(0);
useEffect(() => {
const listener = () => setPluginsChanged((v) => v + 1);
client.on('plugins-change', listener);
return () => {
client.off('plugins-change', listener);
};
}, [client]);
const onFavorite = useCallback(
(plugin: PluginDefinition) => {
starPlugin({
selectedApp: client.query.app,
plugin,
});
},
[client, starPlugin],
);
const allPlugins = Array.from(clientPlugins.values()).filter(
(p) => client.plugins.indexOf(p.id) > -1,
);
const favoritePlugins: FlipperPlugins = getFavoritePlugins(
device,
client,
allPlugins,
userStarredPlugins[client.query.app],
true,
);
const selectedNonFavoritePlugin =
selectedApp === client.id &&
client.plugins.includes(selectedPlugin!) &&
!favoritePlugins.find((plugin) => plugin.id === selectedPlugin);
const allPluginsStarred = favoritePlugins.length === allPlugins.length;
return (
<SidebarSection
level={2}
key={client.id}
title={client.query.app}
storageKey={`${device.serial}:${client.query.app}`}
color={getColorByApp(client.query.app)}>
{favoritePlugins.length === 0 ? (
<ListItem>
<SmallText center>No plugins enabled</SmallText>
</ListItem>
) : (
<PluginsByCategory
client={client}
device={device}
plugins={favoritePlugins}
starred={true}
onFavorite={onFavorite}
selectedPlugin={selectedPlugin}
selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
)}
{!allPluginsStarred && (
<SidebarSection
level={3}
color={colors.macOSTitleBarIconBlur}
storageKey={`${device.serial}:${client.query.app}:disabled-plugins`}
defaultCollapsed={
favoritePlugins.length > 0 && !selectedNonFavoritePlugin
}
title={(collapsed) => (
<ShowMoreButton collapsed={collapsed}>
<Glyph
color={colors.macOSTitleBarIconBlur}
size={8}
name="chevron-down"
/>
</ShowMoreButton>
)}>
<PluginsByCategory
client={client}
device={device}
plugins={getFavoritePlugins(
device,
client,
allPlugins,
userStarredPlugins[client.query.app],
false,
)}
starred={false}
onFavorite={onFavorite}
selectedPlugin={selectedPlugin}
selectedApp={selectedApp}
selectPlugin={selectPlugin}
/>
</SidebarSection>
)}
</SidebarSection>
);
});
const PluginsByCategory = memo(function PluginsByCategory({
client,
plugins,
starred,
onFavorite,
selectedPlugin,
selectedApp,
selectPlugin,
device,
}: {
client: Client;
device: BaseDevice;
plugins: FlipperPlugins;
starred: boolean;
selectedPlugin?: null | string;
selectedApp?: null | string;
onFavorite: (plugin: PluginDefinition) => void;
selectPlugin: SelectPlugin;
}) {
return (
<>
{groupPluginsByCategory(plugins).map(([category, plugins]) => (
<Fragment key={category}>
{category && (
<ListItem>
<CategoryName>{category}</CategoryName>
</ListItem>
)}
{plugins.map((plugin) => (
<PluginSidebarListItem
key={plugin.id}
isActive={
plugin.id === selectedPlugin && selectedApp === client.id
}
onClick={() =>
selectPlugin({
selectedPlugin: plugin.id,
selectedApp: client.id,
deepLinkPayload: null,
selectedDevice: device,
})
}
plugin={plugin}
app={client.query.app}
onFavorite={() => onFavorite(plugin)}
starred={device.isArchived ? undefined : starred}
/>
))}
</Fragment>
))}
</>
);
});

View File

@@ -1,200 +0,0 @@
/**
* 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 from 'react';
import {connect} from 'react-redux';
import config from '../../fb-stubs/config';
import {PluginNotification} from '../../reducers/notifications';
import {ActiveSheet, ACTIVE_SHEET_PLUGINS} from '../../reducers/application';
import {State as Store} from '../../reducers';
import NotificationScreen from '../NotificationScreen';
import {StaticView, setStaticView} from '../../reducers/connections';
import {setActiveSheet} from '../../reducers/application';
import UserAccount from '../UserAccount';
import SupportRequestFormV2 from '../../fb-stubs/SupportRequestFormV2';
import {
isStaticViewActive,
PluginIcon,
PluginName,
ListItem,
} from './sidebarUtils';
import {Group} from '../../reducers/supportForm';
import {getInstance} from '../../fb-stubs/Logger';
import {ConsoleLogs, errorCounterAtom} from '../ConsoleLogs';
import {useValue} from 'flipper-plugin';
import {colors} from '../../ui';
import GK from '../../fb-stubs/GK';
import WatchTools from '../../fb-stubs/WatchTools';
type OwnProps = {};
type StateFromProps = {
staticView: StaticView;
selectedGroup: Group;
};
type DispatchFromProps = {
setActiveSheet: (activeSheet: ActiveSheet) => void;
setStaticView: (payload: StaticView) => void;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
function MainSidebarUtilsSection({
staticView,
selectedGroup,
setActiveSheet,
setStaticView,
}: Props) {
const showWatchDebugRoot = GK.get('watch_team_flipper_clientless_access');
return (
<div style={{flexShrink: 0, borderTop: `1px solid ${colors.blackAlpha10}`}}>
{showWatchDebugRoot &&
(function () {
const active = isStaticViewActive(staticView, WatchTools);
return (
<ListItem
active={active}
style={{
borderTop: `1px solid ${colors.blackAlpha10}`,
}}
onClick={() => setStaticView(WatchTools)}>
<PluginIcon
color={colors.light50}
name={'watch-tv'}
isActive={active}
/>
<PluginName isActive={active}>Watch</PluginName>
</ListItem>
);
})()}
<RenderNotificationsEntry />
{(function () {
const active = isStaticViewActive(staticView, SupportRequestFormV2);
return (
<ListItem
active={active}
onClick={() => {
getInstance().track('usage', 'support-form-source', {
source: 'sidebar',
group: selectedGroup.name,
});
setStaticView(SupportRequestFormV2);
}}>
<PluginIcon
color={colors.light50}
name={'app-dailies'}
isActive={active}
/>
<PluginName isActive={active}>Support Requests</PluginName>
</ListItem>
);
})()}
<ListItem onClick={() => setActiveSheet(ACTIVE_SHEET_PLUGINS)}>
<PluginIcon
name="question-circle"
color={colors.light50}
isActive={false}
/>
Manage Plugins
</ListItem>
<DebugLogsEntry staticView={staticView} setStaticView={setStaticView} />
{config.showLogin && <UserAccount />}
</div>
);
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({connections: {staticView}, supportForm: {supportFormV2}}) => ({
staticView,
selectedGroup: supportFormV2.selectedGroup,
}),
{
setStaticView,
setActiveSheet,
},
)(MainSidebarUtilsSection);
type RenderNotificationsEntryProps = {
numNotifications: number;
staticView: StaticView;
};
type RenderNotificationsEntryDispatchFromProps = {
setStaticView: (payload: StaticView) => void;
};
type RenderEntryProps = RenderNotificationsEntryProps &
RenderNotificationsEntryDispatchFromProps;
const RenderNotificationsEntry = connect<
RenderNotificationsEntryProps,
RenderNotificationsEntryDispatchFromProps,
{},
Store
>(
({
connections: {staticView},
notifications: {activeNotifications, blocklistedPlugins},
}) => ({
numNotifications: (() => {
const blocklist = new Set(blocklistedPlugins);
return activeNotifications.filter(
(n: PluginNotification) => !blocklist.has(n.pluginId),
).length;
})(),
staticView,
}),
{
setStaticView,
},
)(({staticView, setStaticView, numNotifications}: RenderEntryProps) => {
if (GK.get('flipper_disable_notifications')) {
return null;
}
const active = isStaticViewActive(staticView, NotificationScreen);
return (
<ListItem active={active} onClick={() => setStaticView(NotificationScreen)}>
<PluginIcon
color={colors.light50}
name={numNotifications > 0 ? 'bell' : 'bell-null'}
isActive={active}
/>
<PluginName count={numNotifications} isActive={active}>
Notifications
</PluginName>
</ListItem>
);
});
function DebugLogsEntry({
staticView,
setStaticView,
}: {
staticView: StaticView;
setStaticView: (payload: StaticView) => void;
}) {
const active = isStaticViewActive(staticView, ConsoleLogs);
const errorCount = useValue(errorCounterAtom);
return (
<ListItem onClick={() => setStaticView(ConsoleLogs)} active={active}>
<PluginIcon
name="caution-octagon"
color={colors.light50}
isActive={active}
/>
<PluginName count={errorCount} isActive={active}>
Debug Logs
</PluginName>
</ListItem>
);
}

View File

@@ -1,260 +0,0 @@
/**
* 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, {useRef, useEffect} from 'react';
import {
FlexBox,
colors,
Text,
Glyph,
styled,
FlexColumn,
ToggleButton,
brandColors,
Spacer,
Heading,
} from '../../ui';
import {Property} from 'csstype';
import {getPluginTitle} from '../../utils/pluginUtils';
import {PluginDefinition} from '../../plugin';
import {StaticView} from '../../reducers/connections';
import BaseDevice from '../../devices/BaseDevice';
import Client from '../../Client';
export type FlipperPlugins = PluginDefinition[];
export type PluginsByCategory = [string, FlipperPlugins][];
export const ListItem = styled.div<{active?: boolean; disabled?: boolean}>(
({active, disabled}) => ({
paddingLeft: 10,
display: 'flex',
alignItems: 'center',
marginBottom: 6,
flexShrink: 0,
backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none',
color: disabled
? 'rgba(0, 0, 0, 0.5)'
: active
? colors.white
: colors.macOSSidebarSectionItem,
lineHeight: '25px',
padding: '0 10px',
'&[disabled]': {
color: 'rgba(0, 0, 0, 0.5)',
},
}),
);
export function PluginIcon({
isActive,
backgroundColor,
name,
color,
}: {
isActive: boolean;
backgroundColor?: string;
name: string;
color: string;
}) {
return (
<PluginShape backgroundColor={backgroundColor}>
<Glyph size={12} name={name} color={isActive ? colors.white : color} />
</PluginShape>
);
}
const PluginShape = styled(FlexBox)<{
backgroundColor?: Property.BackgroundColor;
}>(({backgroundColor}) => ({
marginRight: 8,
backgroundColor,
borderRadius: 3,
flexShrink: 0,
width: 18,
height: 18,
justifyContent: 'center',
alignItems: 'center',
top: '-1px',
}));
export const PluginName = styled(Text)<{isActive?: boolean; count?: number}>(
(props) => ({
cursor: 'default',
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,
},
}),
);
export function isStaticViewActive(
current: StaticView,
selected: StaticView,
): boolean {
return Boolean(current && selected && current === selected);
}
export const CategoryName = styled(PluginName)({
color: colors.macOSSidebarSectionTitle,
textTransform: 'uppercase',
fontSize: '0.9em',
});
export const Plugins = styled(FlexColumn)({
flexGrow: 1,
overflow: 'auto',
});
export const PluginSidebarListItem: React.FC<{
onClick: () => void;
isActive: boolean;
plugin: PluginDefinition;
app?: string | null | undefined;
helpRef?: any;
provided?: any;
onFavorite?: () => void;
starred?: boolean; // undefined means: not starrable
}> = function (props) {
const {isActive, plugin, onFavorite, starred} = props;
const iconColor = getColorByApp(props.app);
const domRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const node = domRef.current;
if (isActive && node) {
const rect = node.getBoundingClientRect();
if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) {
node.scrollIntoView();
}
}
}, [isActive]);
return (
<ListItem
ref={domRef}
active={isActive}
onClick={props.onClick}
disabled={starred === false}>
<PluginIcon
isActive={isActive}
name={plugin.icon || 'apps'}
backgroundColor={starred === false ? colors.light20 : iconColor}
color={colors.white}
/>
<PluginName
title={`${getPluginTitle(plugin)} ${plugin.version} ${
plugin.details?.description ? '- ' + plugin.details?.description : ''
}`}>
{getPluginTitle(plugin)}
</PluginName>
{starred !== undefined && (!starred || isActive) && (
<ToggleButton
onClick={onFavorite}
toggled={starred}
tooltip="Click to enable / disable this plugin"
/>
)}
</ListItem>
);
};
export function getColorByApp(app?: string | null): string {
let iconColor: string | undefined = (brandColors as any)[app!];
if (!iconColor) {
if (!app) {
// Device plugin
iconColor = colors.macOSTitleBarIconBlur;
} else {
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 iconColor;
}
export const NoDevices = () => (
<ListItem
style={{
textAlign: 'center',
marginTop: 50,
flexDirection: 'column',
}}>
<Glyph name="mobile" size={32} color={colors.red}></Glyph>
<Spacer style={{height: 20}} />
<Heading>Select a device to get started</Heading>
</ListItem>
);
export const NoClients = () => (
<ListItem style={{marginTop: 8}}>
<Glyph
name="mobile-engagement"
size={16}
color={colors.red}
style={{marginRight: 10}}
/>
No clients connected
</ListItem>
);
export function getFavoritePlugins(
device: BaseDevice,
client: Client,
allPlugins: FlipperPlugins,
starredPlugins: undefined | string[],
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
): FlipperPlugins {
if (device.isArchived) {
if (!returnFavoredPlugins) {
return [];
}
// for archived plugins, all stored plugins are enabled
return allPlugins.filter(
(plugin) => client.plugins.indexOf(plugin.id) !== -1,
);
}
if (!starredPlugins || !starredPlugins.length) {
return returnFavoredPlugins ? [] : allPlugins;
}
return allPlugins.filter((plugin) => {
const idx = starredPlugins.indexOf(plugin.id);
return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins;
});
}

View File

@@ -6,7 +6,7 @@ exports[`load PluginInstaller list 1`] = `
class="css-9dawc5-View-FlexBox-FlexColumn"
>
<div
class="css-cjspgc-View-FlexBox-FlexRow-ToolbarContainer e13mj6h82"
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e13mj6h81"
>
<div
class="css-bl14sl-View-FlexBox-SearchBox e271nro4"
@@ -139,8 +139,9 @@ exports[`load PluginInstaller list 1`] = `
<div
class="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<span
class="css-ad6n9d-StyledLink e1mzoj7l0"
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-hello"
>
<div
class="css-1tfjvvq-ColoredIconCustom e528dze0"
@@ -148,7 +149,7 @@ exports[`load PluginInstaller list 1`] = `
size="16"
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</span>
</a>
</div>
</div>
<div
@@ -156,12 +157,14 @@ exports[`load PluginInstaller list 1`] = `
title=""
width="15%"
>
<div
class="css-iby3qx-StyledButton enfqd41"
type="primary"
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Install
</div>
</span>
</button>
</div>
</div>
<div
@@ -206,8 +209,9 @@ exports[`load PluginInstaller list 1`] = `
<div
class="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<span
class="css-ad6n9d-StyledLink e1mzoj7l0"
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-world"
>
<div
class="css-1tfjvvq-ColoredIconCustom e528dze0"
@@ -215,7 +219,7 @@ exports[`load PluginInstaller list 1`] = `
size="16"
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</span>
</a>
</div>
</div>
<div
@@ -223,19 +227,21 @@ exports[`load PluginInstaller list 1`] = `
title=""
width="15%"
>
<div
class="css-iby3qx-StyledButton enfqd41"
type="primary"
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Install
</div>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
class="css-cjspgc-View-FlexBox-FlexRow-ToolbarContainer e13mj6h82"
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e13mj6h81"
>
<div
class="css-1spj5hr-View-FlexBox-FlexRow-Container ersmi542"
@@ -278,14 +284,15 @@ exports[`load PluginInstaller list 1`] = `
<div
class="css-wospjg-View-FlexBox-FlexRow epz0qe20"
>
<div
class="css-1rxpsn8-StyledButton enfqd41"
<button
class="ant-btn ant-btn-primary"
disabled=""
title="Cannot install plugin package by the specified path"
type="primary"
type="button"
>
<span>
Install
</div>
</span>
</button>
<div
class="css-170i4ha-View-FlexBox-FlexRow-ErrorGlyphContainer evd5j490"
/>
@@ -301,7 +308,7 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
class="css-9dawc5-View-FlexBox-FlexColumn"
>
<div
class="css-cjspgc-View-FlexBox-FlexRow-ToolbarContainer e13mj6h82"
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e13mj6h81"
>
<div
class="css-bl14sl-View-FlexBox-SearchBox e271nro4"
@@ -434,8 +441,9 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
<div
class="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<span
class="css-ad6n9d-StyledLink e1mzoj7l0"
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-hello"
>
<div
class="css-1tfjvvq-ColoredIconCustom e528dze0"
@@ -443,7 +451,7 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
size="16"
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</span>
</a>
</div>
</div>
<div
@@ -451,11 +459,14 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
title=""
width="15%"
>
<div
class="css-iby3qx-StyledButton enfqd41"
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Remove
</div>
</span>
</button>
</div>
</div>
<div
@@ -500,8 +511,9 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
<div
class="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<span
class="css-ad6n9d-StyledLink e1mzoj7l0"
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-world"
>
<div
class="css-1tfjvvq-ColoredIconCustom e528dze0"
@@ -509,7 +521,7 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
size="16"
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</span>
</a>
</div>
</div>
<div
@@ -517,19 +529,21 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
title=""
width="15%"
>
<div
class="css-iby3qx-StyledButton enfqd41"
type="primary"
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Install
</div>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
class="css-cjspgc-View-FlexBox-FlexRow-ToolbarContainer e13mj6h82"
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e13mj6h81"
>
<div
class="css-1spj5hr-View-FlexBox-FlexRow-Container ersmi542"
@@ -572,14 +586,15 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
<div
class="css-wospjg-View-FlexBox-FlexRow epz0qe20"
>
<div
class="css-1rxpsn8-StyledButton enfqd41"
<button
class="ant-btn ant-btn-primary"
disabled=""
title="Cannot install plugin package by the specified path"
type="primary"
type="button"
>
<span>
Install
</div>
</span>
</button>
<div
class="css-170i4ha-View-FlexBox-FlexRow-ErrorGlyphContainer evd5j490"
/>

View File

@@ -14,7 +14,7 @@ import {
InstalledPluginDetails,
installPluginFromFile,
} from 'flipper-plugin-lib';
import {Actions, State, Store} from '../reducers/index';
import {State, Store} from '../reducers/index';
import {
PluginDownloadStatus,
pluginDownloadStarted,
@@ -26,17 +26,9 @@ import fs from 'fs-extra';
import path from 'path';
import tmp from 'tmp';
import {promisify} from 'util';
import {requirePlugin} from './plugins';
import {registerPluginUpdate, selectPlugin} from '../reducers/connections';
import {Button} from 'antd';
import React from 'react';
import {reportPlatformFailures, reportUsage} from '../utils/metrics';
import {addNotification, removeNotification} from '../reducers/notifications';
import reloadFlipper from '../utils/reloadFlipper';
import {activatePlugin, pluginInstalled} from '../reducers/pluginManager';
import {Dispatch} from 'redux';
import {showErrorNotification} from '../utils/notifications';
import isSandyEnabled from '../utils/isSandyEnabled';
// Adapter which forces node.js implementation for axios instead of browser implementation
// used by default in Electron. Node.js implementation is better, because it
@@ -144,8 +136,6 @@ async function handlePluginDownload(
notifyIfFailed: startedByUser,
}),
);
} else if (!isSandyEnabled()) {
notifyAboutUpdatedPluginNonSandy(installedPlugin, store.dispatch);
}
console.log(
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
@@ -178,59 +168,3 @@ function pluginIsDisabledForAllConnectedClients(
)
);
}
function notifyAboutUpdatedPluginNonSandy(
plugin: InstalledPluginDetails,
dispatch: Dispatch<Actions>,
) {
const {name, version, title, id} = plugin;
const reloadPluginAndRemoveNotification = () => {
reportUsage('plugin-auto-update:notification:reloadClicked', undefined, id);
dispatch(
registerPluginUpdate({
plugin: requirePlugin(plugin),
enablePlugin: false,
}),
);
dispatch(
removeNotification({
pluginId: 'plugin-auto-update',
client: null,
notificationId: `auto-update.${name}.${version}`,
}),
);
dispatch(
selectPlugin({
selectedPlugin: id,
deepLinkPayload: null,
}),
);
};
const reloadAll = () => {
reportUsage('plugin-auto-update:notification:reloadAllClicked');
reloadFlipper();
};
dispatch(
addNotification({
pluginId: 'plugin-auto-update',
client: null,
notification: {
id: `auto-update.${name}.${version}`,
title: `${title} ${version} is ready to install`,
message: (
<div>
{title} {version} has been downloaded. Reload is required to apply
the update.{' '}
<Button onClick={reloadPluginAndRemoveNotification}>
Reload Plugin
</Button>
<Button onClick={reloadAll}>Reload Flipper</Button>
</div>
),
severity: 'warning',
timestamp: Date.now(),
category: `Plugin Auto Update`,
},
}),
);
}

View File

@@ -13,7 +13,7 @@ import ReactDOM from 'react-dom';
import ContextMenuProvider from './ui/components/ContextMenuProvider';
import GK from './fb-stubs/GK';
import {init as initLogger} from './fb-stubs/Logger';
import App from './chrome/AppWrapper';
import {SandyApp} from './sandy-chrome/SandyApp';
import setupPrefetcher from './fb-stubs/Prefetcher';
import {persistStore} from 'redux-persist';
import {Store} from './reducers/index';
@@ -42,7 +42,6 @@ import {
_LoggerContext,
} from 'flipper-plugin';
import isProduction from './utils/isProduction';
import isSandyEnabled from './utils/isSandyEnabled';
if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') {
// By default Node.JS has its internal certificate storage and doesn't use
@@ -67,7 +66,7 @@ const AppFrame = ({logger}: {logger: Logger}) => (
<PopoverProvider>
<ContextMenuProvider>
<_NuxManagerContext.Provider value={_createNuxManager()}>
<App logger={logger} />
<SandyApp />
</_NuxManagerContext.Provider>
</ContextMenuProvider>
</PopoverProvider>
@@ -124,18 +123,14 @@ function init() {
store,
{name: 'loadTheme', fireImmediately: true, throttleMs: 500},
(state) => ({
sandy: isSandyEnabled(),
dark: state.settingsState.darkMode,
}),
(theme) => {
(document.getElementById(
'flipper-theme-import',
) as HTMLLinkElement).href = `themes/${
theme.sandy && theme.dark ? 'dark' : 'light'
theme.dark ? 'dark' : 'light'
}.css`;
document
.getElementById('root')
?.classList.toggle('flipperlegacy_design', !theme.sandy);
},
);
}

View File

@@ -43,7 +43,6 @@ export type Settings = {
openDevMenu: string;
};
};
disableSandy: boolean;
darkMode: boolean;
showWelcomeAtStartup: boolean;
};
@@ -78,7 +77,6 @@ const initialState: Settings = {
openDevMenu: 'Alt+Shift+D',
},
},
disableSandy: false,
darkMode: false,
showWelcomeAtStartup: true,
};

View File

@@ -48,9 +48,8 @@ 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 {setStaticView, StaticView} from '../reducers/connections';
import {getInstance} from '../fb-stubs/Logger';
import {isStaticViewActive} from '../chrome/mainsidebar/sidebarUtils';
import {getUser} from '../fb-stubs/user';
import {SandyRatingButton} from '../chrome/RatingButton';
import {filterNotifications} from './notification/notificationUtils';
@@ -310,7 +309,7 @@ function ShowSettingsButton() {
selected={showSettings}
/>
{showSettings && (
<SettingsSheet platform={process.platform} onHide={onClose} useSandy />
<SettingsSheet platform={process.platform} onHide={onClose} />
)}
</>
);
@@ -417,7 +416,14 @@ function LoginButton() {
title="Log In"
onClick={() => setShowLogin(true)}
/>
{showLogin && <SignInSheet onHide={onClose} useSandy />}
{showLogin && <SignInSheet onHide={onClose} />}
</>
);
}
function isStaticViewActive(
current: StaticView,
selected: StaticView,
): boolean {
return Boolean(current && selected && current === selected);
}

View File

@@ -12,11 +12,11 @@ import {TrackingScope, useLogger} from 'flipper-plugin';
import {styled} from '../ui';
import {Layout, Sidebar} from '../ui';
import {theme} from 'flipper-plugin';
import {ipcRenderer} from 'electron';
import {Logger} from '../fb-interfaces/Logger';
import {LeftRail} from './LeftRail';
import {registerStartupTime} from '../chrome/LegacyApp';
import {useStore, useDispatch} from '../utils/useStore';
import {SandyContext} from './SandyContext';
import {ConsoleLogs} from '../chrome/ConsoleLogs';
import {setStaticView} from '../reducers/connections';
import {
@@ -30,10 +30,15 @@ import {ContentContainer} from './ContentContainer';
import {Notification} from './notification/Notification';
import {SheetRenderer} from '../chrome/SheetRenderer';
import {hasNewChangesToShow} from '../chrome/ChangelogSheet';
import {SandyWelcomScreen} from './SandyWelcomeScreen';
import {SandyWelcomeScreen} from './SandyWelcomeScreen';
import {getVersionString} from '../utils/versionString';
import config from '../fb-stubs/config';
import {WelcomeScreenStaticView} from './WelcomeScreen';
import QPL, {QuickLogActionType, FLIPPER_QPL_EVENTS} from '../fb-stubs/QPL';
import fbConfig from '../fb-stubs/config';
import {isFBEmployee} from '../utils/fbEmployee';
import {notification} from 'antd';
import isProduction from '../utils/isProduction';
export type ToplevelNavItem =
| 'appinspect'
@@ -99,6 +104,27 @@ export function SandyApp() {
// eslint-disable-next-line
}, []);
useEffect(() => {
if (fbConfig.warnFBEmployees && isProduction()) {
isFBEmployee().then((isEmployee) => {
if (isEmployee) {
notification.warning({
placement: 'bottomLeft',
message: 'Please use Flipper@FB',
description: (
<>
You are using the open-source version of Flipper. Install the
internal build from Managed Software Center to get access to
more plugins.
</>
),
duration: null,
});
}
});
}
}, []);
const leftMenuContent = !leftSidebarVisible ? null : toplevelSelection ===
'appinspect' ? (
<AppInspect />
@@ -107,11 +133,10 @@ export function SandyApp() {
) : null;
return (
<SandyContext.Provider value={true}>
<Layout.Top>
<>
<SheetRenderer logger={logger} />
<SandyWelcomScreen />
<SandyWelcomeScreen />
</>
<Layout.Left>
<Layout.Horizontal>
@@ -153,7 +178,6 @@ export function SandyApp() {
</MainContainer>
</Layout.Left>
</Layout.Top>
</SandyContext.Provider>
);
}
@@ -183,3 +207,23 @@ const MainContainer = styled(Layout.Container)({
background: theme.backgroundWash,
padding: `${theme.space.large}px ${theme.space.large}px ${theme.space.large}px 0`,
});
function registerStartupTime(logger: Logger) {
// track time since launch
const [s, ns] = process.hrtime();
const launchEndTime = s * 1e3 + ns / 1e6;
ipcRenderer.on('getLaunchTime', (_: any, launchStartTime: number) => {
logger.track('performance', 'launchTime', launchEndTime - launchStartTime);
QPL.markerStart(FLIPPER_QPL_EVENTS.STARTUP, 0, launchStartTime);
QPL.markerEnd(
FLIPPER_QPL_EVENTS.STARTUP,
QuickLogActionType.SUCCESS,
0,
launchEndTime,
);
});
ipcRenderer.send('getLaunchTime');
ipcRenderer.send('componentDidMount');
}

View File

@@ -1,16 +0,0 @@
/**
* 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 {createContext, useContext} from 'react';
export const SandyContext = createContext(false);
export function useIsSandy(): boolean {
return useContext(SandyContext);
}

View File

@@ -16,7 +16,7 @@ import {useLocalStorage} from '../utils/useLocalStorage';
const {Title, Text, Link} = Typography;
export function SandyWelcomScreen() {
export function SandyWelcomeScreen() {
const [dismissed, setDismissed] = useState(false);
const [showWelcomeScreen, setShowWelcomeScreen] = useLocalStorage(
'flipper-sandy-show-welcome-screen',
@@ -63,10 +63,6 @@ export function SandyWelcomScreen() {
to find new or improved features.
</Text>
<Text>
It is possible to enable the experimental dark mode, or switch back to
the old design, by using the{' '}
<Button icon={<SettingOutlined />} disabled size="small" /> settings
button.{' '}
{constants.IS_PUBLIC_BUILD ? (
<>
Feel free let us know your thoughts about the new experience on{' '}

View File

@@ -73,8 +73,8 @@ export function AppInspect() {
)}
{!isArchived && activeDevice && (
<Toolbar gap>
<MetroButton useSandy />
<ScreenCaptureButtons useSandy />
<MetroButton />
<ScreenCaptureButtons />
</Toolbar>
)}
</Layout.Container>

View File

@@ -27,10 +27,9 @@ import {
selectDevice,
} from '../../reducers/connections';
import BaseDevice, {OS} from '../../devices/BaseDevice';
import {getColorByApp} from '../../chrome/mainsidebar/sidebarUtils';
import Client from '../../Client';
import {State} from '../../reducers';
import {brandIcons} from '../../ui/components/colors';
import {brandColors, brandIcons, colors} from '../../ui/components/colors';
import {showEmulatorLauncher} from './LaunchEmulator';
const {Text, Link, Title} = Typography;
@@ -251,3 +250,29 @@ function NoDevices() {
/>
);
}
function getColorByApp(app?: string | null): string {
let iconColor: string | undefined = (brandColors as any)[app!];
if (!iconColor) {
if (!app) {
// Device plugin
iconColor = colors.macOSTitleBarIconBlur;
} else {
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 iconColor;
}

View File

@@ -18,7 +18,7 @@ import {
useTrackedCallback,
useValue,
} from 'flipper-plugin';
import {navPluginStateSelector} from '../../chrome/LocationsButton';
import {State} from '../../reducers';
// eslint-disable-next-line flipper/no-relative-imports-across-packages
import type {NavigationPlugin} from '../../../../plugins/navigation/index';
@@ -136,3 +136,15 @@ const StyledAutoComplete = styled(AutoComplete)({
flex: 1,
},
});
const NAVIGATION_PLUGIN_ID = 'Navigation';
function navPluginStateSelector(state: State) {
const {selectedApp, clients} = state.connections;
if (!selectedApp) return undefined;
const client = clients.find((client) => client.id === selectedApp);
if (!client) return undefined;
return client.sandyPluginStates.get(NAVIGATION_PLUGIN_ID)?.instanceApi as
| undefined
| NavigationPlugin;
}

View File

@@ -21,12 +21,15 @@ import {Glyph, Layout, styled} from '../../ui';
import {theme, NUX, Tracked} from 'flipper-plugin';
import {useDispatch, useStore} from '../../utils/useStore';
import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
import {ClientPluginDefinition, DevicePluginDefinition} from '../../plugin';
import {
ClientPluginDefinition,
DevicePluginDefinition,
PluginDefinition,
} from '../../plugin';
import {selectPlugin, starPlugin} from '../../reducers/connections';
import Client from '../../Client';
import {State} from '../../reducers';
import BaseDevice from '../../devices/BaseDevice';
import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
import {PluginDetails, DownloadablePluginDetails} from 'flipper-plugin-lib';
import {useMemoize} from '../../utils/useMemoize';
import MetroDevice from '../../devices/MetroDevice';
@@ -649,3 +652,28 @@ function iconStyle(disabled: boolean) {
height: 24,
};
}
function getFavoritePlugins(
device: BaseDevice,
client: Client,
allPlugins: PluginDefinition[],
starredPlugins: undefined | string[],
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
): PluginDefinition[] {
if (device.isArchived) {
if (!returnFavoredPlugins) {
return [];
}
// for archived plugins, all stored plugins are enabled
return allPlugins.filter(
(plugin) => client.plugins.indexOf(plugin.id) !== -1,
);
}
if (!starredPlugins || !starredPlugins.length) {
return returnFavoredPlugins ? [] : allPlugins;
}
return allPlugins.filter((plugin) => {
const idx = starredPlugins.indexOf(plugin.id);
return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins;
});
}

View File

@@ -7,21 +7,17 @@
* @format
*/
import React, {useContext, useState, useRef, useCallback, useMemo} from 'react';
import React, {useState, useCallback, useMemo} from 'react';
import electron, {MenuItemConstructorOptions} from 'electron';
import styled from '@emotion/styled';
import {findDOMNode} from 'react-dom';
import {keyframes} from '@emotion/css';
import {Button as AntdButton, Dropdown, Menu} from 'antd';
import {colors} from './colors';
import Glyph, {IconSize} from './Glyph';
import {ButtonGroupContext} from './ButtonGroup';
import {useStore} from '../../utils/useStore';
import {useIsSandy} from '../../sandy-chrome/SandyContext';
import type {ButtonProps} from 'antd/lib/button';
import {DownOutlined, CheckOutlined} from '@ant-design/icons';
import {theme, Tracked} from 'flipper-plugin';
import {theme} from 'flipper-plugin';
type ButtonType = 'primary' | 'success' | 'warning' | 'danger';
@@ -266,120 +262,7 @@ type Props = {
* A simple button, used in many parts of the application.
*/
export default function Button(props: Props) {
const isSandy = useIsSandy();
return isSandy ? <SandyButton {...props} /> : <ClassicButton {...props} />;
}
function ClassicButton(props: Props) {
const windowIsFocused = useStore(
(state) => state.application.windowIsFocused,
);
const inButtonGroup = useContext(ButtonGroupContext);
const [active, setActive] = useState(false);
const [wasClosed, setWasClosed] = useState(false);
const _ref = useRef<any>();
const onMouseDown = useCallback(
(e: React.MouseEvent) => {
setActive(true);
setWasClosed(false);
props.onMouseDown?.(e);
},
[props.onMouseDown],
);
const onMouseUp = useCallback(() => {
if (props.disabled === true) {
return;
}
if (props.dropdown && !wasClosed) {
const menu = electron.remote.Menu.buildFromTemplate(props.dropdown);
const position: {
x?: number;
y?: number;
} = {};
const {current} = _ref;
if (current) {
const node = findDOMNode(current);
if (node instanceof Element) {
const {left, bottom} = node.getBoundingClientRect();
position.x = Math.floor(left);
position.y = Math.floor(bottom) + 6;
}
}
menu.popup({
window: electron.remote.getCurrentWindow(),
// @ts-ignore: async is private API in electron
async: true,
...position,
callback: () => {
setWasClosed(true);
},
});
}
setActive(false);
setWasClosed(false);
}, [props.disabled, props.dropdown, wasClosed]);
const onClick = useCallback(
(e: React.MouseEvent) => {
if (props.disabled === true) {
return;
}
props.onClick?.(e);
if (props.href != null) {
electron.shell.openExternal(props.href);
}
},
[props.disabled, props.onClick, props.href],
);
const {icon, children, selected, iconSize, iconVariant, ...restProps} = props;
let color = colors.macOSTitleBarIcon;
if (props.disabled === true) {
color = colors.macOSTitleBarIconBlur;
} else if (windowIsFocused && selected === true) {
color = colors.macOSTitleBarIconSelected;
} else if (!windowIsFocused && (selected == null || selected === false)) {
color = colors.macOSTitleBarIconBlur;
} else if (!windowIsFocused && selected === true) {
color = colors.macOSTitleBarIconSelectedBlur;
} else if (selected == null && active) {
color = colors.macOSTitleBarIconActive;
} else if (props.type === 'danger') {
color = colors.red;
}
let iconComponent;
if (icon != null) {
iconComponent = (
<Icon
name={icon}
size={iconSize || (props.compact === true ? 12 : 16)}
color={color}
variant={iconVariant || 'filled'}
hasText={Boolean(children)}
/>
);
}
return (
<Tracked>
<StyledButton
{...restProps}
ref={_ref as any}
windowIsFocused={windowIsFocused}
onClick={onClick}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
inButtonGroup={inButtonGroup}>
{iconComponent}
{children}
</StyledButton>
</Tracked>
);
return <SandyButton {...props} />;
}
/**

View File

@@ -9,7 +9,6 @@
import styled from '@emotion/styled';
import React, {createContext} from 'react';
import {useIsSandy} from '../../sandy-chrome/SandyContext';
import {Space} from 'antd';
const ButtonGroupContainer = styled.div({
@@ -35,14 +34,9 @@ export const ButtonGroupContext = createContext(false);
* ```
*/
export default function ButtonGroup({children}: {children: React.ReactNode}) {
const isSandy = useIsSandy(); // according to Ant design guides buttons should only be grouped if they are radios
return isSandy ? (
return (
<ButtonGroupContext.Provider value={true}>
<Space>{children}</Space>
</ButtonGroupContext.Provider>
) : (
<ButtonGroupContext.Provider value={true}>
<ButtonGroupContainer>{children}</ButtonGroupContainer>
</ButtonGroupContext.Provider>
);
}

View File

@@ -7,23 +7,11 @@
* @format
*/
import styled from '@emotion/styled';
import {colors} from './colors';
import {useCallback} from 'react';
import {shell} from 'electron';
import React from 'react';
import {useIsSandy} from '../../sandy-chrome/SandyContext';
import {Typography} from 'antd';
const StyledLink = styled.span({
color: colors.highlight,
'&:hover': {
cursor: 'pointer',
textDecoration: 'underline',
},
});
StyledLink.displayName = 'Link:StyledLink';
const AntOriginalLink = Typography.Link;
export default function Link(props: {
@@ -32,7 +20,6 @@ export default function Link(props: {
style?: React.CSSProperties;
onClick?: ((event: React.MouseEvent<any>) => void) | undefined;
}) {
const isSandy = useIsSandy();
const onClick = useCallback(
(e: React.MouseEvent<any>) => {
shell.openExternal(props.href);
@@ -42,13 +29,7 @@ export default function Link(props: {
[props.href],
);
return isSandy ? (
<AntOriginalLink {...props} onClick={props.onClick ?? onClick} />
) : (
<StyledLink onClick={props.onClick ?? onClick} style={props.style}>
{props.children || props.href}
</StyledLink>
);
return <AntOriginalLink {...props} onClick={props.onClick ?? onClick} />;
}
// XXX. For consistent usage, we monkey patch AntDesign's Link component,

View File

@@ -8,39 +8,10 @@
*/
import React from 'react';
import {colors} from './colors';
import FlexRow from './FlexRow';
import FlexBox from './FlexBox';
import styled from '@emotion/styled';
import {useIsSandy} from '../../sandy-chrome/SandyContext';
import {theme, Layout} from 'flipper-plugin';
/**
* A toolbar.
*/
const ToolbarContainer = styled(FlexRow)<{
position?: 'bottom' | 'top';
compact?: boolean;
}>((props) => ({
userSelect: 'none',
backgroundColor: colors.light02,
borderBottom:
props.position === 'bottom'
? 'none'
: `1px solid ${colors.sectionHeaderBorder}`,
borderTop:
props.position === 'bottom'
? `1px solid ${colors.sectionHeaderBorder}`
: 'none',
flexShrink: 0,
height: props.compact ? 28 : 42,
lineHeight: '32px',
alignItems: 'center',
padding: 6,
width: '100%',
}));
ToolbarContainer.displayName = 'ToolbarContainer';
const SandyToolbarContainer = styled(Layout.Horizontal)({
flexWrap: 'wrap',
padding: theme.space.small,
@@ -55,21 +26,15 @@ Spacer.displayName = 'Spacer';
export default function Toolbar({
children,
style,
...rest
}: {
children?: React.ReactNode;
position?: 'bottom' | 'top';
compact?: boolean;
style?: React.CSSProperties;
}) {
const isSandy = useIsSandy();
return isSandy ? (
return (
<SandyToolbarContainer style={style} gap={theme.space.small} center>
{children}
</SandyToolbarContainer>
) : (
<ToolbarContainer style={style} {...rest}>
{children}
</ToolbarContainer>
);
}

View File

@@ -1,14 +0,0 @@
/**
* 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 {store} from '../store';
export default function isSandyEnabled() {
return !store.getState().settingsState.disableSandy;
}

View File

@@ -8,12 +8,12 @@
*/
import {useStore} from '../../../app/src/utils/useStore';
import isSandyEnabled from './isSandyEnabled';
/**
* This hook returns whether dark mode is currently being used.
* Generally should be avoided in favor of using the above theme object,
* which will provide colors that reflect the theme
*/
export function useIsDarkMode(): boolean {
return useStore((state) => isSandyEnabled() && state.settingsState.darkMode);
return useStore((state) => state.settingsState.darkMode);
}