Wire up tracking to Sandy Chrome

Summary: Wired up tracking to all chrome sections and some import UI elements

Reviewed By: jknoxville

Differential Revision: D25219089

fbshipit-source-id: c75bed91894609dafc5fcc6423a5228211fb92d8
This commit is contained in:
Michel Weststrate
2020-12-03 04:13:07 -08:00
committed by Facebook GitHub Bot
parent dd6f39c2b3
commit 84c6e05b8a
18 changed files with 539 additions and 447 deletions

View File

@@ -26,7 +26,11 @@ import electron, {MenuItemConstructorOptions} from 'electron';
import {notNull} from './utils/typeUtils'; import {notNull} from './utils/typeUtils';
import constants from './fb-stubs/constants'; import constants from './fb-stubs/constants';
import {Logger} from './fb-interfaces/Logger'; import {Logger} from './fb-interfaces/Logger';
import {NormalizedMenuEntry, _buildInMenuEntries} from 'flipper-plugin'; import {
NormalizedMenuEntry,
_buildInMenuEntries,
_wrapInteractionHandler,
} from 'flipper-plugin';
import {StyleGuide} from './sandy-chrome/StyleGuide'; import {StyleGuide} from './sandy-chrome/StyleGuide';
import {showEmulatorLauncher} from './sandy-chrome/appinspect/LaunchEmulator'; import {showEmulatorLauncher} from './sandy-chrome/appinspect/LaunchEmulator';
@@ -191,7 +195,13 @@ export function addSandyPluginEntries(entries: NormalizedMenuEntry[]) {
if (parent) { if (parent) {
const item = new electron.remote.MenuItem({ const item = new electron.remote.MenuItem({
enabled: true, enabled: true,
click: () => pluginActionHandler?.(entry.action!), click: _wrapInteractionHandler(
() => pluginActionHandler?.(entry.action!),
'MenuItem',
'onClick',
'flipper:menu:' + entry.topLevelMenu,
entry.label,
),
label: entry.label, label: entry.label,
accelerator: entry.accelerator, accelerator: entry.accelerator,
}); });
@@ -206,6 +216,20 @@ export function addSandyPluginEntries(entries: NormalizedMenuEntry[]) {
} }
} }
function trackMenuItems(menu: string, items: MenuItemConstructorOptions[]) {
items.forEach((item) => {
if (item.label && item.click) {
item.click = _wrapInteractionHandler(
item.click,
'MenuItem',
'onClick',
'flipper:menu:' + menu,
item.label,
);
}
});
}
function getTemplate( function getTemplate(
app: electron.App, app: electron.App,
shell: electron.Shell, shell: electron.Shell,
@@ -226,6 +250,8 @@ function getTemplate(
click: () => startLinkExport(store.dispatch), click: () => startLinkExport(store.dispatch),
}); });
} }
trackMenuItems('export', exportSubmenu);
const fileSubmenu: MenuItemConstructorOptions[] = [ const fileSubmenu: MenuItemConstructorOptions[] = [
{ {
label: 'Launch Emulator...', label: 'Launch Emulator...',
@@ -250,6 +276,8 @@ function getTemplate(
submenu: exportSubmenu, submenu: exportSubmenu,
}, },
]; ];
trackMenuItems('file', fileSubmenu);
const supportRequestSubmenu = [ const supportRequestSubmenu = [
{ {
label: 'Create...', label: 'Create...',
@@ -259,11 +287,101 @@ function getTemplate(
}, },
}, },
]; ];
trackMenuItems('support', supportRequestSubmenu);
fileSubmenu.push({ fileSubmenu.push({
label: 'Support Requests', label: 'Support Requests',
submenu: supportRequestSubmenu, submenu: supportRequestSubmenu,
}); });
const viewMenu: MenuItemConstructorOptions[] = [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: function (_, focusedWindow: electron.BrowserWindow | undefined) {
if (focusedWindow) {
logger.track('usage', 'reload');
focusedWindow.reload();
}
},
},
{
label: 'Toggle Full Screen',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Ctrl+Command+F';
} else {
return 'F11';
}
})(),
click: function (_, focusedWindow: electron.BrowserWindow | undefined) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
},
{
label: 'Manage Plugins...',
click: function () {
store.dispatch(setActiveSheet(ACTIVE_SHEET_PLUGINS));
},
},
{
label: 'Flipper style guide',
click() {
store.dispatch(setStaticView(StyleGuide));
},
},
{
label: 'Toggle Developer Tools',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
} else {
return 'Ctrl+Shift+I';
}
})(),
click: function (_, focusedWindow: electron.BrowserWindow | undefined) {
if (focusedWindow) {
// @ts-ignore: https://github.com/electron/electron/issues/7832
focusedWindow.toggleDevTools();
}
},
},
{
type: 'separator',
},
];
trackMenuItems('view', viewMenu);
const helpMenu: MenuItemConstructorOptions[] = [
{
label: 'Getting started',
click: function () {
shell.openExternal('https://fbflipper.com/docs/getting-started/index');
},
},
{
label: 'Create plugins',
click: function () {
shell.openExternal('https://fbflipper.com/docs/tutorial/intro');
},
},
{
label: 'Report problems',
click: function () {
shell.openExternal(constants.FEEDBACK_GROUP_LINK);
},
},
{
label: 'Changelog',
click() {
store.dispatch(setActiveSheet(ACTIVE_SHEET_CHANGELOG));
},
},
];
trackMenuItems('help', helpMenu);
const template: MenuItemConstructorOptions[] = [ const template: MenuItemConstructorOptions[] = [
{ {
label: 'File', label: 'File',
@@ -309,73 +427,7 @@ function getTemplate(
}, },
{ {
label: 'View', label: 'View',
submenu: [ submenu: viewMenu,
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: function (
_,
focusedWindow: electron.BrowserWindow | undefined,
) {
if (focusedWindow) {
logger.track('usage', 'reload');
focusedWindow.reload();
}
},
},
{
label: 'Toggle Full Screen',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Ctrl+Command+F';
} else {
return 'F11';
}
})(),
click: function (
_,
focusedWindow: electron.BrowserWindow | undefined,
) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
},
{
label: 'Manage Plugins...',
click: function () {
store.dispatch(setActiveSheet(ACTIVE_SHEET_PLUGINS));
},
},
{
label: 'Flipper style guide',
click() {
store.dispatch(setStaticView(StyleGuide));
},
},
{
label: 'Toggle Developer Tools',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
} else {
return 'Ctrl+Shift+I';
}
})(),
click: function (
_,
focusedWindow: electron.BrowserWindow | undefined,
) {
if (focusedWindow) {
// @ts-ignore: https://github.com/electron/electron/issues/7832
focusedWindow.toggleDevTools();
}
},
},
{
type: 'separator',
},
],
}, },
{ {
label: 'Window', label: 'Window',
@@ -396,36 +448,10 @@ function getTemplate(
{ {
label: 'Help', label: 'Help',
role: 'help', role: 'help',
submenu: [ submenu: helpMenu,
{
label: 'Getting started',
click: function () {
shell.openExternal(
'https://fbflipper.com/docs/getting-started/index',
);
},
},
{
label: 'Create plugins',
click: function () {
shell.openExternal('https://fbflipper.com/docs/tutorial/intro');
},
},
{
label: 'Report problems',
click: function () {
shell.openExternal(constants.FEEDBACK_GROUP_LINK);
},
},
{
label: 'Changelog',
click() {
store.dispatch(setActiveSheet(ACTIVE_SHEET_CHANGELOG));
},
},
],
}, },
]; ];
trackMenuItems('support', supportRequestSubmenu);
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const name = app.name; const name = app.name;

View File

@@ -48,15 +48,15 @@ export default function ScreenCaptureButtons({useSandy}: {useSandy?: boolean}) {
const handleScreenshot = useCallback(() => { const handleScreenshot = useCallback(() => {
setIsTakingScreenshot(true); setIsTakingScreenshot(true);
capture(selectedDevice!) const p = capture(selectedDevice!).then(openFile);
.then(openFile)
.catch((e) => { p.catch((e) => {
console.error('Taking screenshot failed:', e); console.error('Taking screenshot failed:', e);
message.error('Taking screenshot failed:' + e); message.error('Taking screenshot failed:' + e);
}) }).finally(() => {
.finally(() => { setIsTakingScreenshot(false);
setIsTakingScreenshot(false); });
}); return p;
}, [selectedDevice]); }, [selectedDevice]);
const handleRecording = useCallback(() => { const handleRecording = useCallback(() => {
if (!selectedDevice) { if (!selectedDevice) {
@@ -65,13 +65,13 @@ export default function ScreenCaptureButtons({useSandy}: {useSandy?: boolean}) {
if (!isRecording) { if (!isRecording) {
setIsRecording(true); setIsRecording(true);
const videoPath = path.join(CAPTURE_LOCATION, getFileName('mp4')); const videoPath = path.join(CAPTURE_LOCATION, getFileName('mp4'));
selectedDevice.startScreenCapture(videoPath).catch((e) => { return selectedDevice.startScreenCapture(videoPath).catch((e) => {
console.error('Failed to start recording', e); console.error('Failed to start recording', e);
message.error('Failed to start recording' + e); message.error('Failed to start recording' + e);
setIsRecording(false); setIsRecording(false);
}); });
} else { } else {
selectedDevice return selectedDevice
.stopScreenCapture() .stopScreenCapture()
.then(openFile) .then(openFile)
.catch((e) => { .catch((e) => {

View File

@@ -28,7 +28,7 @@ import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel';
import SandySettingsPanel from '../fb-stubs/SandySettingsPanel'; import SandySettingsPanel from '../fb-stubs/SandySettingsPanel';
import {reportUsage} from '../utils/metrics'; import {reportUsage} from '../utils/metrics';
import {Modal} from 'antd'; import {Modal} from 'antd';
import {Layout, _NuxManagerContext} from 'flipper-plugin'; import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin';
const Container = styled(FlexColumn)({ const Container = styled(FlexColumn)({
padding: 20, padding: 20,
@@ -356,7 +356,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
isXcodeDetected: application.xcodeCommandLineToolsDetected, isXcodeDetected: application.xcodeCommandLineToolsDetected,
}), }),
{updateSettings, updateLauncherSettings}, {updateSettings, updateLauncherSettings},
)(SettingsSheet); )(withTrackingScope(SettingsSheet));
function ResetTooltips() { function ResetTooltips() {
const nuxManager = useContext(_NuxManagerContext); const nuxManager = useContext(_NuxManagerContext);

View File

@@ -27,6 +27,7 @@ import ContextMenu from '../ui/components/ContextMenu';
import {clipboard} from 'electron'; import {clipboard} from 'electron';
import {reportPlatformFailures} from '../utils/metrics'; import {reportPlatformFailures} from '../utils/metrics';
import {Modal} from 'antd'; import {Modal} from 'antd';
import {TrackingScope} from 'flipper-plugin';
const Container = styled(FlexColumn)({ const Container = styled(FlexColumn)({
padding: 20, padding: 20,
@@ -135,15 +136,17 @@ class SignInSheet extends Component<Props, State> {
footer: React.ReactElement, footer: React.ReactElement,
) { ) {
return ( return (
<Modal <TrackingScope scope="logindialog">
visible <Modal
centered visible
onCancel={this.props.onHide} centered
width={570} onCancel={this.props.onHide}
title="Login" width={570}
footer={footer}> title="Login"
<FlexColumn>{contents}</FlexColumn> footer={footer}>
</Modal> <FlexColumn>{contents}</FlexColumn>
</Modal>
</TrackingScope>
); );
} }

View File

@@ -21,6 +21,7 @@ const {shell, remote} = !isHeadless()
: {shell: undefined, remote: undefined}; : {shell: undefined, remote: undefined};
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import React from 'react'; import React from 'react';
import {Tracked, TrackingScope} from 'flipper-plugin';
const Container = styled(FlexColumn)({ const Container = styled(FlexColumn)({
height: '100%', height: '100%',
@@ -127,64 +128,72 @@ export default class WelcomeScreen extends PureComponent<Props, State> {
render() { render() {
return ( return (
<Container> <Container>
<Welcome isMounted={this.state.isMounted}> <TrackingScope scope="welcomescreen">
<Logo src="./icon.png" /> <Welcome isMounted={this.state.isMounted}>
<Title>Welcome to Flipper</Title> <Logo src="./icon.png" />
<Version> <Title>Welcome to Flipper</Title>
{isProduction() && remote <Version>
? `Version ${remote.app.getVersion()}` {isProduction() && remote
: 'Development Mode'} ? `Version ${remote.app.getVersion()}`
</Version> : 'Development Mode'}
<Item </Version>
onClick={() => <Tracked>
shell && <Item
shell.openExternal('https://fbflipper.com/docs/features/index') onClick={() => {
}> shell &&
<Icon size={20} name="rocket" color={brandColors.Flipper} /> shell.openExternal(
<FlexColumn> 'https://fbflipper.com/docs/features/index',
<ItemTitle>Using Flipper</ItemTitle> );
<ItemSubTitle> }}>
Learn how Flipper can help you debug your App <Icon size={20} name="rocket" color={brandColors.Flipper} />
</ItemSubTitle> <FlexColumn>
</FlexColumn> <ItemTitle>Using Flipper</ItemTitle>
</Item> <ItemSubTitle>
<Item Learn how Flipper can help you debug your App
onClick={() => </ItemSubTitle>
shell && </FlexColumn>
shell.openExternal('https://fbflipper.com/docs/tutorial/intro') </Item>
}> <Item
<Icon size={20} name="magic-wand" color={brandColors.Flipper} /> onClick={() =>
<FlexColumn> shell &&
<ItemTitle>Create your own plugin</ItemTitle> shell.openExternal(
<ItemSubTitle>Get started with these pointers</ItemSubTitle> 'https://fbflipper.com/docs/tutorial/intro',
</FlexColumn> )
</Item> }>
<Item <Icon size={20} name="magic-wand" color={brandColors.Flipper} />
onClick={() => <FlexColumn>
shell && <ItemTitle>Create your own plugin</ItemTitle>
shell.openExternal( <ItemSubTitle>Get started with these pointers</ItemSubTitle>
'https://fbflipper.com/docs/getting-started/index', </FlexColumn>
) </Item>
}> <Item
<Icon size={20} name="tools" color={brandColors.Flipper} /> onClick={() =>
<FlexColumn> shell &&
<ItemTitle>Add Flipper support to your app</ItemTitle> shell.openExternal(
<ItemSubTitle>Get started with these pointers</ItemSubTitle> 'https://fbflipper.com/docs/getting-started/index',
</FlexColumn> )
</Item> }>
<Item <Icon size={20} name="tools" color={brandColors.Flipper} />
onClick={() => <FlexColumn>
shell && shell.openExternal(constants.FEEDBACK_GROUP_LINK) <ItemTitle>Add Flipper support to your app</ItemTitle>
}> <ItemSubTitle>Get started with these pointers</ItemSubTitle>
<Icon size={20} name="posts" color={brandColors.Flipper} /> </FlexColumn>
<FlexColumn> </Item>
<ItemTitle>Contributing and Feedback</ItemTitle> <Item
<ItemSubTitle> onClick={() =>
Report issues and help us improve Flipper shell && shell.openExternal(constants.FEEDBACK_GROUP_LINK)
</ItemSubTitle> }>
</FlexColumn> <Icon size={20} name="posts" color={brandColors.Flipper} />
</Item> <FlexColumn>
</Welcome> <ItemTitle>Contributing and Feedback</ItemTitle>
<ItemSubTitle>
Report issues and help us improve Flipper
</ItemSubTitle>
</FlexColumn>
</Item>
</Tracked>
</Welcome>
</TrackingScope>
</Container> </Container>
); );
} }

View File

@@ -343,7 +343,7 @@ const demos: PreviewProps[] = [
], ],
demos: { demos: {
'Basic example': ( 'Basic example': (
<TrackingScope scope="Tracking scope demo"> <TrackingScope scope="tracking scope demo">
<Tracked> <Tracked>
<Button onClick={() => {}}>Test</Button> <Button onClick={() => {}}>Test</Button>
</Tracked> </Tracked>
@@ -356,65 +356,67 @@ const demos: PreviewProps[] = [
function ComponentPreview({title, demos, description, props}: PreviewProps) { function ComponentPreview({title, demos, description, props}: PreviewProps) {
return ( return (
<Card title={title} size="small" type="inner"> <Card title={title} size="small" type="inner">
<Layout.Container gap="small"> <TrackingScope scope={title}>
<Text type="secondary">{description}</Text> <Layout.Container gap="small">
<Collapse ghost> <Text type="secondary">{description}</Text>
<Collapse.Panel header="Examples" key="demos"> <Collapse ghost>
<Layout.Container gap="large"> <Collapse.Panel header="Examples" key="demos">
{Object.entries(demos).map(([name, children]) => ( <Layout.Container gap="large">
<div key={name}> {Object.entries(demos).map(([name, children]) => (
<Tabs type="line"> <div key={name}>
<Tabs.TabPane tab={name} key="1"> <Tabs type="line">
<div <Tabs.TabPane tab={name} key="1">
style={{ <div
background: theme.backgroundWash, style={{
width: '100%', background: theme.backgroundWash,
}}> width: '100%',
{children} }}>
</div> {children}
</Tabs.TabPane> </div>
<Tabs.TabPane tab={<CodeOutlined />} key="2"> </Tabs.TabPane>
<div <Tabs.TabPane tab={<CodeOutlined />} key="2">
style={{ <div
background: theme.backgroundWash, style={{
width: '100%', background: theme.backgroundWash,
padding: theme.space.medium, width: '100%',
}}> padding: theme.space.medium,
<pre>{reactElementToJSXString(children)}</pre> }}>
</div> <pre>{reactElementToJSXString(children)}</pre>
</Tabs.TabPane> </div>
</Tabs> </Tabs.TabPane>
</div> </Tabs>
))} </div>
</Layout.Container> ))}
</Collapse.Panel> </Layout.Container>
<Collapse.Panel header="Props" key="props"> </Collapse.Panel>
<Table <Collapse.Panel header="Props" key="props">
size="small" <Table
pagination={false} size="small"
dataSource={props.map((prop) => pagination={false}
Object.assign(prop, {key: prop[0]}), dataSource={props.map((prop) =>
)} Object.assign(prop, {key: prop[0]}),
columns={[ )}
{ columns={[
title: 'Property', {
dataIndex: 0, title: 'Property',
width: 100, dataIndex: 0,
}, width: 100,
{ },
title: 'Type and default', {
dataIndex: 1, title: 'Type and default',
width: 200, dataIndex: 1,
}, width: 200,
{ },
title: 'Description', {
dataIndex: 2, title: 'Description',
}, dataIndex: 2,
]} },
/> ]}
</Collapse.Panel> />
</Collapse> </Collapse.Panel>
</Layout.Container> </Collapse>
</Layout.Container>
</TrackingScope>
</Card> </Card>
); );
} }

View File

@@ -35,7 +35,7 @@ import {
toggleLeftSidebarVisible, toggleLeftSidebarVisible,
toggleRightSidebarVisible, toggleRightSidebarVisible,
} from '../reducers/application'; } from '../reducers/application';
import {theme, Layout} from 'flipper-plugin'; import {theme, Layout, withTrackingScope} from 'flipper-plugin';
import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen'; import SetupDoctorScreen, {checkHasNewProblem} from './SetupDoctorScreen';
import SettingsSheet from '../chrome/SettingsSheet'; import SettingsSheet from '../chrome/SettingsSheet';
import WelcomeScreen from './WelcomeScreen'; import WelcomeScreen from './WelcomeScreen';
@@ -98,6 +98,7 @@ export function LeftRailButton({
return ( return (
<Tooltip title={title} placement="right"> <Tooltip title={title} placement="right">
<LeftRailButtonElem <LeftRailButtonElem
title={title}
kind={small ? 'small' : undefined} kind={small ? 'small' : undefined}
type={selected ? 'primary' : 'ghost'} type={selected ? 'primary' : 'ghost'}
icon={iconElement} icon={iconElement}
@@ -119,7 +120,7 @@ const LeftRailDivider = styled(Divider)({
}); });
LeftRailDivider.displayName = 'LeftRailDividier'; LeftRailDivider.displayName = 'LeftRailDividier';
export function LeftRail({ export const LeftRail = withTrackingScope(function LeftRail({
toplevelSelection, toplevelSelection,
setToplevelSelection, setToplevelSelection,
}: ToplevelProps) { }: ToplevelProps) {
@@ -167,7 +168,7 @@ export function LeftRail({
</Layout.Bottom> </Layout.Bottom>
</Layout.Container> </Layout.Container>
); );
} });
function LeftSidebarToggleButton() { function LeftSidebarToggleButton() {
const dispatch = useDispatch(); const dispatch = useDispatch();

View File

@@ -127,9 +127,9 @@ export function SandyApp({logger}: {logger: Logger}) {
{staticView ? ( {staticView ? (
<TrackingScope <TrackingScope
scope={ scope={
staticView.constructor?.name ??
staticView.displayName ?? staticView.displayName ??
staticView.name ?? staticView.name ??
staticView.constructor?.name ??
'unknown static view' 'unknown static view'
}> }>
<ContentContainer> <ContentContainer>

View File

@@ -16,7 +16,7 @@ import {
CodeOutlined, CodeOutlined,
BugOutlined, BugOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import {theme} from 'flipper-plugin'; import {theme, Tracked, TrackingScope} from 'flipper-plugin';
const {Text, Title} = Typography; const {Text, Title} = Typography;
@@ -45,19 +45,21 @@ function Row(props: {
onClick?: () => void; onClick?: () => void;
}) { }) {
return ( return (
<RowContainer onClick={props.onClick}> <Tracked action={props.title}>
<Space size="middle"> <RowContainer onClick={props.onClick}>
{cloneElement(props.icon, { <Space size="middle">
style: {fontSize: 36, color: theme.primaryColor}, {cloneElement(props.icon, {
})} style: {fontSize: 36, color: theme.primaryColor},
<FlexColumn> })}
<Title level={3} style={{color: theme.primaryColor}}> <FlexColumn>
{props.title} <Title level={3} style={{color: theme.primaryColor}}>
</Title> {props.title}
<Text type="secondary">{props.subtitle}</Text> </Title>
</FlexColumn> <Text type="secondary">{props.subtitle}</Text>
</Space> </FlexColumn>
</RowContainer> </Space>
</RowContainer>
</Tracked>
); );
} }
@@ -114,46 +116,48 @@ export default function WelcomeScreen({
/> />
} }
onCancel={onClose}> onCancel={onClose}>
<Space <TrackingScope scope="welcomescreen">
direction="vertical" <Space
size="middle" direction="vertical"
style={{width: '100%', padding: '32px', alignItems: 'center'}}> size="middle"
<Image width={125} height={125} src="./icon.png" preview={false} /> style={{width: '100%', padding: '32px', alignItems: 'center'}}>
<Title level={1}>Welcome to Flipper</Title> <Image width={125} height={125} src="./icon.png" preview={false} />
<Text style={{color: theme.textColorPlaceholder}}> <Title level={1}>Welcome to Flipper</Title>
{isProduction() && remote <Text style={{color: theme.textColorPlaceholder}}>
? `Version ${remote.app.getVersion()}` {isProduction() && remote
: 'Development Mode'} ? `Version ${remote.app.getVersion()}`
</Text> : 'Development Mode'}
</Space> </Text>
<Space direction="vertical" size="large" style={{width: '100%'}}> </Space>
<Row <Space direction="vertical" size="large" style={{width: '100%'}}>
icon={<RocketOutlined />} <Row
title="Using Flipper" icon={<RocketOutlined />}
subtitle="Learn how Flipper can help you debug your App" title="Using Flipper"
onClick={openExternal('https://fbflipper.com/docs/features/index')} subtitle="Learn how Flipper can help you debug your App"
/> onClick={openExternal('https://fbflipper.com/docs/features/index')}
<Row />
icon={<AppstoreAddOutlined />} <Row
title="Create Your Own Plugin" icon={<AppstoreAddOutlined />}
subtitle="Get started with these pointers" title="Create Your Own Plugin"
onClick={openExternal('https://fbflipper.com/docs/tutorial/intro')} subtitle="Get started with these pointers"
/> onClick={openExternal('https://fbflipper.com/docs/tutorial/intro')}
<Row />
icon={<CodeOutlined />} <Row
title="Add Flipper Support to Your App" icon={<CodeOutlined />}
subtitle="Get started with these pointers" title="Add Flipper Support to Your App"
onClick={openExternal( subtitle="Get started with these pointers"
'https://fbflipper.com/docs/getting-started/index', onClick={openExternal(
)} 'https://fbflipper.com/docs/getting-started/index',
/> )}
<Row />
icon={<BugOutlined />} <Row
title="Contributing and Feedback" icon={<BugOutlined />}
subtitle="Report issues and help us improve Flipper" title="Contributing and Feedback"
onClick={openExternal(constants.FEEDBACK_GROUP_LINK)} subtitle="Report issues and help us improve Flipper"
/> onClick={openExternal(constants.FEEDBACK_GROUP_LINK)}
</Space> />
</Space>
</TrackingScope>
</Modal> </Modal>
); );
} }

View File

@@ -16,9 +16,9 @@ import {
CaretDownOutlined, CaretDownOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import {Glyph, Layout, styled} from '../../ui'; import {Glyph, Layout, styled} from '../../ui';
import {theme} from 'flipper-plugin'; import {theme, useTrackedCallback} from 'flipper-plugin';
import {batch} from 'react-redux'; import {batch} from 'react-redux';
import {Dispatch, useDispatch, useStore} from '../../utils/useStore'; import {useDispatch, useStore} from '../../utils/useStore';
import { import {
canBeDefaultDevice, canBeDefaultDevice,
getAvailableClients, getAvailableClients,
@@ -55,11 +55,34 @@ export function AppSelector() {
uninitializedClients, uninitializedClients,
selectedApp, selectedApp,
} = useStore((state) => state.connections); } = useStore((state) => state.connections);
const onSelectDevice = useTrackedCallback(
'select-device',
(device: BaseDevice) => {
batch(() => {
dispatch(selectDevice(device));
dispatch(selectClient(null));
});
},
[],
);
const onSelectApp = useTrackedCallback(
'select-app',
(device: BaseDevice, client: Client) => {
batch(() => {
dispatch(selectDevice(device));
dispatch(selectClient(client.id));
});
},
[],
);
const entries = computeEntries( const entries = computeEntries(
devices, devices,
dispatch,
clients, clients,
uninitializedClients, uninitializedClients,
onSelectDevice,
onSelectApp,
); );
const client = clients.find((client) => client.id === selectedApp); const client = clients.find((client) => client.id === selectedApp);
@@ -144,9 +167,10 @@ const AppIconContainer = styled.div({
function computeEntries( function computeEntries(
devices: BaseDevice[], devices: BaseDevice[],
dispatch: Dispatch,
clients: Client[], clients: Client[],
uninitializedClients: State['connections']['uninitializedClients'], uninitializedClients: State['connections']['uninitializedClients'],
onSelectDevice: (device: BaseDevice) => void,
onSelectApp: (device: BaseDevice, client: Client) => void,
) { ) {
const entries = devices.filter(canBeDefaultDevice).map((device) => { const entries = devices.filter(canBeDefaultDevice).map((device) => {
const deviceEntry = ( const deviceEntry = (
@@ -155,10 +179,7 @@ function computeEntries(
key={device.serial} key={device.serial}
style={{fontWeight: 'bold'}} style={{fontWeight: 'bold'}}
onClick={() => { onClick={() => {
batch(() => { onSelectDevice(device);
dispatch(selectDevice(device));
dispatch(selectClient(null));
});
}}> }}>
{device.displayTitle()} {device.displayTitle()}
</Menu.Item> </Menu.Item>
@@ -167,10 +188,7 @@ function computeEntries(
<Menu.Item <Menu.Item
key={client.id} key={client.id}
onClick={() => { onClick={() => {
batch(() => { onSelectApp(device, client);
dispatch(selectDevice(device));
dispatch(selectClient(client.id));
});
}}> }}>
<Radio value={client.id}>{client.query.app}</Radio> <Radio value={client.id}>{client.query.app}</Radio>
</Menu.Item> </Menu.Item>

View File

@@ -7,11 +7,17 @@
* @format * @format
*/ */
import React, {useCallback, useMemo} from 'react'; import React, {useMemo} from 'react';
import {AutoComplete, Input, Typography} from 'antd'; import {AutoComplete, Input, Typography} from 'antd';
import {StarFilled, StarOutlined} from '@ant-design/icons'; import {StarFilled, StarOutlined} from '@ant-design/icons';
import {useStore} from '../../utils/useStore'; import {useStore} from '../../utils/useStore';
import {Layout, NUX, useValue} from 'flipper-plugin'; import {
Layout,
NUX,
TrackingScope,
useTrackedCallback,
useValue,
} from 'flipper-plugin';
import {navPluginStateSelector} from '../../chrome/LocationsButton'; import {navPluginStateSelector} from '../../chrome/LocationsButton';
// eslint-disable-next-line flipper/no-relative-imports-across-packages // eslint-disable-next-line flipper/no-relative-imports-across-packages
@@ -25,11 +31,13 @@ export function BookmarkSection() {
const navPlugin = useStore(navPluginStateSelector); const navPlugin = useStore(navPluginStateSelector);
return navPlugin ? ( return navPlugin ? (
<NUX <TrackingScope scope="bookmarks">
title="Use bookmarks to directly navigate to a location in the app." <NUX
placement="right"> title="Use bookmarks to directly navigate to a location in the app."
<BookmarkSectionInput navPlugin={navPlugin} /> placement="right">
</NUX> <BookmarkSectionInput navPlugin={navPlugin} />
</NUX>
</TrackingScope>
) : null; ) : null;
} }
@@ -48,16 +56,22 @@ function BookmarkSectionInput({navPlugin}: {navPlugin: NavigationPlugin}) {
[currentURI, bookmarks, patterns, 20], [currentURI, bookmarks, patterns, 20],
); );
const handleBookmarkClick = useCallback(() => { const handleBookmarkClick = useTrackedCallback(
if (isBookmarked) { 'bookmark',
navPlugin.removeBookmark(currentURI); () => {
} else if (currentURI) { if (isBookmarked) {
navPlugin.addBookmark({ navPlugin.removeBookmark(currentURI);
uri: currentURI, } else if (currentURI) {
commonName: null, navPlugin.addBookmark({
}); uri: currentURI,
} commonName: null,
}, [navPlugin, currentURI, isBookmarked]); });
}
},
[navPlugin, currentURI, isBookmarked],
);
const navigate = useTrackedCallback('navigate', navPlugin.navigateTo, []);
const bookmarkButton = isBookmarked ? ( const bookmarkButton = isBookmarked ? (
<StarFilled onClick={handleBookmarkClick} /> <StarFilled onClick={handleBookmarkClick} />
@@ -69,7 +83,7 @@ function BookmarkSectionInput({navPlugin}: {navPlugin: NavigationPlugin}) {
<StyledAutoComplete <StyledAutoComplete
dropdownMatchSelectWidth={500} dropdownMatchSelectWidth={500}
value={currentURI} value={currentURI}
onSelect={navPlugin.navigateTo} onSelect={navigate}
style={{flex: 1}} style={{flex: 1}}
options={[ options={[
{ {
@@ -99,7 +113,7 @@ function BookmarkSectionInput({navPlugin}: {navPlugin: NavigationPlugin}) {
navPlugin.currentURI.set(e.target.value); navPlugin.currentURI.set(e.target.value);
}} }}
onPressEnter={() => { onPressEnter={() => {
navPlugin.navigateTo(currentURI); navigate(currentURI);
}} }}
/> />
</StyledAutoComplete> </StyledAutoComplete>

View File

@@ -13,7 +13,7 @@ import {AndroidOutlined, AppleOutlined} from '@ant-design/icons';
import {Store} from '../../reducers'; import {Store} from '../../reducers';
import {useStore} from '../../utils/useStore'; import {useStore} from '../../utils/useStore';
import {launchEmulator} from '../../devices/AndroidDevice'; import {launchEmulator} from '../../devices/AndroidDevice';
import {Layout, renderReactRoot} from 'flipper-plugin'; import {Layout, renderReactRoot, withTrackingScope} from 'flipper-plugin';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import { import {
launchSimulator, launchSimulator,
@@ -31,77 +31,81 @@ export function showEmulatorLauncher(store: Store) {
type GetSimulators = typeof getSimulators; type GetSimulators = typeof getSimulators;
export function LaunchEmulatorDialog({ export const LaunchEmulatorDialog = withTrackingScope(
onClose, function LaunchEmulatorDialog({
getSimulators, onClose,
}: { getSimulators,
onClose: () => void; }: {
getSimulators: GetSimulators; onClose: () => void;
}) { getSimulators: GetSimulators;
const iosEnabled = useStore((state) => state.settingsState.enableIOS); }) {
const androidEmulators = useStore((state) => const iosEnabled = useStore((state) => state.settingsState.enableIOS);
state.settingsState.enableAndroid ? state.connections.androidEmulators : [], const androidEmulators = useStore((state) =>
); state.settingsState.enableAndroid
const [iosEmulators, setIosEmulators] = useState<IOSDeviceParams[]>([]); ? state.connections.androidEmulators
: [],
);
const [iosEmulators, setIosEmulators] = useState<IOSDeviceParams[]>([]);
useEffect(() => { useEffect(() => {
if (!iosEnabled) { if (!iosEnabled) {
return; return;
} }
getSimulators(false).then((emulators) => { getSimulators(false).then((emulators) => {
setIosEmulators( setIosEmulators(
emulators.filter( emulators.filter(
(device) => (device) =>
device.state === 'Shutdown' && device.state === 'Shutdown' &&
device.deviceTypeIdentifier?.match(/iPhone|iPad/i), device.deviceTypeIdentifier?.match(/iPhone|iPad/i),
), ),
); );
}); });
}, [iosEnabled, getSimulators]); }, [iosEnabled, getSimulators]);
const items = [ const items = [
...androidEmulators.map((name) => ( ...androidEmulators.map((name) => (
<Button <Button
key={name} key={name}
icon={<AndroidOutlined />} icon={<AndroidOutlined />}
onClick={() => { onClick={() =>
launchEmulator(name) launchEmulator(name)
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
message.error('Failed to start emulator: ' + e); message.error('Failed to start emulator: ' + e);
}) })
.finally(onClose); .then(onClose)
}}> }>
{name} {name}
</Button> </Button>
)), )),
...iosEmulators.map((device) => ( ...iosEmulators.map((device) => (
<Button <Button
key={device.udid} key={device.udid}
icon={<AppleOutlined />} icon={<AppleOutlined />}
onClick={() => { onClick={() =>
launchSimulator(device.udid) launchSimulator(device.udid)
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
message.error('Failed to start simulator: ' + e); message.error('Failed to start simulator: ' + e);
}) })
.finally(onClose); .then(onClose)
}}> }>
{device.name} {device.name}
</Button> </Button>
)), )),
]; ];
return ( return (
<Modal <Modal
visible visible
onCancel={onClose} onCancel={onClose}
title="Launch Emulator" title="Launch Emulator"
footer={null} footer={null}
bodyStyle={{maxHeight: 400, overflow: 'auto'}}> bodyStyle={{maxHeight: 400, overflow: 'auto'}}>
<Layout.Container gap> <Layout.Container gap>
{items.length ? items : <Alert message="No emulators available" />} {items.length ? items : <Alert message="No emulators available" />}
</Layout.Container> </Layout.Container>
</Modal> </Modal>
); );
} },
);

View File

@@ -12,7 +12,7 @@ import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
import {InfoIcon, SidebarTitle} from '../LeftSidebar'; import {InfoIcon, SidebarTitle} from '../LeftSidebar';
import {PlusOutlined, MinusOutlined} from '@ant-design/icons'; import {PlusOutlined, MinusOutlined} from '@ant-design/icons';
import {Glyph, Layout, styled} from '../../ui'; import {Glyph, Layout, styled} from '../../ui';
import {theme, NUX} from 'flipper-plugin'; import {theme, NUX, Tracked} from 'flipper-plugin';
import {useDispatch, useStore} from '../../utils/useStore'; import {useDispatch, useStore} from '../../utils/useStore';
import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils'; import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
import {ClientPluginDefinition, DevicePluginDefinition} from '../../plugin'; import {ClientPluginDefinition, DevicePluginDefinition} from '../../plugin';
@@ -225,8 +225,9 @@ function ActionButton({
icon={icon} icon={icon}
title={title} title={title}
style={{border: 'none', color: theme.textColorPrimary}} style={{border: 'none', color: theme.textColorPrimary}}
onClick={() => { onClick={(e) => {
onClick(id); onClick(id);
e.stopPropagation();
}} }}
/> />
); );
@@ -273,26 +274,28 @@ const PluginEntry = memo(function PluginEntry({
}, [active]); }, [active]);
return ( return (
<Menu.Item <Tracked action={`open:${plugin.id}`}>
key={plugin.id} <Menu.Item
active={active} key={plugin.id}
disabled={disabled} active={active}
onClick={handleClick} disabled={disabled}
{...rest}> onClick={handleClick}
<Layout.Horizontal {...rest}>
center <Layout.Horizontal
gap={10} center
onMouseEnter={handleMouseEnter} gap={10}
onMouseLeave={handleMouseLeave}> onMouseEnter={handleMouseEnter}
<PluginIconWrapper disabled={disabled} ref={domRef}> onMouseLeave={handleMouseLeave}>
<Glyph size={16} name={plugin.icon || 'apps'} color="white" /> <PluginIconWrapper disabled={disabled} ref={domRef}>
</PluginIconWrapper> <Glyph size={16} name={plugin.icon || 'apps'} color="white" />
<Tooltip placement="right" title={tooltip} mouseEnterDelay={1}> </PluginIconWrapper>
<Text style={{flex: 1}}>{getPluginTitle(plugin)}</Text> <Tooltip placement="right" title={tooltip} mouseEnterDelay={1}>
</Tooltip> <Text style={{flex: 1}}>{getPluginTitle(plugin)}</Text>
{hovering && actions} </Tooltip>
</Layout.Horizontal> {hovering && actions}
</Menu.Item> </Layout.Horizontal>
</Menu.Item>
</Tracked>
); );
}); });

View File

@@ -21,7 +21,7 @@ import {useStore} from '../../utils/useStore';
import {useIsSandy} from '../../sandy-chrome/SandyContext'; import {useIsSandy} from '../../sandy-chrome/SandyContext';
import type {ButtonProps} from 'antd/lib/button'; import type {ButtonProps} from 'antd/lib/button';
import {DownOutlined, CheckOutlined} from '@ant-design/icons'; import {DownOutlined, CheckOutlined} from '@ant-design/icons';
import {theme} from 'flipper-plugin'; import {theme, Tracked} from 'flipper-plugin';
type ButtonType = 'primary' | 'success' | 'warning' | 'danger'; type ButtonType = 'primary' | 'success' | 'warning' | 'danger';
@@ -366,17 +366,19 @@ function ClassicButton(props: Props) {
} }
return ( return (
<StyledButton <Tracked>
{...restProps} <StyledButton
ref={_ref as any} {...restProps}
windowIsFocused={windowIsFocused} ref={_ref as any}
onClick={onClick} windowIsFocused={windowIsFocused}
onMouseDown={onMouseDown} onClick={onClick}
onMouseUp={onMouseUp} onMouseDown={onMouseDown}
inButtonGroup={inButtonGroup}> onMouseUp={onMouseUp}
{iconComponent} inButtonGroup={inButtonGroup}>
{children} {iconComponent}
</StyledButton> {children}
</StyledButton>
</Tracked>
); );
} }

View File

@@ -12,6 +12,7 @@ import Tooltip from './Tooltip';
import {colors} from './colors'; import {colors} from './colors';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React from 'react'; import React from 'react';
import {Tracked} from 'flipper-plugin';
type Props = React.ComponentProps<typeof ToolbarIconContainer> & { type Props = React.ComponentProps<typeof ToolbarIconContainer> & {
active?: boolean; active?: boolean;
@@ -30,17 +31,19 @@ const ToolbarIconContainer = styled.div({
export default function ToolbarIcon({active, icon, title, ...props}: Props) { export default function ToolbarIcon({active, icon, title, ...props}: Props) {
return ( return (
<Tooltip title={title}> <Tooltip title={title}>
<ToolbarIconContainer {...props}> <Tracked action={title}>
<Glyph <ToolbarIconContainer {...props}>
name={icon} <Glyph
size={16} name={icon}
color={ size={16}
active color={
? colors.macOSTitleBarIconSelected active
: colors.macOSTitleBarIconActive ? colors.macOSTitleBarIconSelected
} : colors.macOSTitleBarIconActive
/> }
</ToolbarIconContainer> />
</ToolbarIconContainer>
</Tracked>
</Tooltip> </Tooltip>
); );
} }

View File

@@ -58,6 +58,7 @@ export {
setGlobalInteractionReporter as _setGlobalInteractionReporter, setGlobalInteractionReporter as _setGlobalInteractionReporter,
withTrackingScope, withTrackingScope,
useTrackedCallback, useTrackedCallback,
wrapInteractionHandler as _wrapInteractionHandler,
} from './ui/Tracked'; } from './ui/Tracked';
export {sleep} from './utils/sleep'; export {sleep} from './utils/sleep';

View File

@@ -21,6 +21,7 @@ import {createHash} from 'crypto';
import type {TooltipPlacement} from 'antd/lib/tooltip'; import type {TooltipPlacement} from 'antd/lib/tooltip';
import {SandyPluginInstance} from '../plugin/Plugin'; import {SandyPluginInstance} from '../plugin/Plugin';
import {theme} from './theme'; import {theme} from './theme';
import {Tracked} from './Tracked';
const {Text} = Typography; const {Text} = Typography;
@@ -121,9 +122,11 @@ export function NUX({
style={{color: theme.textColorPrimary}}> style={{color: theme.textColorPrimary}}>
<BulbTwoTone style={{fontSize: 24}} /> <BulbTwoTone style={{fontSize: 24}} />
<Text>{title}</Text> <Text>{title}</Text>
<Button size="small" type="default" onClick={dismiss}> <Tracked action={'nux:dismiss:' + title.substr(0, 50)}>
Dismiss <Button size="small" type="default" onClick={dismiss}>
</Button> Dismiss
</Button>
</Tracked>
</Layout.Container> </Layout.Container>
}> }>
<Pulse /> <Pulse />

View File

@@ -109,7 +109,6 @@ export function useTrackedCallback<T extends Function>(
}, deps) as any; }, deps) as any;
} }
// Exported for test
export function wrapInteractionHandler<T extends Function>( export function wrapInteractionHandler<T extends Function>(
fn: T, fn: T,
element: React.ReactElement | null | string, element: React.ReactElement | null | string,