Files
flipper/desktop/app/src/plugin.tsx
Michel Weststrate 9deed974be Support active sheet mechanism in Sandy
Summary:
This diffs adds support for the activeSheets mechanism in Sandy. It should be removed in the future (see T78696648) since open a dialog and keeping dialog state locally results in much more straight forward code, but supporting this for now makes sure that old flows are still supported.

With this change the plugin selection during a Flipper export for example wouldn't become visible

Reviewed By: cekkaewnumchai

Differential Revision: D24620074

fbshipit-source-id: f0558f5738e86a84a5cd0b9d574a3cfd0a3bf424
2020-10-30 04:23:43 -07:00

313 lines
8.5 KiB
TypeScript

/**
* 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 {KeyboardActions} from './MenuBar';
import {Logger} from './fb-interfaces/Logger';
import Client from './Client';
import {Store} from './reducers/index';
import {MetricType} from './utils/exportMetrics';
import {ReactNode, Component} from 'react';
import BaseDevice from './devices/BaseDevice';
import {serialize, deserialize} from './utils/serialization';
import {Idler} from './utils/Idler';
import {StaticView} from './reducers/connections';
import {State as ReduxState} from './reducers';
import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue';
import {PluginDetails} from 'flipper-plugin-lib';
import {Settings} from './reducers/settings';
import {SandyPluginDefinition} from 'flipper-plugin';
type Parameters = {[key: string]: any};
export type PluginDefinition = ClientPluginDefinition | DevicePluginDefinition;
export type DevicePluginDefinition =
| typeof FlipperDevicePlugin
| SandyPluginDefinition;
export type ClientPluginDefinition =
| typeof FlipperPlugin
| SandyPluginDefinition;
export type ClientPluginMap = Map<string, ClientPluginDefinition>;
export type DevicePluginMap = Map<string, DevicePluginDefinition>;
export function isSandyPlugin(
plugin?: PluginDefinition | null,
): plugin is SandyPluginDefinition {
return plugin instanceof SandyPluginDefinition;
}
// This function is intended to be called from outside of the plugin.
// If you want to `call` from the plugin use, this.client.call
export function callClient(
client: Client,
id: string,
): (method: string, params: Parameters) => Promise<any> {
return (method, params) => client.call(id, method, false, params);
}
// This function is intended to be called from outside of the plugin.
// If you want to `supportsMethod` from the plugin use, this.client.supportsMethod
export function supportsMethod(
client: Client,
id: string,
): (method: string) => Promise<boolean> {
return (method) => client.supportsMethod(id, method);
}
export interface PluginClient {
// eslint-disable-next-line
send(method: string, params?: Parameters): void;
// eslint-disable-next-line
call(method: string, params?: Parameters): Promise<any>;
// eslint-disable-next-line
subscribe(method: string, callback: (params: any) => void): void;
// eslint-disable-next-line
supportsMethod(method: string): Promise<boolean>;
}
type PluginTarget = BaseDevice | Client;
export type Notification = {
id: string;
title: string;
message: string | ReactNode;
severity: 'warning' | 'error';
timestamp?: number;
category?: string;
action?: string;
};
export type Props<T> = {
logger: Logger;
persistedState: T;
setPersistedState: (state: Partial<T>) => void;
target: PluginTarget;
deepLinkPayload: unknown;
selectPlugin: (pluginID: string, deepLinkPayload: unknown) => boolean;
isArchivedDevice: boolean;
selectedApp: string | null;
setStaticView: (payload: StaticView) => void;
settingsState: Settings;
};
export type BaseAction = {
type: string;
};
export type PersistedStateReducer = (
persistedState: StaticPersistedState,
method: string,
data: any,
) => StaticPersistedState;
type StaticPersistedState = any;
export abstract class FlipperBasePlugin<
State,
Actions extends BaseAction,
PersistedState
> extends Component<Props<PersistedState>, State> {
abstract ['constructor']: any;
static title: string | null = null;
static category: string | null = null;
static id: string = '';
static packageName: string = '';
static version: string = '';
static icon: string | null = null;
static gatekeeper: string | null = null;
static entry: string | null = null;
static isDefault: boolean;
static details: PluginDetails;
static keyboardActions: KeyboardActions | null;
static screenshot: string | null;
static defaultPersistedState: any;
static persistedStateReducer: PersistedStateReducer | null;
static maxQueueSize: number = DEFAULT_MAX_QUEUE_SIZE;
static metricsReducer:
| ((persistedState: StaticPersistedState) => Promise<MetricType>)
| undefined;
static exportPersistedState:
| ((
callClient: (method: string, params?: any) => Promise<any>,
persistedState: StaticPersistedState | undefined,
store: ReduxState | undefined,
idler?: Idler,
statusUpdate?: (msg: string) => void,
supportsMethod?: (method: string) => Promise<boolean>,
) => Promise<StaticPersistedState | undefined>)
| undefined;
static getActiveNotifications:
| ((persistedState: StaticPersistedState) => Array<Notification>)
| undefined;
static onRegisterDevice:
| ((
store: Store,
baseDevice: BaseDevice,
setPersistedState: (
pluginKey: string,
newPluginState: StaticPersistedState | null,
) => void,
) => void)
| null;
reducers: {
[actionName: string]: (state: State, actionData: any) => Partial<State>;
} = {};
onKeyboardAction: ((action: string) => void) | undefined;
toJSON() {
return `<${this.constructor.name}#${this.constructor.id}>`;
}
// methods to be overriden by plugins
init(): void {}
static serializePersistedState: (
persistedState: StaticPersistedState,
statusUpdate?: (msg: string) => void,
idler?: Idler,
pluginName?: string,
) => Promise<string> = (
persistedState: StaticPersistedState,
statusUpdate?: (msg: string) => void,
idler?: Idler,
pluginName?: string,
) => {
return serialize(
persistedState,
idler,
statusUpdate,
pluginName != null ? `Serializing ${pluginName}` : undefined,
);
};
static deserializePersistedState: (
serializedString: string,
) => StaticPersistedState = (serializedString: string) => {
return deserialize(serializedString);
};
teardown(): void {}
computeNotifications(
_props: Props<PersistedState>,
_state: State,
): Array<Notification> {
return [];
}
// methods to be overridden by subclasses
_init(): void {}
_teardown(): void {}
dispatchAction(actionData: Actions) {
const action = this.reducers[actionData.type];
if (!action) {
throw new ReferenceError(`Unknown action ${actionData.type}`);
}
if (typeof action === 'function') {
this.setState(action.call(this, this.state, actionData) as State);
} else {
throw new TypeError(`Reducer ${actionData.type} isn't a function`);
}
}
}
export class FlipperDevicePlugin<
S,
A extends BaseAction,
P
> extends FlipperBasePlugin<S, A, P> {
['constructor']: typeof FlipperPlugin;
device: BaseDevice;
constructor(props: Props<P>) {
super(props);
this.device = props.target as BaseDevice;
}
_init() {
this.init();
}
_teardown() {
this.teardown();
}
static supportsDevice(_device: BaseDevice): boolean {
throw new Error(
'supportsDevice is unimplemented in FlipperDevicePlugin class',
);
}
}
export class FlipperPlugin<
S,
A extends BaseAction,
P
> extends FlipperBasePlugin<S, A, P> {
['constructor']: typeof FlipperPlugin;
constructor(props: Props<P>) {
super(props);
// @ts-ignore constructor should be assigned already
const {id} = this.constructor;
this.subscriptions = [];
this.realClient = props.target as Client;
this.client = {
call: (method, params) => this.realClient.call(id, method, true, params),
send: (method, params) => this.realClient.send(id, method, params),
subscribe: (method, callback) => {
this.subscriptions.push({
method,
callback,
});
this.realClient.subscribe(id, method, callback);
},
supportsMethod: (method) => this.realClient.supportsMethod(id, method),
};
}
subscriptions: Array<{
method: string;
callback: Function;
}>;
client: PluginClient;
realClient: Client;
getDevice(): Promise<BaseDevice> {
return this.realClient.device;
}
_teardown() {
// automatically unsubscribe subscriptions
const pluginId = this.constructor.id;
for (const {method, callback} of this.subscriptions) {
this.realClient.unsubscribe(pluginId, method, callback);
}
// run plugin teardown
this.teardown();
if (!this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.deinitPlugin(pluginId);
}
}
_init() {
const pluginId = this.constructor.id;
if (!this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.initPlugin(pluginId);
}
this.init();
}
}