Files
flipper/src/plugin.tsx
Pascal Hartig 6c7aeef7ed Migrate exportMetrics
Summary: _typescript_

Reviewed By: jknoxville

Differential Revision: D16712087

fbshipit-source-id: 63dc45a26eb8112aef2b1bfa0d67bf57aa9ec72a
2019-08-12 03:53:16 -07:00

240 lines
6.2 KiB
TypeScript

/**
* Copyright 2018-present Facebook.
* 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.js';
import {App} from './App.js';
import {Logger} from './fb-interfaces/Logger.js';
import Client from './Client';
import {Store, MiddlewareAPI} from './reducers/index';
import {MetricType} from './utils/exportMetrics';
import {ReactNode, Component} from 'react';
import BaseDevice from './devices/BaseDevice';
// 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,
): (string, params: Object | null) => Promise<Object> {
return (method, params) => client.call(id, method, false, params);
}
export interface PluginClient {
// eslint-disable-next-line
send(method: string, params?: Object): void;
// eslint-disable-next-line
call(method: string, params?: Object): 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: string | null;
selectPlugin: (pluginID: string, deepLinkPayload: string | null) => boolean;
isArchivedDevice: boolean;
selectedApp: string | null;
};
export type BaseAction = {
type: string;
};
export abstract class FlipperBasePlugin<
State,
Actions extends BaseAction,
PersistedState
> extends Component<Props<PersistedState>, State> {
abstract ['constructor']: any;
static title: string | null = null;
static id: string = '';
static icon: string | null = null;
static gatekeeper: string | null = null;
static entry: string | null = null;
static bugs:
| ({
email?: string;
url?: string;
})
| null = null;
static keyboardActions: KeyboardActions | null;
static screenshot: string | null;
static defaultPersistedState: any;
static persistedStateReducer:
| (<U>(persistedState: U, method: string, data: Object) => Partial<U>)
| null;
static metricsReducer: (<U>(persistedState: U) => Promise<MetricType>) | null;
static exportPersistedState:
| (<U>(
callClient: (string, params: Object | null) => Promise<Object>,
persistedState: U | null,
store: MiddlewareAPI | null,
) => Promise<U>)
| null;
static getActiveNotifications:
| (<U>(persistedState: U) => Array<Notification>)
| null;
static onRegisterDevice:
| (<U>(
store: Store,
baseDevice: BaseDevice,
setPersistedState: (
pluginKey: string,
newPluginState: U | null,
) => void,
) => void)
| null;
// forbid instance properties that should be static
title: never;
id: never;
persist: never;
icon: never;
keyboardActions: never;
screenshot: never;
reducers: {
[actionName: string]: (state: State, actionData: Object) => Partial<State>;
} = {};
app: App;
onKeyboardAction: ((action: string) => void) | null;
toJSON() {
return `<${this.constructor.name}#${this.constructor.id}>`;
}
// methods to be overriden by plugins
init(): void {}
teardown(): void {}
computeNotifications(
_props: Props<PersistedState>,
_state: State,
): Array<Notification> {
return [];
}
// methods to be overridden by subclasses
_init(): void {}
_teardown(): void {}
dispatchAction(actionData: Actions) {
// $FlowFixMe
const action = this.reducers[actionData.type];
if (!action) {
// $FlowFixMe
throw new ReferenceError(`Unknown action ${actionData.type}`);
}
if (typeof action === 'function') {
this.setState(action.call(this, this.state, actionData));
} else {
// $FlowFixMe
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);
// @ts-ignore props.target will be instance of Device
this.device = props.target;
}
_init() {
this.init();
}
_teardown() {
this.teardown();
}
static supportsDevice(_device: BaseDevice) {
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);
const {id} = this.constructor;
this.subscriptions = [];
// @ts-ignore props.target will be instance of Client
this.realClient = props.target;
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
for (const {method, callback} of this.subscriptions) {
this.realClient.unsubscribe(this.constructor.id, method, callback);
}
// run plugin teardown
this.teardown();
if (this.realClient.connected) {
this.realClient.deinitPlugin(this.constructor.id);
}
}
_init() {
this.realClient.initPlugin(this.constructor.id);
this.init();
}
}