Make getFlipperLib generally available, and use it to decouple opening links from Electron

Summary:
This stack reduces our direct dependency on Electron, for example by exposing our own API to open links.

Also exposing `getFlipperLib` as API from `flipper-plugin`, so that these utility methods are available outside plugin contexts as well.

Reviewed By: timur-valiev

Differential Revision: D29661689

fbshipit-source-id: 0c0523326eeb0d9d8fbe3e03c4609327bb53596b
This commit is contained in:
Michel Weststrate
2021-07-15 01:51:58 -07:00
committed by Facebook GitHub Bot
parent 2b236c6114
commit 5dbd3bd414
20 changed files with 60 additions and 54 deletions

View File

@@ -26,11 +26,7 @@ import {processMessagesLater} from './utils/messageQueue';
import {emitBytesReceived} from './dispatcher/tracking'; import {emitBytesReceived} from './dispatcher/tracking';
import {debounce} from 'lodash'; import {debounce} from 'lodash';
import {batch} from 'react-redux'; import {batch} from 'react-redux';
import { import {createState, _SandyPluginInstance, getFlipperLib} from 'flipper-plugin';
createState,
_SandyPluginInstance,
_getFlipperLibImplementation,
} from 'flipper-plugin';
import {freeze} from 'immer'; import {freeze} from 'immer';
import GK from './fb-stubs/GK'; import GK from './fb-stubs/GK';
import {message} from 'antd'; import {message} from 'antd';
@@ -224,7 +220,7 @@ export default class Client extends EventEmitter {
this.sandyPluginStates.set( this.sandyPluginStates.set(
plugin.id, plugin.id,
new _SandyPluginInstance( new _SandyPluginInstance(
_getFlipperLibImplementation(), getFlipperLib(),
plugin, plugin,
this, this,
getPluginKey(this.id, {serial: this.query.device_id}, plugin.id), getPluginKey(this.id, {serial: this.query.device_id}, plugin.id),
@@ -260,7 +256,7 @@ export default class Client extends EventEmitter {
this.sandyPluginStates.set( this.sandyPluginStates.set(
plugin.id, plugin.id,
new _SandyPluginInstance( new _SandyPluginInstance(
_getFlipperLibImplementation(), getFlipperLib(),
plugin, plugin,
this, this,
getPluginKey(this.id, {serial: this.query.device_id}, plugin.id), getPluginKey(this.id, {serial: this.query.device_id}, plugin.id),

View File

@@ -29,6 +29,7 @@ import {
NormalizedMenuEntry, NormalizedMenuEntry,
_buildInMenuEntries, _buildInMenuEntries,
_wrapInteractionHandler, _wrapInteractionHandler,
getFlipperLib,
} from 'flipper-plugin'; } 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';
@@ -373,19 +374,21 @@ function getTemplate(
{ {
label: 'Getting started', label: 'Getting started',
click: function () { click: function () {
shell.openExternal('https://fbflipper.com/docs/getting-started/index'); getFlipperLib().openLink(
'https://fbflipper.com/docs/getting-started/index',
);
}, },
}, },
{ {
label: 'Create plugins', label: 'Create plugins',
click: function () { click: function () {
shell.openExternal('https://fbflipper.com/docs/tutorial/intro'); getFlipperLib().openLink('https://fbflipper.com/docs/tutorial/intro');
}, },
}, },
{ {
label: 'Report problems', label: 'Report problems',
click: function () { click: function () {
shell.openExternal(constants.FEEDBACK_GROUP_LINK); getFlipperLib().openLink(constants.FEEDBACK_GROUP_LINK);
}, },
}, },
{ {

View File

@@ -24,7 +24,6 @@ import {
import {PluginDefinition, DevicePluginMap, ClientPluginMap} from './plugin'; import {PluginDefinition, DevicePluginMap, ClientPluginMap} from './plugin';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import React, {Component, Fragment} from 'react'; import React, {Component, Fragment} from 'react';
import {clipboard} from 'electron';
import { import {
PluginNotification, PluginNotification,
updatePluginBlocklist, updatePluginBlocklist,
@@ -35,6 +34,7 @@ import {State as StoreState} from './reducers/index';
import textContent from './utils/textContent'; import textContent from './utils/textContent';
import createPaste from './fb-stubs/createPaste'; import createPaste from './fb-stubs/createPaste';
import {getPluginTitle} from './utils/pluginUtils'; import {getPluginTitle} from './utils/pluginUtils';
import {getFlipperLib} from 'flipper-plugin';
type OwnProps = { type OwnProps = {
onClear: () => void; onClear: () => void;
@@ -459,7 +459,7 @@ class NotificationItem extends Component<
createPaste(this.getContent()); createPaste(this.getContent());
}; };
copy = () => clipboard.writeText(this.getContent()); copy = () => getFlipperLib().writeTextToClipboard(this.getContent());
getContent = (): string => getContent = (): string =>
[ [

View File

@@ -37,7 +37,7 @@ import runHealthchecks, {
HealthcheckSettings, HealthcheckSettings,
HealthcheckEventsHandler, HealthcheckEventsHandler,
} from '../utils/runHealthchecks'; } from '../utils/runHealthchecks';
import {shell} from 'electron'; import {getFlipperLib} from 'flipper-plugin';
import {reportUsage} from '../utils/metrics'; import {reportUsage} from '../utils/metrics';
type StateFromProps = { type StateFromProps = {
@@ -295,7 +295,7 @@ class DoctorSheet extends Component<Props, State> {
} }
openHelpUrl(helpUrl?: string): void { openHelpUrl(helpUrl?: string): void {
helpUrl && shell.openExternal(helpUrl); helpUrl && getFlipperLib().openLink(helpUrl);
} }
async runHealthchecks(): Promise<void> { async runHealthchecks(): Promise<void> {

View File

@@ -27,7 +27,6 @@ import {
EXPORT_FLIPPER_TRACE_EVENT, EXPORT_FLIPPER_TRACE_EVENT,
displayFetchMetadataErrors, displayFetchMetadataErrors,
} from '../utils/exportData'; } from '../utils/exportData';
import {clipboard} from 'electron';
import ShareSheetErrorList from './ShareSheetErrorList'; import ShareSheetErrorList from './ShareSheetErrorList';
import {reportPlatformFailures} from '../utils/metrics'; import {reportPlatformFailures} from '../utils/metrics';
import CancellableExportStatus from './CancellableExportStatus'; import CancellableExportStatus from './CancellableExportStatus';
@@ -36,6 +35,7 @@ import ShareSheetPendingDialog from './ShareSheetPendingDialog';
import {getInstance as getLogger} from '../fb-stubs/Logger'; import {getInstance as getLogger} from '../fb-stubs/Logger';
import {resetSupportFormV2State} from '../reducers/supportForm'; import {resetSupportFormV2State} from '../reducers/supportForm';
import {MiddlewareAPI} from '../reducers/index'; import {MiddlewareAPI} from '../reducers/index';
import {getFlipperLib} from 'flipper-plugin';
export const SHARE_FLIPPER_TRACE_EVENT = 'share-flipper-link'; export const SHARE_FLIPPER_TRACE_EVENT = 'share-flipper-link';
@@ -148,7 +148,7 @@ export default class ShareSheetExportUrl extends Component<Props, State> {
if (flipperUrl) { if (flipperUrl) {
this.store.dispatch(setExportURL(flipperUrl)); this.store.dispatch(setExportURL(flipperUrl));
if (this.state.runInBackground) { if (this.state.runInBackground) {
clipboard.writeText(String(flipperUrl)); getFlipperLib().writeTextToClipboard(String(flipperUrl));
new Notification('Shareable Flipper Export created', { new Notification('Shareable Flipper Export created', {
body: 'URL copied to clipboard', body: 'URL copied to clipboard',
requireInteraction: true, requireInteraction: true,

View File

@@ -16,7 +16,7 @@ import {
DeviceLogListener, DeviceLogListener,
Idler, Idler,
createState, createState,
_getFlipperLibImplementation, getFlipperLib,
} from 'flipper-plugin'; } from 'flipper-plugin';
import {PluginDefinition, DevicePluginMap} from '../plugin'; import {PluginDefinition, DevicePluginMap} from '../plugin';
import {DeviceSpec, OS as PluginOS, PluginDetails} from 'flipper-plugin-lib'; import {DeviceSpec, OS as PluginOS, PluginDetails} from 'flipper-plugin-lib';
@@ -245,7 +245,7 @@ export default class BaseDevice {
this.sandyPluginStates.set( this.sandyPluginStates.set(
plugin.id, plugin.id,
new _SandyDevicePluginInstance( new _SandyDevicePluginInstance(
_getFlipperLibImplementation(), getFlipperLib(),
plugin, plugin,
this, this,
// break circular dep, one of those days again... // break circular dep, one of those days again...

View File

@@ -40,13 +40,13 @@ import {
_LoggerContext, _LoggerContext,
Layout, Layout,
theme, theme,
getFlipperLib,
} from 'flipper-plugin'; } from 'flipper-plugin';
import isProduction from './utils/isProduction'; import isProduction from './utils/isProduction';
import {Button, Input, Result, Typography} from 'antd'; import {Button, Input, Result, Typography} from 'antd';
import constants from './fb-stubs/constants'; import constants from './fb-stubs/constants';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {CopyOutlined} from '@ant-design/icons'; import {CopyOutlined} from '@ant-design/icons';
import {clipboard} from 'electron/common';
import {getVersionString} from './utils/versionString'; import {getVersionString} from './utils/versionString';
import {PersistGate} from 'redux-persist/integration/react'; import {PersistGate} from 'redux-persist/integration/react';
import {ipcRenderer} from 'electron'; import {ipcRenderer} from 'electron';
@@ -107,7 +107,7 @@ class AppFrame extends React.Component<
key="copy_error" key="copy_error"
icon={<CopyOutlined />} icon={<CopyOutlined />}
onClick={() => { onClick={() => {
clipboard.writeText(this.getError()); getFlipperLib().writeTextToClipboard(this.getError());
}}> }}>
Copy error Copy error
</Button>, </Button>,

View File

@@ -23,7 +23,7 @@ const {Text, Title} = Typography;
import constants from '../fb-stubs/constants'; import constants from '../fb-stubs/constants';
import isProduction from '../utils/isProduction'; import isProduction from '../utils/isProduction';
import {getAppVersion} from '../utils/info'; import {getAppVersion} from '../utils/info';
import {shell} from 'electron'; import {getFlipperLib} from 'flipper-plugin';
const RowContainer = styled(FlexRow)({ const RowContainer = styled(FlexRow)({
alignItems: 'flex-start', alignItems: 'flex-start',
@@ -89,7 +89,7 @@ function WelcomeFooter({
); );
} }
const openExternal = (url: string) => () => shell && shell.openExternal(url); const openExternal = (url: string) => () => getFlipperLib().openLink(url);
export default function WelcomeScreen({ export default function WelcomeScreen({
visible, visible,

View File

@@ -8,14 +8,14 @@
*/ */
import React, {useState, useCallback, useMemo} from 'react'; import React, {useState, useCallback, useMemo} from 'react';
import electron, {MenuItemConstructorOptions} from 'electron'; import {MenuItemConstructorOptions} from 'electron';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {Button as AntdButton, Dropdown, Menu} from 'antd'; import {Button as AntdButton, Dropdown, Menu} from 'antd';
import Glyph, {IconSize} from './Glyph'; import Glyph, {IconSize} from './Glyph';
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, getFlipperLib} from 'flipper-plugin';
type ButtonType = 'primary' | 'success' | 'warning' | 'danger'; type ButtonType = 'primary' | 'success' | 'warning' | 'danger';
@@ -124,7 +124,7 @@ function SandyButton({
} }
onClick?.(e); onClick?.(e);
if (href != null) { if (href != null) {
electron.shell.openExternal(href); // TODO: decouple from Electron getFlipperLib().openLink(href);
} }
}, },
[disabled, onClick, href], [disabled, onClick, href],

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import {shell} from 'electron'; import {getFlipperLib} from 'flipper-plugin';
import {Typography} from 'antd'; import {Typography} from 'antd';
const AntOriginalLink = Typography.Link; const AntOriginalLink = Typography.Link;
@@ -15,7 +15,7 @@ const AntOriginalLink = Typography.Link;
// used by patch for Typography.Link in AntD // used by patch for Typography.Link in AntD
// @ts-ignore // @ts-ignore
global.flipperOpenLink = function openLinkExternal(url: string) { global.flipperOpenLink = function openLinkExternal(url: string) {
shell.openExternal(url); getFlipperLib().openLink(url);
}; };
export default AntOriginalLink; export default AntOriginalLink;

View File

@@ -11,7 +11,7 @@ import React, {CSSProperties, ReactNode} from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import {colors} from './colors'; import {colors} from './colors';
import {shell} from 'electron'; import {getFlipperLib} from 'flipper-plugin';
const Container = styled.div({ const Container = styled.div({
padding: 10, padding: 10,
@@ -73,7 +73,9 @@ const Link = styled.span({
}); });
function LinkReference(props: {href: string; children: Array<ReactNode>}) { function LinkReference(props: {href: string; children: Array<ReactNode>}) {
return ( return (
<Link onClick={() => shell.openExternal(props.href)}>{props.children}</Link> <Link onClick={() => getFlipperLib().openLink(props.href)}>
{props.children}
</Link>
); );
} }

View File

@@ -13,7 +13,7 @@ import type {Store} from '../reducers';
import createPaste from '../fb-stubs/createPaste'; import createPaste from '../fb-stubs/createPaste';
import GK from '../fb-stubs/GK'; import GK from '../fb-stubs/GK';
import type BaseDevice from '../devices/BaseDevice'; import type BaseDevice from '../devices/BaseDevice';
import {clipboard} from 'electron'; import {clipboard, shell} from 'electron';
import constants from '../fb-stubs/constants'; import constants from '../fb-stubs/constants';
import {addNotification} from '../reducers/notifications'; import {addNotification} from '../reducers/notifications';
import {deconstructPluginKey} from './clientUtils'; import {deconstructPluginKey} from './clientUtils';
@@ -50,6 +50,9 @@ export function initializeFlipperLibImplementation(
writeTextToClipboard(text: string) { writeTextToClipboard(text: string) {
clipboard.writeText(text); clipboard.writeText(text);
}, },
openLink(url: string) {
shell.openExternal(url);
},
showNotification(pluginId, notification) { showNotification(pluginId, notification) {
const parts = deconstructPluginKey(pluginId); const parts = deconstructPluginKey(pluginId);
store.dispatch( store.dispatch(

View File

@@ -7,13 +7,15 @@
* @format * @format
*/ */
import {getFlipperLib} from 'flipper-plugin';
import {getPreferredEditorUriScheme} from '../fb-stubs/user'; import {getPreferredEditorUriScheme} from '../fb-stubs/user';
import {shell} from 'electron';
let preferredEditorUriScheme: string | undefined = undefined; let preferredEditorUriScheme: string | undefined = undefined;
export function callVSCode(plugin: string, command: string, params?: string) { export function callVSCode(plugin: string, command: string, params?: string) {
getVSCodeUrl(plugin, command, params).then((url) => shell.openExternal(url)); getVSCodeUrl(plugin, command, params).then((url) =>
getFlipperLib().openLink(url),
);
} }
export async function getVSCodeUrl( export async function getVSCodeUrl(

View File

@@ -52,6 +52,7 @@ test('Correct top level API exposed', () => {
"createDataSource", "createDataSource",
"createState", "createState",
"createTablePlugin", "createTablePlugin",
"getFlipperLib",
"produce", "produce",
"renderReactRoot", "renderReactRoot",
"sleep", "sleep",

View File

@@ -37,7 +37,7 @@ export {createState, useValue, Atom} from './state/atom';
export {batch} from './state/batch'; export {batch} from './state/batch';
export { export {
FlipperLib, FlipperLib,
getFlipperLibImplementation as _getFlipperLibImplementation, getFlipperLib,
setFlipperLibImplementation as _setFlipperLibImplementation, setFlipperLibImplementation as _setFlipperLibImplementation,
} from './plugin/FlipperLib'; } from './plugin/FlipperLib';
export { export {

View File

@@ -30,19 +30,20 @@ export interface FlipperLib {
deeplink: unknown, deeplink: unknown,
): void; ): void;
writeTextToClipboard(text: string): void; writeTextToClipboard(text: string): void;
openLink(url: string): void;
showNotification(pluginKey: string, notification: Notification): void; showNotification(pluginKey: string, notification: Notification): void;
DetailsSidebarImplementation?( DetailsSidebarImplementation?(
props: DetailSidebarProps, props: DetailSidebarProps,
): React.ReactElement | null; ): React.ReactElement | null;
} }
let flipperLibInstance: FlipperLib | undefined; export let flipperLibInstance: FlipperLib | undefined;
export function tryGetFlipperLibImplementation(): FlipperLib | undefined { export function tryGetFlipperLibImplementation(): FlipperLib | undefined {
return flipperLibInstance; return flipperLibInstance;
} }
export function getFlipperLibImplementation(): FlipperLib { export function getFlipperLib(): FlipperLib {
if (!flipperLibInstance) { if (!flipperLibInstance) {
throw new Error('Flipper lib not instantiated'); throw new Error('Flipper lib not instantiated');
} }

View File

@@ -374,6 +374,7 @@ export function createMockFlipperLib(options?: StartPluginOptions): FlipperLib {
}, },
selectPlugin: jest.fn(), selectPlugin: jest.fn(),
writeTextToClipboard: jest.fn(), writeTextToClipboard: jest.fn(),
openLink: jest.fn(),
showNotification: jest.fn(), showNotification: jest.fn(),
}; };
} }

View File

@@ -14,7 +14,7 @@ import styled from '@emotion/styled';
import React, {MouseEvent, KeyboardEvent} from 'react'; import React, {MouseEvent, KeyboardEvent} from 'react';
import {theme} from '../theme'; import {theme} from '../theme';
import {Layout} from '../Layout'; import {Layout} from '../Layout';
import {_getFlipperLibImplementation} from 'flipper-plugin'; import {getFlipperLib} from 'flipper-plugin';
import {DownOutlined, RightOutlined} from '@ant-design/icons'; import {DownOutlined, RightOutlined} from '@ant-design/icons';
const {Text} = Typography; const {Text} = Typography;
@@ -221,7 +221,7 @@ class ElementsRow extends PureComponent<ElementsRowProps, ElementsRowState> {
{ {
label: 'Copy', label: 'Copy',
click: () => { click: () => {
_getFlipperLibImplementation()?.writeTextToClipboard( getFlipperLib()?.writeTextToClipboard(
props.onCopyExpandedTree(props.element, 0), props.onCopyExpandedTree(props.element, 0),
); );
}, },
@@ -229,7 +229,7 @@ class ElementsRow extends PureComponent<ElementsRowProps, ElementsRowState> {
{ {
label: 'Copy expanded child elements', label: 'Copy expanded child elements',
click: () => click: () =>
_getFlipperLibImplementation()?.writeTextToClipboard( getFlipperLib()?.writeTextToClipboard(
props.onCopyExpandedTree(props.element, 255), props.onCopyExpandedTree(props.element, 255),
), ),
}, },
@@ -253,7 +253,7 @@ class ElementsRow extends PureComponent<ElementsRowProps, ElementsRowState> {
return { return {
label: `Copy ${o.name}`, label: `Copy ${o.name}`,
click: () => { click: () => {
_getFlipperLibImplementation()?.writeTextToClipboard(o.value); getFlipperLib()?.writeTextToClipboard(o.value);
}, },
}; };
}), }),
@@ -555,9 +555,7 @@ export class Elements extends PureComponent<ElementsProps, ElementsState> {
) { ) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
_getFlipperLibImplementation()?.writeTextToClipboard( getFlipperLib()?.writeTextToClipboard(selectedElement.name);
selectedElement.name,
);
return; return;
} }

View File

@@ -8,8 +8,8 @@
*/ */
import React from 'react'; import React from 'react';
import {shell} from 'electron';
import {styled, colors, FlexRow, Text, GK} from 'flipper'; import {styled, colors, FlexRow, Text, GK} from 'flipper';
import {Typography} from 'antd';
const BannerContainer = styled(FlexRow)({ const BannerContainer = styled(FlexRow)({
height: '30px', height: '30px',
@@ -34,14 +34,6 @@ const BannerLink = styled(CustomLink)({
}, },
}); });
const StyledLink = styled.span({
'&:hover': {
cursor: 'pointer',
},
});
StyledLink.displayName = 'CustomLink:StyledLink';
function CustomLink(props: { function CustomLink(props: {
href: string; href: string;
className?: string; className?: string;
@@ -49,12 +41,12 @@ function CustomLink(props: {
style?: React.CSSProperties; style?: React.CSSProperties;
}) { }) {
return ( return (
<StyledLink <Typography.Link
className={props.className} className={props.className}
onClick={() => shell.openExternal(props.href)} href={props.href}
style={props.style}> style={props.style}>
{props.children || props.href} {props.children || props.href}
</StyledLink> </Typography.Link>
); );
} }

View File

@@ -935,6 +935,13 @@ See `View > Flipper Style Guide` inside the Flipper application for more details
## Utilities ## Utilities
### getFlipperLib
A set of globally available utilities like opening links, interacting with the clipboard, etc.
Example: `getFlipperLib().writeTextToClipboard("hello from Flipper");
The full set of utilities can be found [here](https://github.com/facebook/flipper/blob/master/desktop/flipper-plugin/src/plugin/FlipperLib.tsx#L20)
### createTablePlugin ### createTablePlugin
Utility to create a plugin that consists of a master table and details JSON view with minimal effort. See the [Showing a table](../tutorial/js-table.mdx) tutorial for an example. Utility to create a plugin that consists of a master table and details JSON view with minimal effort. See the [Showing a table](../tutorial/js-table.mdx) tutorial for an example.