Use selectors to compute plugin lists according to the selected device and app

Summary: Use selectors to re-compute and cache plugin lists according to the selected device and app.

Reviewed By: mweststrate

Differential Revision: D29247845

fbshipit-source-id: 4bc669d5d441d605c4090086c4ce59b6d9684a4c
This commit is contained in:
Anton Nikolaev
2021-06-29 13:00:18 -07:00
committed by Facebook GitHub Bot
parent 1d26faeacb
commit ff5d8ba29f
20 changed files with 294 additions and 358 deletions

View File

@@ -65,6 +65,7 @@
"recursive-readdir": "^2.2.2",
"redux": "^4.1.0",
"redux-persist": "^6.0.0",
"reselect": "^4.0.0",
"rsocket-core": "^0.0.19",
"rsocket-flowable": "^0.0.25",
"rsocket-tcp-server": "^0.0.25",

View File

@@ -2,22 +2,6 @@
exports[`can create a Fake flipper 1`] = `
Object {
"activeClient": Object {
"id": "TestApp#Android#MockAndroidDevice#serial",
"query": Object {
"app": "TestApp",
"device": "MockAndroidDevice",
"device_id": "serial",
"os": "Android",
"sdk_version": 4,
},
},
"activeDevice": Object {
"deviceType": "physical",
"os": "Android",
"serial": "serial",
"title": "MockAndroidDevice",
},
"androidEmulators": Array [],
"clients": Array [
Object {
@@ -52,8 +36,8 @@ Object {
"TestPlugin",
],
},
"metroDevice": null,
"selectedApp": "TestApp#Android#MockAndroidDevice#serial",
"selectedAppPluginListRevision": 0,
"selectedDevice": Object {
"deviceType": "physical",
"os": "Android",
@@ -90,22 +74,6 @@ Object {
exports[`can create a Fake flipper with legacy wrapper 1`] = `
Object {
"activeClient": Object {
"id": "TestApp#Android#MockAndroidDevice#serial",
"query": Object {
"app": "TestApp",
"device": "MockAndroidDevice",
"device_id": "serial",
"os": "Android",
"sdk_version": 4,
},
},
"activeDevice": Object {
"deviceType": "physical",
"os": "Android",
"serial": "serial",
"title": "MockAndroidDevice",
},
"androidEmulators": Array [],
"clients": Array [
Object {
@@ -140,8 +108,8 @@ Object {
"TestPlugin",
],
},
"metroDevice": null,
"selectedApp": "TestApp#Android#MockAndroidDevice#serial",
"selectedAppPluginListRevision": 0,
"selectedDevice": Object {
"deviceType": "physical",
"os": "Android",

View File

@@ -13,7 +13,6 @@ import {ShareType} from '../reducers/application';
import {State as Store} from '../reducers';
import {ActiveSheet} from '../reducers/application';
import {selectedPlugins as actionForSelectedPlugins} from '../reducers/plugins';
import {getExportablePlugins} from '../utils/pluginUtils';
import {
ACTIVE_SHEET_SHARE_DATA,
setActiveSheet as getActiveSheetAction,
@@ -23,6 +22,7 @@ import ListView from './ListView';
import {Dispatch, Action} from 'redux';
import {unsetShare} from '../reducers/application';
import {FlexColumn, styled} from '../ui';
import {getExportablePlugins} from '../selectors/connections';
type OwnProps = {
onHide: () => void;
@@ -104,14 +104,7 @@ class ExportDataPluginSheet extends Component<Props, {}> {
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
(state) => {
const selectedClient = state.connections.clients.find((o) => {
return o.id === state.connections.selectedApp;
});
const availablePluginsToExport = getExportablePlugins(
state,
state.connections.selectedDevice ?? undefined,
selectedClient,
);
const availablePluginsToExport = getExportablePlugins(state);
return {
share: state.application.share,
selectedPlugins: state.plugins.selectedPlugins,

View File

@@ -12,9 +12,10 @@ import {create, act, ReactTestRenderer} from 'react-test-renderer';
import {Provider} from 'react-redux';
import ExportDataPluginSheet from '../ExportDataPluginSheet';
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {getExportablePlugins, getPluginKey} from '../../utils/pluginUtils';
import {getPluginKey} from '../../utils/pluginUtils';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {setPluginState} from '../../reducers/pluginStates';
import {getExportablePlugins} from '../../selectors/connections';
class TestPlugin extends FlipperPlugin<any, any, any> {
static details = {
@@ -56,7 +57,7 @@ test('SettingsSheet snapshot with nothing enabled', async () => {
}),
);
expect(getExportablePlugins(store.getState(), device, client)).toEqual([]);
expect(getExportablePlugins(store.getState())).toEqual([]);
// makes device plugin visible
store.dispatch(
@@ -66,7 +67,7 @@ test('SettingsSheet snapshot with nothing enabled', async () => {
}),
);
expect(getExportablePlugins(store.getState(), device, client)).toEqual([
expect(getExportablePlugins(store.getState())).toEqual([
{
id: 'TestDevicePlugin',
label: 'TestDevicePlugin',
@@ -86,7 +87,7 @@ test('SettingsSheet snapshot with nothing enabled', async () => {
test('SettingsSheet snapshot with one plugin enabled', async () => {
let root: ReactTestRenderer;
const {store, device, client, pluginKey} = await createMockFlipperWithPlugin(
const {store, device, pluginKey} = await createMockFlipperWithPlugin(
TestPlugin,
{
additionalPlugins: [TestDevicePlugin],
@@ -96,7 +97,7 @@ test('SettingsSheet snapshot with one plugin enabled', async () => {
// enabled
// in Sandy wrapper, a plugin is either persistable or not, but it doesn't depend on the current state.
// So this plugin will show up, even though its state is still the default
expect(getExportablePlugins(store.getState(), device, client)).toEqual([
expect(getExportablePlugins(store.getState())).toEqual([
{
id: 'TestPlugin',
label: 'TestPlugin',
@@ -115,7 +116,7 @@ test('SettingsSheet snapshot with one plugin enabled', async () => {
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState(), device, client)).toEqual([
expect(getExportablePlugins(store.getState())).toEqual([
{
id: 'TestDevicePlugin',
label: 'TestDevicePlugin',

View File

@@ -14,6 +14,8 @@ import {switchPlugin} from '../../reducers/pluginManager';
import {isPluginEnabled} from '../../reducers/connections';
import {theme} from 'flipper-plugin';
import {PluginDefinition} from '../../plugin';
import {useSelector} from 'react-redux';
import {getActiveClient} from '../../selectors/connections';
const Waiting = styled(Layout.Container)({
width: '100%',
@@ -30,7 +32,7 @@ export default function PluginInfo() {
const enabledDevicePlugins = useStore(
(state) => state.connections.enabledDevicePlugins,
);
const activeClient = useStore((state) => state.connections.activeClient);
const activeClient = useSelector(getActiveClient);
const clientPlugins = useStore((state) => state.plugins.clientPlugins);
const devicePlugins = useStore((state) => state.plugins.devicePlugins);
const selectedClientId = activeClient?.id ?? null;

View File

@@ -23,7 +23,7 @@ import reactNative from './reactNative';
import pluginMarketplace from './fb-stubs/pluginMarketplace';
import pluginDownloads from './pluginDownloads';
import info from '../utils/info';
import pluginLists from './pluginLists';
import pluginChangeListener from './pluginsChangeListener';
import {Logger} from '../fb-interfaces/Logger';
import {Store} from '../reducers/index';
@@ -52,7 +52,7 @@ export default function (store: Store, logger: Logger): () => Promise<void> {
pluginMarketplace,
pluginDownloads,
info,
pluginLists,
pluginChangeListener,
].filter(notNull);
const globalCleanup = dispatchers
.map((dispatcher) => dispatcher(store, logger))

View File

@@ -1,120 +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 Client from '../Client';
import {Logger} from '../fb-interfaces/Logger';
import {Store} from '../reducers';
import {pluginListsChanged} from '../reducers/pluginLists';
import {computePluginLists} from '../utils/pluginUtils';
import {sideEffect} from '../utils/sideEffect';
export default (store: Store, _logger: Logger) => {
const recomputePluginList = () => {
store.dispatch(
pluginListsChanged(
computePluginLists(
store.getState().connections,
store.getState().plugins,
),
),
);
};
let prevClient: null | Client = null;
sideEffect(
store,
{name: 'computePluginLists', throttleMs: 100, fireImmediately: true},
(state) => {
const {
activeClient,
activeDevice,
metroDevice,
enabledDevicePlugins,
enabledPlugins,
} = state.connections;
const {
bundledPlugins,
marketplacePlugins,
loadedPlugins,
devicePlugins,
disabledPlugins,
gatekeepedPlugins,
failedPlugins,
clientPlugins,
} = state.plugins;
return {
activeClient,
activeDevice,
metroDevice,
enabledDevicePlugins,
enabledPlugins,
bundledPlugins,
marketplacePlugins,
loadedPlugins,
devicePlugins,
disabledPlugins,
gatekeepedPlugins,
failedPlugins,
clientPlugins,
};
},
(
{
activeClient,
activeDevice,
metroDevice,
enabledDevicePlugins,
enabledPlugins,
bundledPlugins,
marketplacePlugins,
loadedPlugins,
devicePlugins,
disabledPlugins,
gatekeepedPlugins,
failedPlugins,
clientPlugins,
},
store,
) => {
store.dispatch(
pluginListsChanged(
computePluginLists(
{
activeClient,
activeDevice,
metroDevice,
enabledDevicePlugins,
enabledPlugins,
},
{
bundledPlugins,
marketplacePlugins,
loadedPlugins,
devicePlugins,
disabledPlugins,
gatekeepedPlugins,
failedPlugins,
clientPlugins,
},
),
),
);
if (activeClient !== prevClient) {
if (prevClient) {
prevClient.off('plugins-change', recomputePluginList);
}
prevClient = activeClient;
if (prevClient) {
prevClient.on('plugins-change', recomputePluginList);
}
}
},
);
};

View File

@@ -0,0 +1,40 @@
/**
* 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 Client from '../Client';
import {Logger} from '../fb-interfaces/Logger';
import {Store} from '../reducers';
import {appPluginListChanged} from '../reducers/connections';
import {getActiveClient} from '../selectors/connections';
import {sideEffect} from '../utils/sideEffect';
export default (store: Store, _logger: Logger) => {
let prevClient: null | Client = null;
const onActiveAppPluginListChanged = () => {
store.dispatch(appPluginListChanged());
};
sideEffect(
store,
{name: 'pluginsChangeListener', throttleMs: 100, fireImmediately: true},
getActiveClient,
(activeClient, _store) => {
if (activeClient !== prevClient) {
if (prevClient) {
prevClient.off('plugins-change', onActiveAppPluginListChanged);
}
prevClient = activeClient;
if (prevClient) {
prevClient.on('plugins-change', onActiveAppPluginListChanged);
}
}
},
);
};

View File

@@ -78,9 +78,7 @@ type StateV2 = {
}>;
deepLinkPayload: unknown;
staticView: StaticView;
activeClient: Client | null;
activeDevice: BaseDevice | null;
metroDevice: MetroDevice | null;
selectedAppPluginListRevision: number;
};
type StateV1 = Omit<StateV2, 'enabledPlugins' | 'enabledDevicePlugins'> & {
@@ -179,6 +177,9 @@ export type Action =
type: 'SELECT_CLIENT';
payload: string | null;
}
| {
type: 'APP_PLUGIN_LIST_CHANGED';
}
| RegisterPluginAction;
const DEFAULT_PLUGIN = 'DeviceLogs';
@@ -204,9 +205,7 @@ const INITAL_STATE: State = {
uninitializedClients: [],
deepLinkPayload: null,
staticView: WelcomeScreenStaticView,
activeClient: null,
activeDevice: null,
metroDevice: null,
selectedAppPluginListRevision: 0,
};
export default (state: State = INITAL_STATE, action: Actions): State => {
@@ -469,6 +468,11 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
draft.enabledDevicePlugins.delete(pluginId);
});
}
case 'APP_PLUGIN_LIST_CHANGED': {
return produce(state, (draft) => {
draft.selectedAppPluginListRevision++;
});
}
default:
return state;
}
@@ -544,6 +548,10 @@ export const setPluginDisabled = (pluginId: string, appId: string): Action => ({
},
});
export const appPluginListChanged = (): Action => ({
type: 'APP_PLUGIN_LIST_CHANGED',
});
export function getAvailableClients(
device: null | undefined | BaseDevice,
clients: Client[],
@@ -568,10 +576,10 @@ function getBestAvailableClient(
device: BaseDevice | null | undefined,
clients: Client[],
preferredClient: string | null,
): Client | undefined {
): Client | null {
const availableClients = getAvailableClients(device, clients);
if (availableClients.length === 0) {
return undefined;
return null;
}
return (
getClientById(availableClients, preferredClient) ||
@@ -625,29 +633,12 @@ function updateSelection(state: Readonly<State>): State {
}
// Select client based on device
updates.activeClient = getBestAvailableClient(
const client = getBestAvailableClient(
device,
state.clients,
state.selectedApp || state.userPreferredApp,
);
updates.selectedApp = updates.activeClient ? updates.activeClient.id : null;
updates.metroDevice =
(state.devices?.find(
(device) => device.os === 'Metro' && !device.isArchived,
) as MetroDevice) ?? null;
updates.activeClient =
state.clients.find(
(c) => c.id === (updates.selectedApp || state.userPreferredApp),
) ?? null;
// if the selected device is Metro, we want to keep the owner of the selected App as active device if possible
updates.activeDevice = findBestDevice(
state,
updates.activeClient,
updates.metroDevice,
);
updates.selectedApp = client ? client.id : null;
if (
// Try the preferred plugin first
@@ -667,31 +658,6 @@ function updateSelection(state: Readonly<State>): State {
return {...state, ...updates};
}
export function findBestDevice(
state: State,
client: Client | null,
metroDevice: BaseDevice | null,
): BaseDevice | null {
// if not Metro device, use the selected device as metro device
const selected = state.selectedDevice ?? null;
if (selected !== metroDevice) {
return selected;
}
// if there is an active app, use device owning the app
if (client) {
return client.deviceSync;
}
// if no active app, use the preferred device
if (state.userPreferredDevice) {
return (
state.devices.find(
(device) => device.title === state.userPreferredDevice,
) ?? selected
);
}
return selected;
}
export function getSelectedPluginKey(state: State): string | undefined {
return state.selectedPlugin
? getPluginKey(

View File

@@ -64,10 +64,6 @@ import usageTracking, {
Action as TrackingAction,
State as TrackingState,
} from './usageTracking';
import pluginLists, {
State as PluginListsState,
Action as PluginListsAction,
} from './pluginLists';
import user, {State as UserState, Action as UserAction} from './user';
import JsonFileStorage from '../utils/jsonFileReduxPersistStorage';
import LauncherSettingsStorage from '../utils/launcherSettingsStorage';
@@ -97,7 +93,6 @@ export type Actions =
| HealthcheckAction
| TrackingAction
| PluginDownloadsAction
| PluginListsAction
| {type: 'INIT'};
export type State = {
@@ -115,7 +110,6 @@ export type State = {
healthchecks: HealthcheckState & PersistPartial;
usageTracking: TrackingState;
pluginDownloads: PluginDownloadsState;
pluginLists: PluginListsState;
};
export type Store = ReduxStore<State, Actions>;
@@ -218,6 +212,5 @@ export function createRootReducer() {
),
usageTracking,
pluginDownloads,
pluginLists,
});
}

View File

@@ -1,68 +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 {
PluginDetails,
DownloadablePluginDetails,
BundledPluginDetails,
} from 'flipper-plugin-lib';
import {Actions} from '.';
import {
DevicePluginDefinition,
ClientPluginDefinition,
PluginDefinition,
} from '../plugin';
import produce from 'immer';
export type State = {
devicePlugins: DevicePluginDefinition[];
metroPlugins: DevicePluginDefinition[];
enabledPlugins: ClientPluginDefinition[];
disabledPlugins: PluginDefinition[];
unavailablePlugins: [plugin: PluginDetails, reason: string][];
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
};
const INITIAL_STATE: State = {
devicePlugins: [],
metroPlugins: [],
enabledPlugins: [],
disabledPlugins: [],
unavailablePlugins: [],
downloadablePlugins: [],
};
export type Action = {
type: 'PLUGIN_LISTS_CHANGED';
payload: State;
};
export default function reducer(
state: State | undefined = INITIAL_STATE,
action: Actions,
): State {
if (action.type === 'PLUGIN_LISTS_CHANGED') {
const payload = action.payload;
return produce(state, (draft) => {
draft.devicePlugins = payload.devicePlugins;
draft.metroPlugins = payload.metroPlugins;
draft.enabledPlugins = payload.enabledPlugins;
draft.disabledPlugins = payload.disabledPlugins;
draft.unavailablePlugins = payload.unavailablePlugins;
draft.downloadablePlugins = payload.downloadablePlugins;
});
} else {
return state;
}
}
export const pluginListsChanged = (payload: State): Action => ({
type: 'PLUGIN_LISTS_CHANGED',
payload,
});

View File

@@ -17,10 +17,9 @@ import {addStatusMessage, removeStatusMessage} from './application';
import constants from '../fb-stubs/constants';
import {getInstance} from '../fb-stubs/Logger';
import {logPlatformSuccessRate} from '../utils/metrics';
import {getExportablePlugins} from '../utils/pluginUtils';
export const SUPPORT_FORM_PREFIX = 'support-form-v2';
import Client from '../Client';
import BaseDevice, {OS} from '../devices/BaseDevice';
import {OS} from '../devices/BaseDevice';
import {getExportablePlugins} from '../selectors/connections';
const {DEFAULT_SUPPORT_GROUP} = constants;
@@ -193,11 +192,7 @@ export class Group {
selectedGroup: this,
}),
);
const pluginsList = getExportablePlugins(
store.getState(),
store.getState().connections.selectedDevice ?? undefined,
selectedClient,
);
const pluginsList = getExportablePlugins(store.getState());
store.dispatch(
setSelectedPlugins(
@@ -220,10 +215,8 @@ export class Group {
getWarningMessage(
state: Parameters<typeof getExportablePlugins>[0],
device: BaseDevice | undefined,
client: Client,
): string | null {
const activePersistentPlugins = getExportablePlugins(state, device, client);
const activePersistentPlugins = getExportablePlugins(state);
const emptyPlugins: Array<string> = [];
for (const plugin of this.requiredPlugins) {
if (

View File

@@ -13,7 +13,6 @@ import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
import {Layout, Link, styled} from '../../ui';
import {theme, useValue} from 'flipper-plugin';
import {AppSelector} from './AppSelector';
import {useStore} from '../../utils/useStore';
import {PluginList} from './PluginList';
import ScreenCaptureButtons from '../../chrome/ScreenCaptureButtons';
import MetroButton from '../../chrome/MetroButton';
@@ -21,6 +20,12 @@ import {BookmarkSection} from './BookmarkSection';
import Client from '../../Client';
import BaseDevice from '../../devices/BaseDevice';
import {ExclamationCircleOutlined, FieldTimeOutlined} from '@ant-design/icons';
import {useSelector} from 'react-redux';
import {
getActiveClient,
getActiveDevice,
getMetroDevice,
} from '../../selectors/connections';
const {Text} = Typography;
@@ -36,11 +41,9 @@ const appTooltip = (
);
export function AppInspect() {
const connections = useStore((state) => state.connections);
const metroDevice = connections.metroDevice;
const client = connections.activeClient;
const activeDevice = connections.activeDevice;
const metroDevice = useSelector(getMetroDevice);
const client = useSelector(getActiveClient);
const activeDevice = useSelector(getActiveDevice);
const isDeviceConnected = useValue(activeDevice?.connected, false);
const isAppConnected = useValue(client?.connected, false);

View File

@@ -39,6 +39,8 @@ import {
import {BundledPluginDetails} from 'flipper-plugin-lib';
import {reportUsage} from '../../utils/metrics';
import ConnectivityStatus from './fb-stubs/ConnectivityStatus';
import {useSelector} from 'react-redux';
import {getPluginLists} from '../../selectors/connections';
const {SubMenu} = Menu;
const {Text} = Typography;
@@ -55,7 +57,7 @@ export const PluginList = memo(function PluginList({
const dispatch = useDispatch();
const connections = useStore((state) => state.connections);
const plugins = useStore((state) => state.plugins);
const pluginLists = useStore((state) => state.pluginLists);
const pluginLists = useSelector(getPluginLists);
const downloads = useStore((state) => state.pluginDownloads);
const isConnected = useValue(activeDevice?.connected, false);
const metroConnected = useValue(metroDevice?.connected, false);

View File

@@ -28,7 +28,12 @@ import {switchPlugin} from '../../../reducers/pluginManager';
// eslint-disable-next-line
import * as LogsPluginModule from '../../../../../plugins/public/logs/index';
import {createMockDownloadablePluginDetails} from '../../../utils/testUtils';
import {computePluginLists} from '../../../utils/pluginUtils';
import {
getActiveClient,
getActiveDevice,
getMetroDevice,
getPluginLists,
} from '../../../selectors/connections';
const createMockPluginDetails = TestUtils.createMockPluginDetails;
@@ -39,32 +44,29 @@ const logsPlugin = new _SandyPluginDefinition(
class TestPlugin extends FlipperPlugin<any, any, any> {}
describe('basic findBestDevice', () => {
describe('basic getActiveDevice', () => {
let flipper: MockFlipperResult;
beforeEach(async () => {
flipper = await createMockFlipperWithPlugin(TestPlugin);
});
test('findBestDevice prefers selected device', () => {
const {device} = flipper;
const {connections} = flipper.store.getState();
expect(connections.activeDevice).toBe(device);
test('getActiveDevice prefers selected device', () => {
const {device, store} = flipper;
expect(getActiveDevice(store.getState())).toBe(device);
});
test('findBestDevice picks device of current client', () => {
const {device} = flipper;
const {connections} = flipper.store.getState();
expect(connections.activeDevice).toBe(device);
test('getActiveDevice picks device of current client', () => {
const {device, store} = flipper;
expect(getActiveDevice(store.getState())).toBe(device);
});
test('findBestDevice picks preferred device if no client and device', () => {
const {device} = flipper;
const {connections} = flipper.store.getState();
expect(connections.activeDevice).toBe(device);
test('getActiveDevice picks preferred device if no client and device', () => {
const {device, store} = flipper;
expect(getActiveDevice(store.getState())).toBe(device);
});
});
describe('basic findBestDevice with metro present', () => {
describe('basic getActiveDevice with metro present', () => {
let flipper: MockFlipperResult;
let metro: MetroDevice;
let testDevice: BaseDevice;
@@ -77,7 +79,7 @@ describe('basic findBestDevice with metro present', () => {
testDevice = flipper.device;
// flipper.store.dispatch(registerPlugins([LogsPlugin]))
await registerMetroDevice(undefined, flipper.store, flipper.logger);
metro = flipper.store.getState().connections.metroDevice!;
metro = getMetroDevice(flipper.store.getState())!;
metro.supportsPlugin = (p) => {
return p.id !== 'unsupportedDevicePlugin';
};
@@ -88,7 +90,8 @@ describe('basic findBestDevice with metro present', () => {
});
test('correct base selection state', () => {
const {connections} = flipper.store.getState();
const state = flipper.store.getState();
const {connections} = state;
expect(connections).toMatchObject({
devices: [testDevice, metro],
selectedDevice: testDevice,
@@ -97,10 +100,11 @@ describe('basic findBestDevice with metro present', () => {
userPreferredPlugin: 'DeviceLogs',
userPreferredApp: 'TestApp#Android#MockAndroidDevice#serial',
});
expect(connections.activeClient).toBe(flipper.client);
expect(getActiveClient(state)).toBe(flipper.client);
});
test('selecting Metro Logs works but keeps normal device preferred', () => {
expect(getActiveClient(flipper.store.getState())).toBe(flipper.client);
flipper.store.dispatch(
selectPlugin({
selectedPlugin: logsPlugin.id,
@@ -118,16 +122,16 @@ describe('basic findBestDevice with metro present', () => {
userPreferredPlugin: 'DeviceLogs',
userPreferredApp: 'TestApp#Android#MockAndroidDevice#serial',
});
const {connections} = flipper.store.getState();
const state = flipper.store.getState();
// find best device is still metro
expect(connections.activeDevice).toBe(testDevice);
expect(getActiveDevice(state)).toBe(testDevice);
// find best client still returns app
expect(connections.activeClient).toBe(flipper.client);
expect(getActiveClient(state)).toBe(flipper.client);
});
test('computePluginLists', () => {
const state = flipper.store.getState();
expect(computePluginLists(state.connections, state.plugins)).toEqual({
expect(getPluginLists(state)).toEqual({
downloadablePlugins: [],
devicePlugins: [logsPlugin],
metroPlugins: [logsPlugin],
@@ -231,7 +235,7 @@ describe('basic findBestDevice with metro present', () => {
]);
let state = flipper.store.getState();
const pluginLists = computePluginLists(state.connections, state.plugins);
const pluginLists = getPluginLists(state);
expect(pluginLists).toEqual({
devicePlugins: [logsPlugin],
metroPlugins: [logsPlugin],
@@ -265,7 +269,7 @@ describe('basic findBestDevice with metro present', () => {
}),
);
state = flipper.store.getState();
expect(computePluginLists(state.connections, state.plugins)).toMatchObject({
expect(getPluginLists(state)).toMatchObject({
enabledPlugins: [plugin2],
disabledPlugins: [plugin1],
});

View File

@@ -0,0 +1,137 @@
/**
* 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 {
PluginDetails,
DownloadablePluginDetails,
BundledPluginDetails,
} from 'flipper-plugin-lib';
import MetroDevice from '../devices/MetroDevice';
import {
DevicePluginDefinition,
ClientPluginDefinition,
PluginDefinition,
} from '../plugin';
import {State} from '../reducers';
import {
computePluginLists,
computeExportablePlugins,
} from '../utils/pluginUtils';
import createSelector from './createSelector';
export type PluginLists = {
devicePlugins: DevicePluginDefinition[];
metroPlugins: DevicePluginDefinition[];
enabledPlugins: ClientPluginDefinition[];
disabledPlugins: PluginDefinition[];
unavailablePlugins: [plugin: PluginDetails, reason: string][];
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
};
const getSelectedApp = (state: State) =>
state.connections.selectedApp || state.connections.userPreferredApp;
const getSelectedDevice = (state: State) => state.connections.selectedDevice;
const getUserPreferredDevice = (state: State) =>
state.connections.userPreferredDevice;
const getClients = (state: State) => state.connections.clients;
const getDevices = (state: State) => state.connections.devices;
export const getActiveClient = createSelector(
getSelectedApp,
getClients,
(selectedApp, clients) => {
return clients.find((c) => c.id === selectedApp) || null;
},
);
export const getMetroDevice = createSelector(getDevices, (devices) => {
return (
(devices.find(
(device) => device.os === 'Metro' && !device.isArchived,
) as MetroDevice) ?? null
);
});
export const getActiveDevice = createSelector(
getSelectedDevice,
getUserPreferredDevice,
getDevices,
getActiveClient,
getMetroDevice,
(selectedDevice, userPreferredDevice, devices, client, metroDevice) => {
// if not Metro device, use the selected device as metro device
if (selectedDevice !== metroDevice) {
return selectedDevice;
}
// if there is an active app, use device owning the app
if (client) {
return client.deviceSync;
}
// if no active app, use the preferred device
if (userPreferredDevice) {
return (
devices.find((device) => device.title === userPreferredDevice) ??
selectedDevice
);
}
return selectedDevice;
},
);
export const getPluginLists = createSelector(
({
connections: {
enabledDevicePlugins,
enabledPlugins,
selectedAppPluginListRevision, // used only to invalidate cache
},
}: State) => ({
enabledDevicePlugins,
enabledPlugins,
selectedAppPluginListRevision,
}),
({
plugins: {
clientPlugins,
devicePlugins,
bundledPlugins,
marketplacePlugins,
loadedPlugins,
disabledPlugins,
gatekeepedPlugins,
failedPlugins,
},
}: State) => ({
clientPlugins,
devicePlugins,
bundledPlugins,
marketplacePlugins,
loadedPlugins,
disabledPlugins,
gatekeepedPlugins,
failedPlugins,
}),
getActiveDevice,
getMetroDevice,
getActiveClient,
computePluginLists,
);
export const getExportablePlugins = createSelector(
({plugins, connections, pluginStates, pluginMessageQueue}: State) => ({
plugins,
connections,
pluginStates,
pluginMessageQueue,
}),
getActiveDevice,
getActiveClient,
getPluginLists,
computeExportablePlugins,
);

View File

@@ -0,0 +1,18 @@
/**
* 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 {shallowEqual} from 'react-redux';
import {createSelectorCreator, defaultMemoize} from 'reselect';
export const createSelector = createSelectorCreator(
defaultMemoize,
shallowEqual,
);
export default createSelector;

View File

@@ -7,9 +7,10 @@
* @format
*/
import {getExportablePlugins, getPluginKey} from '../pluginUtils';
import {getPluginKey} from '../pluginUtils';
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {getExportablePlugins} from '../../selectors/connections';
function createMockFlipperPluginWithDefaultPersistedState(id: string) {
return class MockFlipperPluginWithDefaultPersistedState extends FlipperPlugin<
@@ -89,7 +90,7 @@ test('getActivePersistentPlugins, where the non persistent plugins getting exclu
[getPluginKey(client.id, device, 'ClientPlugin4')]: {msg: 'ClientPlugin2'},
};
const list = getExportablePlugins(state, device, client);
const list = getExportablePlugins(state);
expect(list).toEqual([
{
id: 'ClientPlugin1',
@@ -130,7 +131,7 @@ test('getActivePersistentPlugins, where the plugins not in pluginState or queue
],
};
const list = getExportablePlugins(store.getState(), device, client);
const list = getExportablePlugins(store.getState());
expect(list).toEqual([
{
id: 'ClientPlugin2', // has state

View File

@@ -26,6 +26,7 @@ import type {
PluginDetails,
} from 'flipper-plugin-lib';
import {getLatestCompatibleVersionOfEachPlugin} from '../dispatcher/plugins';
import {PluginLists} from '../selectors/connections';
export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works
@@ -75,15 +76,15 @@ export function getPersistedState<PersistedState>(
return persistedState;
}
export function getExportablePlugins(
export function computeExportablePlugins(
state: Pick<
State,
'plugins' | 'connections' | 'pluginStates' | 'pluginMessageQueue'
>,
device: BaseDevice | undefined | null,
client?: Client,
device: BaseDevice | null,
client: Client | null,
availablePlugins: PluginLists,
): {id: string; label: string}[] {
const availablePlugins = computePluginLists(state.connections, state.plugins);
return [
...availablePlugins.devicePlugins.filter((plugin) => {
return isExportablePlugin(state, device, client, plugin);
@@ -102,8 +103,8 @@ function isExportablePlugin(
pluginStates,
pluginMessageQueue,
}: Pick<State, 'pluginStates' | 'pluginMessageQueue'>,
device: BaseDevice | undefined | null,
client: Client | undefined,
device: BaseDevice | null,
client: Client | null,
plugin: PluginDefinition,
): boolean {
// can generate an export when requested
@@ -174,11 +175,7 @@ export function getPluginTooltip(details: PluginDetails): string {
export function computePluginLists(
connections: Pick<
State['connections'],
| 'activeDevice'
| 'activeClient'
| 'metroDevice'
| 'enabledDevicePlugins'
| 'enabledPlugins'
'enabledDevicePlugins' | 'enabledPlugins'
>,
plugins: Pick<
State['plugins'],
@@ -191,6 +188,9 @@ export function computePluginLists(
| 'failedPlugins'
| 'clientPlugins'
>,
device: BaseDevice | null,
metroDevice: BaseDevice | null,
client: Client | null,
): {
devicePlugins: DevicePluginDefinition[];
metroPlugins: DevicePluginDefinition[];
@@ -199,9 +199,6 @@ export function computePluginLists(
unavailablePlugins: [plugin: PluginDetails, reason: string][];
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
} {
const device = connections.activeDevice;
const client = connections.activeClient;
const metroDevice = connections.metroDevice;
const enabledDevicePluginsState = connections.enabledDevicePlugins;
const enabledPluginsState = connections.enabledPlugins;
const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin([

View File

@@ -12086,6 +12086,11 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
reselect@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"