Move app/src (mostly) to flipper-ui-core/src
Summary: This diff moves all UI code from app/src to app/flipper-ui-core. That is now slightly too much (e.g. node deps are not removed yet), but from here it should be easier to move things out again, as I don't want this diff to be open for too long to avoid too much merge conflicts. * But at least flipper-ui-core is Electron free :) * Killed all cross module imports as well, as they where now even more in the way * Some unit test needed some changes, most not too big (but emotion hashes got renumbered in the snapshots, feel free to ignore that) * Found some files that were actually meaningless (tsconfig in plugins, WatchTools files, that start generating compile errors, removed those Follow up work: * make flipper-ui-core configurable, and wire up flipper-server-core in Electron instead of here * remove node deps (aigoncharov) * figure out correct place to load GKs, plugins, make intern requests etc., and move to the correct module * clean up deps Reviewed By: aigoncharov Differential Revision: D32427722 fbshipit-source-id: 14fe92e1ceb15b9dcf7bece367c8ab92df927a70
This commit is contained in:
committed by
Facebook GitHub Bot
parent
54b7ce9308
commit
7e50c0466a
@@ -0,0 +1,335 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`acknowledgeProblems 1`] = `
|
||||
Object {
|
||||
"acknowledgedProblems": Array [
|
||||
"ios.sdk",
|
||||
"common.openssl",
|
||||
],
|
||||
"healthcheckReport": Object {
|
||||
"categories": Object {
|
||||
"android": Object {
|
||||
"checks": Object {
|
||||
"android.sdk": Object {
|
||||
"key": "android.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "android",
|
||||
"label": "Android",
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
"common": Object {
|
||||
"checks": Object {
|
||||
"common.openssl": Object {
|
||||
"key": "common.openssl",
|
||||
"label": "OpenSSL Istalled",
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "common",
|
||||
"label": "Common",
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
"ios": Object {
|
||||
"checks": Object {
|
||||
"ios.sdk": Object {
|
||||
"key": "ios.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "ios",
|
||||
"label": "iOS",
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
},
|
||||
"result": Object {
|
||||
"isAcknowledged": true,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`finish 1`] = `
|
||||
Object {
|
||||
"acknowledgedProblems": Array [],
|
||||
"healthcheckReport": Object {
|
||||
"categories": Object {
|
||||
"android": Object {
|
||||
"checks": Object {
|
||||
"android.sdk": Object {
|
||||
"key": "android.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "android",
|
||||
"label": "Android",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
"common": Object {
|
||||
"checks": Object {
|
||||
"common.openssl": Object {
|
||||
"key": "common.openssl",
|
||||
"label": "OpenSSL Istalled",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "common",
|
||||
"label": "Common",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
"ios": Object {
|
||||
"checks": Object {
|
||||
"ios.sdk": Object {
|
||||
"key": "ios.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "ios",
|
||||
"label": "iOS",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"result": Object {
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`startHealthCheck 1`] = `
|
||||
Object {
|
||||
"acknowledgedProblems": Array [],
|
||||
"healthcheckReport": Object {
|
||||
"categories": Object {
|
||||
"android": Object {
|
||||
"checks": Object {
|
||||
"android.sdk": Object {
|
||||
"key": "android.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "android",
|
||||
"label": "Android",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
"common": Object {
|
||||
"checks": Object {
|
||||
"common.openssl": Object {
|
||||
"key": "common.openssl",
|
||||
"label": "OpenSSL Istalled",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "common",
|
||||
"label": "Common",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
"ios": Object {
|
||||
"checks": Object {
|
||||
"ios.sdk": Object {
|
||||
"key": "ios.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "ios",
|
||||
"label": "iOS",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`statuses updated after healthchecks finished 1`] = `
|
||||
Object {
|
||||
"acknowledgedProblems": Array [],
|
||||
"healthcheckReport": Object {
|
||||
"categories": Object {
|
||||
"android": Object {
|
||||
"checks": Object {
|
||||
"android.sdk": Object {
|
||||
"key": "android.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "android",
|
||||
"label": "Android",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
"common": Object {
|
||||
"checks": Object {
|
||||
"common.openssl": Object {
|
||||
"key": "common.openssl",
|
||||
"label": "OpenSSL Istalled",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "common",
|
||||
"label": "Common",
|
||||
"result": Object {
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
"ios": Object {
|
||||
"checks": Object {
|
||||
"ios.sdk": Object {
|
||||
"key": "ios.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "ios",
|
||||
"label": "iOS",
|
||||
"result": Object {
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"status": "FAILED",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`updateHealthcheckResult 1`] = `
|
||||
Object {
|
||||
"acknowledgedProblems": Array [],
|
||||
"healthcheckReport": Object {
|
||||
"categories": Object {
|
||||
"android": Object {
|
||||
"checks": Object {
|
||||
"android.sdk": Object {
|
||||
"key": "android.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"isAcknowledged": false,
|
||||
"message": "Updated Test Message",
|
||||
"status": "SUCCESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "android",
|
||||
"label": "Android",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
"common": Object {
|
||||
"checks": Object {
|
||||
"common.openssl": Object {
|
||||
"key": "common.openssl",
|
||||
"label": "OpenSSL Istalled",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "common",
|
||||
"label": "Common",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
"ios": Object {
|
||||
"checks": Object {
|
||||
"ios.sdk": Object {
|
||||
"key": "ios.sdk",
|
||||
"label": "SDK Installed",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"key": "ios",
|
||||
"label": "iOS",
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
},
|
||||
"result": Object {
|
||||
"status": "IN_PROGRESS",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 reducer from '../application';
|
||||
import {
|
||||
initialState,
|
||||
addStatusMessage,
|
||||
removeStatusMessage,
|
||||
} from '../application';
|
||||
|
||||
test('ADD_STATUS_MSG, to check if the status messages get pushed to the state', () => {
|
||||
const state = reducer(
|
||||
initialState(),
|
||||
addStatusMessage({msg: 'Status Msg', sender: 'Test'}),
|
||||
);
|
||||
expect(state.statusMessages).toEqual(['Test: Status Msg']);
|
||||
const updatedstate = reducer(
|
||||
state,
|
||||
addStatusMessage({msg: 'Status Msg 2', sender: 'Test'}),
|
||||
);
|
||||
expect(updatedstate.statusMessages).toEqual([
|
||||
'Test: Status Msg',
|
||||
'Test: Status Msg 2',
|
||||
]);
|
||||
});
|
||||
|
||||
test('REMOVE_STATUS_MSG, to check if the status messages gets removed from the state', () => {
|
||||
const initState = initialState();
|
||||
const state = reducer(
|
||||
initState,
|
||||
removeStatusMessage({msg: 'Status Msg', sender: 'Test'}),
|
||||
);
|
||||
expect(state).toEqual(initState);
|
||||
const stateWithMessages = reducer(
|
||||
reducer(
|
||||
initialState(),
|
||||
addStatusMessage({msg: 'Status Msg', sender: 'Test'}),
|
||||
),
|
||||
addStatusMessage({msg: 'Status Msg 2', sender: 'Test'}),
|
||||
);
|
||||
const updatedState = reducer(
|
||||
stateWithMessages,
|
||||
removeStatusMessage({msg: 'Status Msg', sender: 'Test'}),
|
||||
);
|
||||
expect(updatedState.statusMessages).toEqual(['Test: Status Msg 2']);
|
||||
const updatedStateWithNoMessages = reducer(
|
||||
updatedState,
|
||||
removeStatusMessage({msg: 'Status Msg 2', sender: 'Test'}),
|
||||
);
|
||||
expect(updatedStateWithNoMessages.statusMessages).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* 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 reducer, {selectClient, selectDevice} from '../connections';
|
||||
import {State, selectPlugin} from '../connections';
|
||||
import {
|
||||
_SandyPluginDefinition,
|
||||
_setFlipperLibImplementation,
|
||||
TestUtils,
|
||||
MockedConsole,
|
||||
} from 'flipper-plugin';
|
||||
import {TestDevice} from '../../test-utils/TestDevice';
|
||||
import {
|
||||
createMockFlipperWithPlugin,
|
||||
MockFlipperResult,
|
||||
} from '../../test-utils/createMockFlipperWithPlugin';
|
||||
import {Store} from '..';
|
||||
import {getActiveClient, getActiveDevice} from '../../selectors/connections';
|
||||
import BaseDevice from '../../devices/BaseDevice';
|
||||
import Client from '../../Client';
|
||||
|
||||
let mockedConsole: MockedConsole;
|
||||
beforeEach(() => {
|
||||
mockedConsole = TestUtils.mockConsole();
|
||||
_setFlipperLibImplementation(TestUtils.createMockFlipperLib());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedConsole.unmock();
|
||||
_setFlipperLibImplementation(undefined);
|
||||
});
|
||||
|
||||
test('doing a double REGISTER_DEVICE fails', () => {
|
||||
const device1 = new TestDevice('serial', 'physical', 'title', 'Android');
|
||||
const device2 = new TestDevice('serial', 'physical', 'title2', 'Android');
|
||||
const initialState: State = reducer(undefined, {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device1,
|
||||
});
|
||||
expect(initialState.devices.length).toBe(1);
|
||||
expect(initialState.devices[0]).toBe(device1);
|
||||
|
||||
expect(() => {
|
||||
reducer(initialState, {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device2,
|
||||
});
|
||||
}).toThrow('still connected');
|
||||
});
|
||||
|
||||
test('register, remove, re-register a metro device works correctly', () => {
|
||||
const device1 = new TestDevice(
|
||||
'http://localhost:8081',
|
||||
'emulator',
|
||||
'React Native',
|
||||
'Metro',
|
||||
);
|
||||
let state: State = reducer(undefined, {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device1,
|
||||
});
|
||||
expect(state.devices.length).toBe(1);
|
||||
expect(state.devices[0].displayTitle()).toBe('React Native');
|
||||
|
||||
device1.disconnect();
|
||||
|
||||
expect(state.devices.length).toBe(1);
|
||||
expect(state.devices[0].displayTitle()).toBe('React Native (Offline)');
|
||||
|
||||
state = reducer(state, {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: new TestDevice(
|
||||
'http://localhost:8081',
|
||||
'emulator',
|
||||
'React Native',
|
||||
'Metro',
|
||||
),
|
||||
});
|
||||
expect(state.devices.length).toBe(1);
|
||||
expect(state.devices[0].displayTitle()).toBe('React Native');
|
||||
expect(state.devices[0]).not.toBe(device1);
|
||||
});
|
||||
|
||||
test('selectPlugin sets deepLinkPayload correctly', () => {
|
||||
const device1 = new TestDevice(
|
||||
'http://localhost:8081',
|
||||
'emulator',
|
||||
'React Native',
|
||||
'Metro',
|
||||
);
|
||||
let state = reducer(undefined, {
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device1,
|
||||
});
|
||||
state = reducer(
|
||||
undefined,
|
||||
selectPlugin({
|
||||
selectedPlugin: 'myPlugin',
|
||||
deepLinkPayload: 'myPayload',
|
||||
selectedDevice: device1,
|
||||
}),
|
||||
);
|
||||
expect(state.deepLinkPayload).toBe('myPayload');
|
||||
});
|
||||
|
||||
test('can handle plugins that throw at start', async () => {
|
||||
const TestPlugin = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails(),
|
||||
{
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
plugin() {
|
||||
throw new Error('Broken plugin');
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {client, store, createClient, createDevice} =
|
||||
await createMockFlipperWithPlugin(TestPlugin);
|
||||
|
||||
// not initialized
|
||||
expect(client.sandyPluginStates.get(TestPlugin.id)).toBe(undefined);
|
||||
|
||||
expect(store.getState().connections.clients.size).toBe(1);
|
||||
expect(client.connected.get()).toBe(true);
|
||||
|
||||
expect((console.error as any).mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Failed to start plugin 'TestPlugin': ",
|
||||
[Error: Broken plugin],
|
||||
]
|
||||
`);
|
||||
|
||||
const device2 = await createDevice({});
|
||||
const client2 = await createClient(device2, client.query.app);
|
||||
|
||||
expect((console.error as any).mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Failed to start plugin 'TestPlugin': ",
|
||||
[Error: Broken plugin],
|
||||
]
|
||||
`);
|
||||
expect(store.getState().connections.clients.size).toBe(2);
|
||||
expect(client2.connected.get()).toBe(true);
|
||||
expect(client2.sandyPluginStates.size).toBe(0);
|
||||
});
|
||||
|
||||
test('can handle device plugins that throw at start', async () => {
|
||||
const TestPlugin = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails(),
|
||||
{
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
devicePlugin() {
|
||||
throw new Error('Broken device plugin');
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {device, store, createDevice} = await createMockFlipperWithPlugin(
|
||||
TestPlugin,
|
||||
);
|
||||
|
||||
expect(mockedConsole.errorCalls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Failed to start device plugin 'TestPlugin': ",
|
||||
[Error: Broken device plugin],
|
||||
]
|
||||
`);
|
||||
|
||||
// not initialized
|
||||
expect(device.sandyPluginStates.get(TestPlugin.id)).toBe(undefined);
|
||||
|
||||
expect(store.getState().connections.devices.length).toBe(1);
|
||||
expect(device.connected.get()).toBe(true);
|
||||
|
||||
const device2 = await createDevice({});
|
||||
expect(store.getState().connections.devices.length).toBe(2);
|
||||
expect(device2.connected.get()).toBe(true);
|
||||
expect(mockedConsole.errorCalls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Failed to start device plugin 'TestPlugin': ",
|
||||
[Error: Broken device plugin],
|
||||
]
|
||||
`);
|
||||
expect(device2.sandyPluginStates.size).toBe(0);
|
||||
});
|
||||
|
||||
describe('selection changes', () => {
|
||||
const TestPlugin1 = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails(),
|
||||
{
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
plugin() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
);
|
||||
const TestPlugin2 = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails(),
|
||||
{
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
plugin() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
);
|
||||
const DevicePlugin1 = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails({pluginType: 'device'}),
|
||||
{
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
devicePlugin() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let device1: BaseDevice;
|
||||
let device2: BaseDevice;
|
||||
let metroDevice: BaseDevice;
|
||||
let d1app1: Client;
|
||||
let d1app2: Client;
|
||||
let d2app1: Client;
|
||||
let d2app2: Client;
|
||||
let store: Store;
|
||||
let mockFlipper: MockFlipperResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFlipper = await createMockFlipperWithPlugin(TestPlugin1, {
|
||||
additionalPlugins: [TestPlugin2, DevicePlugin1],
|
||||
supportedPlugins: [TestPlugin1.id, TestPlugin2.id, DevicePlugin1.id],
|
||||
});
|
||||
|
||||
device1 = mockFlipper.device;
|
||||
device2 = mockFlipper.createDevice({});
|
||||
metroDevice = mockFlipper.createDevice({
|
||||
os: 'Metro',
|
||||
serial: 'http://localhost:8081',
|
||||
});
|
||||
d1app1 = mockFlipper.client;
|
||||
d1app2 = await mockFlipper.createClient(device1, 'd1app2');
|
||||
d2app1 = await mockFlipper.createClient(device2, 'd2app1');
|
||||
d2app2 = await mockFlipper.createClient(device2, 'd2app2');
|
||||
store = mockFlipper.store;
|
||||
});
|
||||
|
||||
test('basic/ device selection change', async () => {
|
||||
// after registering d1app2, this will have become the selection
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device1,
|
||||
selectedPlugin: TestPlugin1.id,
|
||||
selectedAppId: d1app2.id,
|
||||
// no preferences changes, no explicit selection was made
|
||||
userPreferredDevice: device1.title,
|
||||
userPreferredPlugin: TestPlugin1.id,
|
||||
userPreferredApp: d1app1.query.app,
|
||||
});
|
||||
expect(getActiveClient(store.getState())).toBe(d1app2);
|
||||
expect(getActiveDevice(store.getState())).toBe(device1);
|
||||
|
||||
// select plugin 2 on d2app2
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: TestPlugin2.id,
|
||||
selectedAppId: d2app2.id,
|
||||
}),
|
||||
);
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device2,
|
||||
selectedPlugin: TestPlugin2.id,
|
||||
selectedAppId: d2app2.id,
|
||||
userPreferredDevice: device2.title,
|
||||
userPreferredPlugin: TestPlugin2.id,
|
||||
userPreferredApp: d2app2.query.app,
|
||||
});
|
||||
|
||||
// disconnect device1, and then register a new device should select it
|
||||
device1.disconnect();
|
||||
const device3 = await mockFlipper.createDevice({});
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device3,
|
||||
selectedPlugin: TestPlugin2.id,
|
||||
selectedAppId: null,
|
||||
// prefs not updated
|
||||
userPreferredDevice: device2.title,
|
||||
userPreferredPlugin: TestPlugin2.id,
|
||||
userPreferredApp: d2app2.query.app,
|
||||
});
|
||||
|
||||
store.dispatch(selectDevice(device1));
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device1,
|
||||
selectedPlugin: TestPlugin2.id,
|
||||
selectedAppId: null,
|
||||
userPreferredDevice: device1.title,
|
||||
// other prefs not updated
|
||||
userPreferredPlugin: TestPlugin2.id,
|
||||
userPreferredApp: d2app2.query.app,
|
||||
});
|
||||
|
||||
// used by plugin list, to keep main device / app selection correct
|
||||
expect(getActiveClient(store.getState())).toBe(null);
|
||||
expect(getActiveDevice(store.getState())).toBe(device1);
|
||||
});
|
||||
|
||||
test('select a metro device', async () => {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: DevicePlugin1.id,
|
||||
selectedDevice: metroDevice,
|
||||
selectedAppId: d2app1.id, // this app will determine the active device
|
||||
}),
|
||||
);
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.connections).toMatchObject({
|
||||
selectedDevice: metroDevice,
|
||||
selectedPlugin: DevicePlugin1.id,
|
||||
selectedAppId: d2app1.id,
|
||||
userPreferredDevice: metroDevice.title,
|
||||
// other prefs not updated
|
||||
userPreferredPlugin: DevicePlugin1.id,
|
||||
userPreferredApp: d2app1.query.app,
|
||||
});
|
||||
|
||||
// used by plugin list, to keep main device / app selection correct
|
||||
expect(getActiveClient(state)).toBe(d2app1);
|
||||
expect(getActiveDevice(state)).toBe(device2);
|
||||
});
|
||||
|
||||
test('introducing new client does not select it', async () => {
|
||||
await mockFlipper.createClient(device2, 'd2app3');
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device1,
|
||||
selectedPlugin: TestPlugin1.id,
|
||||
selectedAppId: d1app2.id,
|
||||
// other prefs not updated
|
||||
userPreferredDevice: device1.title,
|
||||
userPreferredPlugin: TestPlugin1.id,
|
||||
userPreferredApp: d1app1.query.app,
|
||||
});
|
||||
});
|
||||
|
||||
test('introducing new client does select it if preferred', async () => {
|
||||
// pure testing evil
|
||||
const client3 = await mockFlipper.createClient(
|
||||
device2,
|
||||
store.getState().connections.userPreferredApp!,
|
||||
);
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device2,
|
||||
selectedPlugin: TestPlugin1.id,
|
||||
selectedAppId: client3.id,
|
||||
// other prefs not updated
|
||||
userPreferredDevice: device1.title,
|
||||
userPreferredPlugin: TestPlugin1.id,
|
||||
userPreferredApp: d1app1.query.app,
|
||||
});
|
||||
});
|
||||
|
||||
test('introducing new client does select it if old is offline', async () => {
|
||||
d1app2.disconnect();
|
||||
const client3 = await mockFlipper.createClient(device2, 'd2app3');
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device2,
|
||||
selectedPlugin: TestPlugin1.id,
|
||||
selectedAppId: client3.id,
|
||||
// other prefs not updated
|
||||
userPreferredDevice: device1.title,
|
||||
userPreferredPlugin: TestPlugin1.id,
|
||||
userPreferredApp: d1app1.query.app,
|
||||
});
|
||||
});
|
||||
|
||||
test('select client', () => {
|
||||
store.dispatch(selectClient(d2app2.id));
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: device2,
|
||||
selectedPlugin: TestPlugin1.id,
|
||||
selectedAppId: d2app2.id,
|
||||
userPreferredDevice: device2.title,
|
||||
userPreferredPlugin: TestPlugin1.id,
|
||||
userPreferredApp: d2app2.query.app,
|
||||
});
|
||||
});
|
||||
|
||||
test('select device', () => {
|
||||
store.dispatch(selectDevice(metroDevice));
|
||||
expect(store.getState().connections).toMatchObject({
|
||||
selectedDevice: metroDevice,
|
||||
selectedPlugin: TestPlugin1.id,
|
||||
selectedAppId: null,
|
||||
userPreferredDevice: metroDevice.title,
|
||||
// other prefs not updated
|
||||
userPreferredPlugin: TestPlugin1.id,
|
||||
userPreferredApp: d1app1.query.app,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 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 {
|
||||
default as reducer,
|
||||
startHealthchecks,
|
||||
finishHealthchecks,
|
||||
updateHealthcheckResult,
|
||||
acknowledgeProblems,
|
||||
} from '../healthchecks';
|
||||
import {Healthchecks, EnvironmentInfo} from 'flipper-doctor';
|
||||
|
||||
const HEALTHCHECKS: Healthchecks = {
|
||||
ios: {
|
||||
label: 'iOS',
|
||||
isSkipped: false,
|
||||
isRequired: true,
|
||||
healthchecks: [
|
||||
{
|
||||
key: 'ios.sdk',
|
||||
label: 'SDK Installed',
|
||||
run: async (_env: EnvironmentInfo) => {
|
||||
return {hasProblem: false, message: ''};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
android: {
|
||||
label: 'Android',
|
||||
isSkipped: false,
|
||||
isRequired: true,
|
||||
healthchecks: [
|
||||
{
|
||||
key: 'android.sdk',
|
||||
label: 'SDK Installed',
|
||||
run: async (_env: EnvironmentInfo) => {
|
||||
return {hasProblem: true, message: 'Error'};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
common: {
|
||||
label: 'Common',
|
||||
isSkipped: false,
|
||||
isRequired: false,
|
||||
healthchecks: [
|
||||
{
|
||||
key: 'common.openssl',
|
||||
label: 'OpenSSL Istalled',
|
||||
run: async (_env: EnvironmentInfo) => {
|
||||
return {hasProblem: false, message: ''};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
test('startHealthCheck', () => {
|
||||
const res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
|
||||
expect(res).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('updateHealthcheckResult', () => {
|
||||
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('android', 'android.sdk', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
expect(res).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('finish', () => {
|
||||
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('ios', 'ios.sdk', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('android', 'android.sdk', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('common', 'common.openssl', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
res = reducer(res, finishHealthchecks());
|
||||
expect(res).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('statuses updated after healthchecks finished', () => {
|
||||
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('android', 'android.sdk', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'FAILED',
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('ios', 'ios.sdk', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('common', 'common.openssl', {
|
||||
message: 'Updated Test Message',
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
res = reducer(res, finishHealthchecks());
|
||||
expect(res).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('acknowledgeProblems', () => {
|
||||
let res = reducer(undefined, startHealthchecks(HEALTHCHECKS));
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('ios', 'ios.sdk', {
|
||||
isAcknowledged: false,
|
||||
status: 'FAILED',
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('android', 'android.sdk', {
|
||||
isAcknowledged: false,
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
updateHealthcheckResult('common', 'common.openssl', {
|
||||
isAcknowledged: false,
|
||||
status: 'FAILED',
|
||||
}),
|
||||
);
|
||||
res = reducer(res, finishHealthchecks());
|
||||
res = reducer(res, acknowledgeProblems());
|
||||
expect(res).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* 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 {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
||||
import {
|
||||
_SandyPluginDefinition,
|
||||
TestUtils,
|
||||
PluginClient,
|
||||
Notification,
|
||||
DevicePluginClient,
|
||||
} from 'flipper-plugin';
|
||||
import {State, addNotification, removeNotification} from '../notifications';
|
||||
|
||||
import {
|
||||
default as reducer,
|
||||
setActiveNotifications,
|
||||
clearAllNotifications,
|
||||
updatePluginBlocklist,
|
||||
updateCategoryBlocklist,
|
||||
} from '../notifications';
|
||||
|
||||
const notification: Notification = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
message: 'message',
|
||||
severity: 'warning',
|
||||
};
|
||||
|
||||
function getInitialState(): State {
|
||||
return {
|
||||
activeNotifications: [],
|
||||
invalidatedNotifications: [],
|
||||
blocklistedPlugins: [],
|
||||
blocklistedCategories: [],
|
||||
clearedNotifications: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
test('reduce updateCategoryBlocklist', () => {
|
||||
const blocklistedCategories = ['blocklistedCategory'];
|
||||
const res = reducer(
|
||||
getInitialState(),
|
||||
updateCategoryBlocklist(blocklistedCategories),
|
||||
);
|
||||
expect(res).toEqual({
|
||||
...getInitialState(),
|
||||
blocklistedCategories,
|
||||
});
|
||||
});
|
||||
|
||||
test('reduce updatePluginBlocklist', () => {
|
||||
const blocklistedPlugins = ['blocklistedPlugin'];
|
||||
const res = reducer(
|
||||
getInitialState(),
|
||||
updatePluginBlocklist(blocklistedPlugins),
|
||||
);
|
||||
expect(res).toEqual({
|
||||
...getInitialState(),
|
||||
blocklistedPlugins,
|
||||
});
|
||||
});
|
||||
|
||||
test('reduce clearAllNotifications', () => {
|
||||
const pluginId = 'pluginId';
|
||||
const client = 'client';
|
||||
|
||||
const res = reducer(
|
||||
{
|
||||
...getInitialState(),
|
||||
activeNotifications: [
|
||||
{
|
||||
client,
|
||||
pluginId,
|
||||
notification,
|
||||
},
|
||||
],
|
||||
},
|
||||
clearAllNotifications(),
|
||||
);
|
||||
expect(res).toEqual({
|
||||
...getInitialState(),
|
||||
clearedNotifications: new Set([`${pluginId}#${notification.id}`]),
|
||||
});
|
||||
});
|
||||
|
||||
test('reduce setActiveNotifications', () => {
|
||||
const pluginId = 'pluginId';
|
||||
const client = 'client';
|
||||
|
||||
const res = reducer(
|
||||
getInitialState(),
|
||||
setActiveNotifications({
|
||||
notifications: [notification],
|
||||
client,
|
||||
pluginId,
|
||||
}),
|
||||
);
|
||||
expect(res).toEqual({
|
||||
...getInitialState(),
|
||||
activeNotifications: [
|
||||
{
|
||||
client,
|
||||
pluginId,
|
||||
notification,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('addNotification removes duplicates', () => {
|
||||
let res = reducer(
|
||||
getInitialState(),
|
||||
addNotification({
|
||||
pluginId: 'test',
|
||||
client: null,
|
||||
notification,
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
addNotification({
|
||||
pluginId: 'test',
|
||||
client: null,
|
||||
notification: {
|
||||
...notification,
|
||||
id: 'otherId',
|
||||
},
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
removeNotification({
|
||||
pluginId: 'test',
|
||||
client: null,
|
||||
notificationId: 'id',
|
||||
}),
|
||||
);
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeNotifications": Array [
|
||||
Object {
|
||||
"client": null,
|
||||
"notification": Object {
|
||||
"id": "otherId",
|
||||
"message": "message",
|
||||
"severity": "warning",
|
||||
"title": "title",
|
||||
},
|
||||
"pluginId": "test",
|
||||
},
|
||||
],
|
||||
"blocklistedCategories": Array [],
|
||||
"blocklistedPlugins": Array [],
|
||||
"clearedNotifications": Set {},
|
||||
"invalidatedNotifications": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('reduce removeNotification', () => {
|
||||
let res = reducer(
|
||||
getInitialState(),
|
||||
addNotification({
|
||||
pluginId: 'test',
|
||||
client: null,
|
||||
notification,
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
addNotification({
|
||||
pluginId: 'test',
|
||||
client: null,
|
||||
notification: {
|
||||
...notification,
|
||||
id: 'otherId',
|
||||
},
|
||||
}),
|
||||
);
|
||||
res = reducer(
|
||||
res,
|
||||
addNotification({
|
||||
pluginId: 'test',
|
||||
client: null,
|
||||
notification: {
|
||||
...notification,
|
||||
message: 'slightly different message',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeNotifications": Array [
|
||||
Object {
|
||||
"client": null,
|
||||
"notification": Object {
|
||||
"id": "otherId",
|
||||
"message": "message",
|
||||
"severity": "warning",
|
||||
"title": "title",
|
||||
},
|
||||
"pluginId": "test",
|
||||
},
|
||||
Object {
|
||||
"client": null,
|
||||
"notification": Object {
|
||||
"id": "id",
|
||||
"message": "slightly different message",
|
||||
"severity": "warning",
|
||||
"title": "title",
|
||||
},
|
||||
"pluginId": "test",
|
||||
},
|
||||
],
|
||||
"blocklistedCategories": Array [],
|
||||
"blocklistedPlugins": Array [],
|
||||
"clearedNotifications": Set {},
|
||||
"invalidatedNotifications": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('notifications from plugins arrive in the notifications reducer', async () => {
|
||||
const TestPlugin = TestUtils.createTestPlugin({
|
||||
plugin(client: PluginClient) {
|
||||
client.onUnhandledMessage(() => {
|
||||
client.showNotification({
|
||||
id: 'test',
|
||||
message: 'test message',
|
||||
severity: 'error',
|
||||
title: 'hi',
|
||||
action: 'dosomething',
|
||||
});
|
||||
});
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
const {store, client, sendMessage} = await createMockFlipperWithPlugin(
|
||||
TestPlugin,
|
||||
);
|
||||
sendMessage('testMessage', {});
|
||||
client.flushMessageBuffer();
|
||||
expect(store.getState().notifications).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeNotifications": Array [
|
||||
Object {
|
||||
"client": "TestApp#Android#MockAndroidDevice#serial",
|
||||
"notification": Object {
|
||||
"action": "dosomething",
|
||||
"id": "test",
|
||||
"message": "test message",
|
||||
"severity": "error",
|
||||
"title": "hi",
|
||||
},
|
||||
"pluginId": "TestPlugin",
|
||||
},
|
||||
],
|
||||
"blocklistedCategories": Array [],
|
||||
"blocklistedPlugins": Array [],
|
||||
"clearedNotifications": Set {},
|
||||
"invalidatedNotifications": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('notifications from a device plugin arrive in the notifications reducer', async () => {
|
||||
let trigger: any;
|
||||
const TestPlugin = TestUtils.createTestDevicePlugin({
|
||||
devicePlugin(client: DevicePluginClient) {
|
||||
trigger = () => {
|
||||
client.showNotification({
|
||||
id: 'test',
|
||||
message: 'test message',
|
||||
severity: 'error',
|
||||
title: 'hi',
|
||||
action: 'dosomething',
|
||||
});
|
||||
};
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
const {store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
trigger();
|
||||
expect(store.getState().notifications).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeNotifications": Array [
|
||||
Object {
|
||||
"client": "serial",
|
||||
"notification": Object {
|
||||
"action": "dosomething",
|
||||
"id": "test",
|
||||
"message": "test message",
|
||||
"severity": "error",
|
||||
"title": "hi",
|
||||
},
|
||||
"pluginId": "TestPlugin",
|
||||
},
|
||||
],
|
||||
"blocklistedCategories": Array [],
|
||||
"blocklistedPlugins": Array [],
|
||||
"clearedNotifications": Set {},
|
||||
"invalidatedNotifications": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('errors end up as notifications if crash reporter is active', async () => {
|
||||
const TestPlugin = TestUtils.createTestPlugin({
|
||||
plugin() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const CrashReporterImpl = require('../../../../plugins/public/crash_reporter/index');
|
||||
const CrashPlugin = TestUtils.createTestDevicePlugin(CrashReporterImpl, {
|
||||
id: 'CrashReporter',
|
||||
});
|
||||
|
||||
const {store, client, sendError} = await createMockFlipperWithPlugin(
|
||||
TestPlugin,
|
||||
{
|
||||
additionalPlugins: [CrashPlugin],
|
||||
},
|
||||
);
|
||||
sendError('gone wrong');
|
||||
client.flushMessageBuffer();
|
||||
expect(store.getState().notifications).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeNotifications": Array [
|
||||
Object {
|
||||
"client": "serial",
|
||||
"notification": Object {
|
||||
"action": "0",
|
||||
"category": "\\"gone wrong\\"",
|
||||
"id": "0",
|
||||
"message": "Callstack: No callstack available",
|
||||
"severity": "error",
|
||||
"title": "CRASH: Plugin ErrorReason: \\"gone wrong\\"",
|
||||
},
|
||||
"pluginId": "CrashReporter",
|
||||
},
|
||||
],
|
||||
"blocklistedCategories": Array [],
|
||||
"blocklistedPlugins": Array [],
|
||||
"clearedNotifications": Set {},
|
||||
"invalidatedNotifications": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('errors end NOT up as notifications if crash reporter is active but suppressPluginErrors is disabled', async () => {
|
||||
const TestPlugin = TestUtils.createTestPlugin({
|
||||
plugin() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
const CrashReporterImpl = require('../../../../plugins/public/crash_reporter/index');
|
||||
const CrashPlugin = TestUtils.createTestDevicePlugin(CrashReporterImpl, {
|
||||
id: 'CrashReporter',
|
||||
});
|
||||
|
||||
const {store, client, sendError} = await createMockFlipperWithPlugin(
|
||||
TestPlugin,
|
||||
{
|
||||
additionalPlugins: [CrashPlugin],
|
||||
},
|
||||
);
|
||||
store.dispatch({
|
||||
type: 'UPDATE_SETTINGS',
|
||||
payload: {
|
||||
...store.getState().settingsState,
|
||||
suppressPluginErrors: true,
|
||||
},
|
||||
});
|
||||
sendError('gone wrong');
|
||||
client.flushMessageBuffer();
|
||||
expect(store.getState().notifications).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"activeNotifications": Array [],
|
||||
"blocklistedCategories": Array [],
|
||||
"blocklistedPlugins": Array [],
|
||||
"clearedNotifications": Set {},
|
||||
"invalidatedNotifications": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
161
desktop/flipper-ui-core/src/reducers/__tests__/plugins.node.tsx
Normal file
161
desktop/flipper-ui-core/src/reducers/__tests__/plugins.node.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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 {
|
||||
default as reducer,
|
||||
registerPlugins,
|
||||
addGatekeepedPlugins,
|
||||
registerInstalledPlugins,
|
||||
} from '../plugins';
|
||||
import {FlipperPlugin, FlipperDevicePlugin, BaseAction} from '../../plugin';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
import {wrapSandy} from '../../test-utils/createMockFlipperWithPlugin';
|
||||
|
||||
const testPluginOrig = class extends FlipperPlugin<any, BaseAction, any> {
|
||||
static id = 'TestPlugin';
|
||||
};
|
||||
const testPlugin = wrapSandy(testPluginOrig);
|
||||
|
||||
const testDevicePluginOrig = class extends FlipperDevicePlugin<
|
||||
any,
|
||||
BaseAction,
|
||||
any
|
||||
> {
|
||||
static id = 'TestDevicePlugin';
|
||||
};
|
||||
const testDevicePlugin = wrapSandy(testDevicePluginOrig);
|
||||
|
||||
test('add clientPlugin', () => {
|
||||
const res = reducer(
|
||||
{
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
failedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPluginNames: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
initialized: false,
|
||||
},
|
||||
registerPlugins([testPlugin]),
|
||||
);
|
||||
expect(res.clientPlugins.get(testPlugin.id)).toBe(testPlugin);
|
||||
});
|
||||
|
||||
test('add devicePlugin', () => {
|
||||
const res = reducer(
|
||||
{
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
failedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPluginNames: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
initialized: false,
|
||||
},
|
||||
registerPlugins([testDevicePlugin]),
|
||||
);
|
||||
expect(res.devicePlugins.get(testDevicePlugin.id)).toBe(testDevicePlugin);
|
||||
});
|
||||
|
||||
test('do not add plugin twice', () => {
|
||||
const res = reducer(
|
||||
{
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
failedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPluginNames: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
initialized: false,
|
||||
},
|
||||
registerPlugins([testPlugin, testPlugin]),
|
||||
);
|
||||
expect(res.clientPlugins.size).toEqual(1);
|
||||
});
|
||||
|
||||
test('add gatekeeped plugin', () => {
|
||||
const gatekeepedPlugins: InstalledPluginDetails[] = [
|
||||
{
|
||||
name: 'plugin',
|
||||
version: '1.0.0',
|
||||
dir: '/plugins/test',
|
||||
specVersion: 2,
|
||||
pluginType: 'client',
|
||||
source: 'src/index.ts',
|
||||
isBundled: false,
|
||||
isActivatable: true,
|
||||
main: 'lib/index.js',
|
||||
title: 'test',
|
||||
id: 'test',
|
||||
entry: '/plugins/test/lib/index.js',
|
||||
},
|
||||
];
|
||||
const res = reducer(
|
||||
{
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
failedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPluginNames: new Set(),
|
||||
initialized: false,
|
||||
},
|
||||
addGatekeepedPlugins(gatekeepedPlugins),
|
||||
);
|
||||
expect(res.gatekeepedPlugins).toEqual(gatekeepedPlugins);
|
||||
});
|
||||
|
||||
test('reduce empty registerInstalledPlugins', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||
expect(result.installedPlugins).toEqual(new Map());
|
||||
});
|
||||
|
||||
const EXAMPLE_PLUGIN = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
description: 'my test plugin',
|
||||
dir: '/plugins/test',
|
||||
specVersion: 2,
|
||||
source: 'src/index.ts',
|
||||
isBundled: false,
|
||||
isActivatable: true,
|
||||
main: 'lib/index.js',
|
||||
title: 'test',
|
||||
id: 'test',
|
||||
entry: '/plugins/test/lib/index.js',
|
||||
} as InstalledPluginDetails;
|
||||
|
||||
test('reduce registerInstalledPlugins, clear again', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||
expect(result.installedPlugins).toEqual(
|
||||
new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]),
|
||||
);
|
||||
const result2 = reducer(result, registerInstalledPlugins([]));
|
||||
expect(result2.installedPlugins).toEqual(new Map());
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
||||
import {Store} from '../../reducers/';
|
||||
import {selectPlugin} from '../../reducers/connections';
|
||||
import {
|
||||
_SandyPluginDefinition,
|
||||
_SandyDevicePluginInstance,
|
||||
DevicePluginClient,
|
||||
TestUtils,
|
||||
} from 'flipper-plugin';
|
||||
|
||||
const pluginDetails = TestUtils.createMockPluginDetails();
|
||||
|
||||
let initialized = false;
|
||||
|
||||
beforeEach(() => {
|
||||
initialized = false;
|
||||
});
|
||||
|
||||
function devicePlugin(client: DevicePluginClient) {
|
||||
const activateStub = jest.fn();
|
||||
const deactivateStub = jest.fn();
|
||||
const destroyStub = jest.fn();
|
||||
|
||||
client.onActivate(activateStub);
|
||||
client.onDeactivate(deactivateStub);
|
||||
client.onDestroy(destroyStub);
|
||||
|
||||
initialized = true;
|
||||
|
||||
return {
|
||||
activateStub: activateStub,
|
||||
deactivateStub: deactivateStub,
|
||||
destroyStub,
|
||||
};
|
||||
}
|
||||
const TestPlugin = new _SandyPluginDefinition(pluginDetails, {
|
||||
supportsDevice: jest.fn().mockImplementation(() => true),
|
||||
devicePlugin: jest
|
||||
.fn()
|
||||
.mockImplementation(devicePlugin) as typeof devicePlugin,
|
||||
Component: jest.fn().mockImplementation(() => null),
|
||||
});
|
||||
|
||||
type PluginApi = ReturnType<typeof devicePlugin>;
|
||||
|
||||
function selectTestPlugin(store: Store) {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: TestPlugin.id,
|
||||
selectedAppId: null,
|
||||
deepLinkPayload: null,
|
||||
selectedDevice: store.getState().connections.selectedDevice!,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test('it should initialize device sandy plugins', async () => {
|
||||
const {device, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
|
||||
// already started, so initialized immediately
|
||||
expect(initialized).toBe(true);
|
||||
expect(device.sandyPluginStates.get(TestPlugin.id)).toBeInstanceOf(
|
||||
_SandyDevicePluginInstance,
|
||||
);
|
||||
const instanceApi: PluginApi = device.sandyPluginStates.get(
|
||||
TestPlugin.id,
|
||||
)!.instanceApi;
|
||||
|
||||
expect(instanceApi.activateStub).toBeCalledTimes(0);
|
||||
selectTestPlugin(store);
|
||||
|
||||
// without rendering, non-bg plugins won't connect automatically,
|
||||
// so this isn't the best test, but PluginContainer tests do test that part of the lifecycle
|
||||
device.sandyPluginStates.get(TestPlugin.id)!.activate();
|
||||
expect(instanceApi.activateStub).toBeCalledTimes(1);
|
||||
device.sandyPluginStates.get(TestPlugin.id)!.deactivate();
|
||||
expect(instanceApi.deactivateStub).toBeCalledTimes(1);
|
||||
expect(instanceApi.destroyStub).toBeCalledTimes(0);
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* 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 {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
||||
import Client from '../../Client';
|
||||
import {Store} from '../../reducers';
|
||||
import {registerPlugins} from '../../reducers/plugins';
|
||||
import {
|
||||
_SandyPluginDefinition,
|
||||
_SandyPluginInstance,
|
||||
PluginClient,
|
||||
TestUtils,
|
||||
} from 'flipper-plugin';
|
||||
import {switchPlugin} from '../pluginManager';
|
||||
|
||||
const pluginDetails = TestUtils.createMockPluginDetails();
|
||||
|
||||
let initialized = false;
|
||||
|
||||
beforeEach(() => {
|
||||
initialized = false;
|
||||
});
|
||||
|
||||
function plugin(client: PluginClient<any, any>) {
|
||||
const connectStub = jest.fn();
|
||||
const disconnectStub = jest.fn();
|
||||
const destroyStub = jest.fn();
|
||||
const messages: any[] = [];
|
||||
|
||||
client.onConnect(connectStub);
|
||||
client.onDisconnect(disconnectStub);
|
||||
client.onDestroy(destroyStub);
|
||||
client.onMessage('message', (msg) => {
|
||||
messages.push(msg);
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
|
||||
return {
|
||||
connectStub,
|
||||
disconnectStub,
|
||||
destroyStub,
|
||||
send: client.send,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
const TestPlugin = new _SandyPluginDefinition(pluginDetails, {
|
||||
plugin: jest.fn().mockImplementation(plugin) as typeof plugin,
|
||||
Component: jest.fn().mockImplementation(() => null),
|
||||
});
|
||||
|
||||
type PluginApi = ReturnType<typeof plugin>;
|
||||
|
||||
function starTestPlugin(store: Store, client: Client) {
|
||||
store.dispatch(
|
||||
switchPlugin({
|
||||
plugin: TestPlugin,
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test('it should initialize starred sandy plugins', async () => {
|
||||
const {client} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
|
||||
// already started, so initialized immediately
|
||||
expect(initialized).toBe(true);
|
||||
expect(client.sandyPluginStates.get(TestPlugin.id)).toBeInstanceOf(
|
||||
_SandyPluginInstance,
|
||||
);
|
||||
const instanceApi: PluginApi = client.sandyPluginStates.get(
|
||||
TestPlugin.id,
|
||||
)!.instanceApi;
|
||||
|
||||
expect(instanceApi.connectStub).toBeCalledTimes(1);
|
||||
client.deinitPlugin(TestPlugin.id);
|
||||
expect(instanceApi.disconnectStub).toBeCalledTimes(1);
|
||||
expect(instanceApi.destroyStub).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test('it should cleanup a plugin if disabled', async () => {
|
||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
|
||||
expect(TestPlugin.asPluginModule().plugin).toBeCalledTimes(1);
|
||||
const pluginInstance: PluginApi = client.sandyPluginStates.get(
|
||||
TestPlugin.id,
|
||||
)!.instanceApi;
|
||||
expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(0);
|
||||
client.initPlugin(TestPlugin.id);
|
||||
expect(pluginInstance.connectStub).toHaveBeenCalledTimes(1);
|
||||
|
||||
// disable
|
||||
starTestPlugin(store, client);
|
||||
expect(client.sandyPluginStates.has(TestPlugin.id)).toBeFalsy();
|
||||
expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(1);
|
||||
expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it should NOT cleanup if client is removed', async () => {
|
||||
const {client} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
const pluginInstance = client.sandyPluginStates.get(TestPlugin.id)!;
|
||||
expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0);
|
||||
|
||||
pluginInstance.connect();
|
||||
expect(client.connected.get()).toBe(true);
|
||||
client.disconnect();
|
||||
expect(client.connected.get()).toBe(false);
|
||||
expect(client.sandyPluginStates.has(TestPlugin.id)).toBeTruthy();
|
||||
expect(
|
||||
(pluginInstance.instanceApi as PluginApi).disconnectStub,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(
|
||||
(pluginInstance.instanceApi as PluginApi).destroyStub,
|
||||
).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('it should cleanup if client is destroyed', async () => {
|
||||
const {client} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
const pluginInstance = client.sandyPluginStates.get(TestPlugin.id)!;
|
||||
expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0);
|
||||
|
||||
client.destroy();
|
||||
expect(client.sandyPluginStates.has(TestPlugin.id)).toBeFalsy();
|
||||
|
||||
expect(
|
||||
(pluginInstance.instanceApi as PluginApi).destroyStub,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it should not initialize a sandy plugin if not enabled', async () => {
|
||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
|
||||
const Plugin2 = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails({
|
||||
name: 'Plugin2',
|
||||
id: 'Plugin2',
|
||||
}),
|
||||
{
|
||||
plugin: jest.fn().mockImplementation(plugin),
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const pluginState1 = client.sandyPluginStates.get(TestPlugin.id);
|
||||
expect(pluginState1).toBeInstanceOf(_SandyPluginInstance);
|
||||
store.dispatch(registerPlugins([Plugin2]));
|
||||
await client.refreshPlugins();
|
||||
// not yet enabled, so not yet started
|
||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined();
|
||||
expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(0);
|
||||
|
||||
store.dispatch(
|
||||
switchPlugin({
|
||||
plugin: Plugin2,
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBeInstanceOf(
|
||||
_SandyPluginInstance,
|
||||
);
|
||||
const instance = client.sandyPluginStates.get(Plugin2.id)!
|
||||
.instanceApi as PluginApi;
|
||||
expect(client.sandyPluginStates.get(TestPlugin.id)).toBe(pluginState1); // not reinitialized
|
||||
|
||||
expect(TestPlugin.asPluginModule().plugin).toBeCalledTimes(1);
|
||||
expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1);
|
||||
expect(instance.destroyStub).toHaveBeenCalledTimes(0);
|
||||
|
||||
// disable plugin again
|
||||
store.dispatch(
|
||||
switchPlugin({
|
||||
plugin: Plugin2,
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined();
|
||||
expect(instance.connectStub).toHaveBeenCalledTimes(0);
|
||||
// disconnect wasn't called because connect was never called
|
||||
expect(instance.disconnectStub).toHaveBeenCalledTimes(0);
|
||||
expect(instance.destroyStub).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it trigger hooks for background plugins', async () => {
|
||||
const {client} = await createMockFlipperWithPlugin(TestPlugin, {
|
||||
asBackgroundPlugin: true,
|
||||
});
|
||||
const pluginInstance: PluginApi = client.sandyPluginStates.get(
|
||||
TestPlugin.id,
|
||||
)!.instanceApi;
|
||||
expect(client.isBackgroundPlugin(TestPlugin.id)).toBeTruthy();
|
||||
expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(0);
|
||||
expect(pluginInstance.connectStub).toHaveBeenCalledTimes(1);
|
||||
expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(0);
|
||||
|
||||
client.destroy();
|
||||
expect(client.sandyPluginStates.has(TestPlugin.id)).toBeFalsy();
|
||||
expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(1);
|
||||
expect(pluginInstance.connectStub).toHaveBeenCalledTimes(1);
|
||||
expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('it can send messages from sandy clients', async () => {
|
||||
let testMethodCalledWith: any = undefined;
|
||||
const {client} = await createMockFlipperWithPlugin(TestPlugin, {
|
||||
onSend(method, params) {
|
||||
if (method === 'execute') {
|
||||
testMethodCalledWith = params;
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
const pluginInstance: PluginApi = client.sandyPluginStates.get(
|
||||
TestPlugin.id,
|
||||
)!.instanceApi;
|
||||
// without rendering, non-bg plugins won't connect automatically,
|
||||
client.initPlugin(TestPlugin.id);
|
||||
await pluginInstance.send('test', {test: 3});
|
||||
expect(testMethodCalledWith).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"api": "TestPlugin",
|
||||
"method": "test",
|
||||
"params": Object {
|
||||
"test": 3,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it should initialize "Navigation" plugin if not enabled', async () => {
|
||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin, {
|
||||
supportedPlugins: ['Navigation'],
|
||||
});
|
||||
|
||||
const Plugin2 = new _SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails({
|
||||
name: 'Plugin2',
|
||||
id: 'Navigation',
|
||||
}),
|
||||
{
|
||||
plugin: jest.fn().mockImplementation(plugin),
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const pluginState1 = client.sandyPluginStates.get(TestPlugin.id);
|
||||
expect(pluginState1).toBeInstanceOf(_SandyPluginInstance);
|
||||
store.dispatch(registerPlugins([Plugin2]));
|
||||
await client.refreshPlugins();
|
||||
// not enabled, but Navigation is an exception, so we still get an instance
|
||||
const origInstance = client.sandyPluginStates.get(Plugin2.id);
|
||||
expect(origInstance).toBeDefined();
|
||||
expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1);
|
||||
|
||||
store.dispatch(
|
||||
switchPlugin({
|
||||
plugin: Plugin2,
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBe(origInstance);
|
||||
const instance = client.sandyPluginStates.get(Plugin2.id)!
|
||||
.instanceApi as PluginApi;
|
||||
expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1);
|
||||
expect(instance.destroyStub).toHaveBeenCalledTimes(0);
|
||||
|
||||
// disable plugin again
|
||||
store.dispatch(
|
||||
switchPlugin({
|
||||
plugin: Plugin2,
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
|
||||
// stil enabled
|
||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBe(origInstance);
|
||||
expect(instance.connectStub).toHaveBeenCalledTimes(0);
|
||||
// disconnect wasn't called because connect was never called
|
||||
expect(instance.disconnectStub).toHaveBeenCalledTimes(0);
|
||||
expect(instance.destroyStub).toHaveBeenCalledTimes(0);
|
||||
|
||||
// closing does stop the plugin!
|
||||
client.destroy();
|
||||
expect(instance.destroyStub).toHaveBeenCalledTimes(1);
|
||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined();
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 {default as reducer, updateSettings, Tristate} from '../settings';
|
||||
|
||||
test('init', () => {
|
||||
const res = reducer(undefined, {type: 'INIT'});
|
||||
expect(res.enableAndroid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('updateSettings', () => {
|
||||
const initialSettings = reducer(undefined, {type: 'INIT'});
|
||||
const updatedSettings = Object.assign(initialSettings, {
|
||||
enableAndroid: false,
|
||||
enablePrefetching: Tristate.True,
|
||||
jsApps: {
|
||||
webAppLauncher: {
|
||||
height: 900,
|
||||
},
|
||||
},
|
||||
});
|
||||
const res = reducer(initialSettings, updateSettings(updatedSettings));
|
||||
|
||||
expect(res.enableAndroid).toBeFalsy();
|
||||
expect(res.enablePrefetching).toEqual(Tristate.True);
|
||||
expect(res.jsApps.webAppLauncher.height).toEqual(900);
|
||||
expect(res.jsApps.webAppLauncher.width).toEqual(
|
||||
initialSettings.jsApps.webAppLauncher.width,
|
||||
);
|
||||
});
|
||||
26
desktop/flipper-ui-core/src/reducers/__tests__/user.node.tsx
Normal file
26
desktop/flipper-ui-core/src/reducers/__tests__/user.node.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 {default as reducer, login, logout} from '../user';
|
||||
|
||||
test('login', () => {
|
||||
const userData = {name: 'Jane Doe'};
|
||||
const res = reducer({}, login(userData));
|
||||
expect(res).toEqual(userData);
|
||||
});
|
||||
|
||||
test('logout', () => {
|
||||
const res = reducer(
|
||||
{
|
||||
name: 'Jane Doe',
|
||||
},
|
||||
logout(),
|
||||
);
|
||||
expect(res).toEqual({});
|
||||
});
|
||||
196
desktop/flipper-ui-core/src/reducers/application.tsx
Normal file
196
desktop/flipper-ui-core/src/reducers/application.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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 {v1 as uuidv1} from 'uuid';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
import {Actions} from './';
|
||||
|
||||
export type LauncherMsg = {
|
||||
message: string;
|
||||
severity: 'warning' | 'error';
|
||||
};
|
||||
|
||||
export type StatusMessageType = {
|
||||
msg: string;
|
||||
sender: string;
|
||||
};
|
||||
|
||||
type SubShareType =
|
||||
| {
|
||||
type: 'file';
|
||||
file: string;
|
||||
}
|
||||
| {
|
||||
type: 'link';
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type ShareType = {
|
||||
statusComponent?: React.ReactNode;
|
||||
closeOnFinish: boolean;
|
||||
} & SubShareType;
|
||||
|
||||
export type State = {
|
||||
leftSidebarVisible: boolean;
|
||||
rightSidebarVisible: boolean;
|
||||
rightSidebarAvailable: boolean;
|
||||
windowIsFocused: boolean;
|
||||
share: ShareType | null;
|
||||
sessionId: string | null;
|
||||
launcherMsg: LauncherMsg;
|
||||
statusMessages: Array<string>;
|
||||
};
|
||||
|
||||
type BooleanActionType =
|
||||
| 'leftSidebarVisible'
|
||||
| 'rightSidebarVisible'
|
||||
| 'rightSidebarAvailable';
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: BooleanActionType;
|
||||
payload?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'windowIsFocused';
|
||||
payload: {isFocused: boolean; time: number};
|
||||
}
|
||||
| {
|
||||
type: 'LAUNCHER_MSG';
|
||||
payload: {
|
||||
severity: 'warning' | 'error';
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'ADD_STATUS_MSG';
|
||||
payload: {msg: string; sender: string};
|
||||
}
|
||||
| {
|
||||
type: 'REMOVE_STATUS_MSG';
|
||||
payload: {msg: string; sender: string};
|
||||
};
|
||||
|
||||
export const initialState: () => State = () => ({
|
||||
leftSidebarVisible: true,
|
||||
rightSidebarVisible: true,
|
||||
rightSidebarAvailable: false,
|
||||
windowIsFocused: getRenderHostInstance().hasFocus(),
|
||||
activeSheet: null,
|
||||
share: null,
|
||||
sessionId: uuidv1(),
|
||||
launcherMsg: {
|
||||
severity: 'warning',
|
||||
message: '',
|
||||
},
|
||||
statusMessages: [],
|
||||
trackingTimeline: [],
|
||||
});
|
||||
|
||||
function statusMessage(sender: string, msg: string): string {
|
||||
const messageTrimmed = msg.trim();
|
||||
const senderTrimmed = sender.trim();
|
||||
let statusMessage = senderTrimmed.length > 0 ? senderTrimmed : '';
|
||||
statusMessage =
|
||||
statusMessage.length > 0 && messageTrimmed.length > 0
|
||||
? `${statusMessage}: ${messageTrimmed}`
|
||||
: '';
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
export default function reducer(
|
||||
state: State | undefined,
|
||||
action: Actions,
|
||||
): State {
|
||||
state = state || initialState();
|
||||
if (
|
||||
action.type === 'leftSidebarVisible' ||
|
||||
action.type === 'rightSidebarVisible' ||
|
||||
action.type === 'rightSidebarAvailable'
|
||||
) {
|
||||
const newValue =
|
||||
typeof action.payload === 'undefined'
|
||||
? !state[action.type]
|
||||
: action.payload;
|
||||
|
||||
if (state[action.type] === newValue) {
|
||||
// value hasn't changed
|
||||
return state;
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
[action.type]: newValue,
|
||||
};
|
||||
}
|
||||
} else if (action.type === 'windowIsFocused') {
|
||||
return {
|
||||
...state,
|
||||
windowIsFocused: action.payload.isFocused,
|
||||
};
|
||||
} else if (action.type === 'LAUNCHER_MSG') {
|
||||
return {
|
||||
...state,
|
||||
launcherMsg: action.payload,
|
||||
};
|
||||
} else if (action.type === 'ADD_STATUS_MSG') {
|
||||
const {sender, msg} = action.payload;
|
||||
const statusMsg = statusMessage(sender, msg);
|
||||
if (statusMsg.length > 0) {
|
||||
return {
|
||||
...state,
|
||||
statusMessages: [...state.statusMessages, statusMsg],
|
||||
};
|
||||
}
|
||||
return state;
|
||||
} else if (action.type === 'REMOVE_STATUS_MSG') {
|
||||
const {sender, msg} = action.payload;
|
||||
const statusMsg = statusMessage(sender, msg);
|
||||
if (statusMsg.length > 0) {
|
||||
const statusMessages = [...state.statusMessages];
|
||||
statusMessages.splice(statusMessages.indexOf(statusMsg), 1);
|
||||
return {...state, statusMessages};
|
||||
}
|
||||
return state;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleAction = (
|
||||
type: BooleanActionType,
|
||||
payload?: boolean,
|
||||
): Action => ({
|
||||
type,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const toggleLeftSidebarVisible = (payload?: boolean): Action => ({
|
||||
type: 'leftSidebarVisible',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const toggleRightSidebarVisible = (payload?: boolean): Action => ({
|
||||
type: 'rightSidebarVisible',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const toggleRightSidebarAvailable = (payload?: boolean): Action => ({
|
||||
type: 'rightSidebarAvailable',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const addStatusMessage = (payload: StatusMessageType): Action => ({
|
||||
type: 'ADD_STATUS_MSG',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const removeStatusMessage = (payload: StatusMessageType): Action => ({
|
||||
type: 'REMOVE_STATUS_MSG',
|
||||
payload,
|
||||
});
|
||||
598
desktop/flipper-ui-core/src/reducers/connections.tsx
Normal file
598
desktop/flipper-ui-core/src/reducers/connections.tsx
Normal file
@@ -0,0 +1,598 @@
|
||||
/**
|
||||
* 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 {ComponentType} from 'react';
|
||||
import {produce} from 'immer';
|
||||
|
||||
import type BaseDevice from '../devices/BaseDevice';
|
||||
import type Client from '../Client';
|
||||
import type {
|
||||
UninitializedClient,
|
||||
DeviceOS,
|
||||
Logger,
|
||||
FlipperServer,
|
||||
} from 'flipper-common';
|
||||
import {performance} from 'perf_hooks';
|
||||
import type {Actions} from '.';
|
||||
import {WelcomeScreenStaticView} from '../sandy-chrome/WelcomeScreen';
|
||||
import {isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||
import {getPluginKey} from '../utils/pluginKey';
|
||||
|
||||
import {deconstructClientId} from 'flipper-common';
|
||||
import type {RegisterPluginAction} from './plugins';
|
||||
import {shallowEqual} from 'react-redux';
|
||||
import {NormalizedMenuEntry} from 'flipper-plugin';
|
||||
|
||||
export type StaticViewProps = {logger: Logger};
|
||||
|
||||
export type StaticView =
|
||||
| null
|
||||
| ComponentType<StaticViewProps>
|
||||
| React.FunctionComponent<any>;
|
||||
|
||||
export type State = StateV2;
|
||||
|
||||
export const persistVersion = 2;
|
||||
export const persistMigrations = {
|
||||
1: (state: any) => {
|
||||
const stateV0 = state as StateV0;
|
||||
const stateV1 = {
|
||||
...stateV0,
|
||||
enabledPlugins: stateV0.userStarredPlugins ?? {},
|
||||
enabledDevicePlugins:
|
||||
stateV0.userStarredDevicePlugins ??
|
||||
new Set<string>(INITAL_STATE.enabledDevicePlugins),
|
||||
};
|
||||
return stateV1 as any;
|
||||
},
|
||||
2: (state: any) => {
|
||||
const stateV1 = state as StateV1;
|
||||
const stateV2 = {
|
||||
...stateV1,
|
||||
enabledPlugins: stateV1.enabledPlugins ?? {},
|
||||
enabledDevicePlugins:
|
||||
stateV1.enabledDevicePlugins ??
|
||||
new Set<string>(INITAL_STATE.enabledDevicePlugins),
|
||||
};
|
||||
return stateV2 as any;
|
||||
},
|
||||
};
|
||||
|
||||
type StateV2 = {
|
||||
devices: Array<BaseDevice>;
|
||||
selectedDevice: null | BaseDevice;
|
||||
selectedPlugin: null | string;
|
||||
selectedAppId: null | string; // Full quantified identifier of the app
|
||||
pluginMenuEntries: NormalizedMenuEntry[];
|
||||
userPreferredDevice: null | string;
|
||||
userPreferredPlugin: null | string;
|
||||
userPreferredApp: null | string; // The name of the preferred app, e.g. Facebook
|
||||
enabledPlugins: {[client: string]: string[]};
|
||||
enabledDevicePlugins: Set<string>;
|
||||
clients: Map<string, Client>;
|
||||
uninitializedClients: UninitializedClient[];
|
||||
deepLinkPayload: unknown;
|
||||
staticView: StaticView;
|
||||
selectedAppPluginListRevision: number;
|
||||
flipperServer: FlipperServer | undefined;
|
||||
};
|
||||
|
||||
type StateV1 = Omit<StateV2, 'enabledPlugins' | 'enabledDevicePlugins'> & {
|
||||
enabledPlugins?: {[client: string]: string[]};
|
||||
enabledDevicePlugins?: Set<string>;
|
||||
};
|
||||
|
||||
type StateV0 = Omit<StateV1, 'enabledPlugins' | 'enabledDevicePlugins'> & {
|
||||
userStarredPlugins?: {[client: string]: string[]};
|
||||
userStarredDevicePlugins?: Set<string>;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'REGISTER_DEVICE';
|
||||
payload: BaseDevice;
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_DEVICE';
|
||||
payload: BaseDevice;
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_PLUGIN';
|
||||
payload: {
|
||||
selectedPlugin: string;
|
||||
selectedAppId?: null | string; // not set for device plugins
|
||||
deepLinkPayload?: unknown;
|
||||
selectedDevice?: BaseDevice | null;
|
||||
time: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'SET_MENU_ENTRIES';
|
||||
payload: NormalizedMenuEntry[];
|
||||
}
|
||||
| {
|
||||
type: 'NEW_CLIENT';
|
||||
payload: Client;
|
||||
}
|
||||
| {
|
||||
type: 'CLIENT_REMOVED';
|
||||
payload: string;
|
||||
}
|
||||
| {
|
||||
type: 'START_CLIENT_SETUP';
|
||||
payload: UninitializedClient;
|
||||
}
|
||||
| {
|
||||
type: 'SET_STATIC_VIEW';
|
||||
payload: StaticView;
|
||||
deepLinkPayload: unknown;
|
||||
}
|
||||
| {
|
||||
type: 'SET_PLUGIN_ENABLED';
|
||||
payload: {
|
||||
pluginId: string;
|
||||
selectedApp: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'SET_DEVICE_PLUGIN_ENABLED';
|
||||
payload: {
|
||||
pluginId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'SET_PLUGIN_DISABLED';
|
||||
payload: {
|
||||
pluginId: string;
|
||||
selectedApp: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'SET_DEVICE_PLUGIN_DISABLED';
|
||||
payload: {
|
||||
pluginId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'SELECT_CLIENT';
|
||||
payload: string; // App ID
|
||||
}
|
||||
| {
|
||||
type: 'APP_PLUGIN_LIST_CHANGED';
|
||||
}
|
||||
| {
|
||||
type: 'SET_FLIPPER_SERVER';
|
||||
payload: FlipperServer;
|
||||
}
|
||||
| RegisterPluginAction;
|
||||
|
||||
const DEFAULT_PLUGIN = 'DeviceLogs';
|
||||
const DEFAULT_DEVICE_BLACKLIST: DeviceOS[] = ['MacOS', 'Metro', 'Windows'];
|
||||
const INITAL_STATE: State = {
|
||||
devices: [],
|
||||
selectedDevice: null,
|
||||
selectedAppId: null,
|
||||
selectedPlugin: DEFAULT_PLUGIN,
|
||||
pluginMenuEntries: [],
|
||||
userPreferredDevice: null,
|
||||
userPreferredPlugin: null,
|
||||
userPreferredApp: null,
|
||||
enabledPlugins: {},
|
||||
enabledDevicePlugins: new Set([
|
||||
'DeviceLogs',
|
||||
'CrashReporter',
|
||||
'MobileBuilds',
|
||||
'Hermesdebuggerrn',
|
||||
'React',
|
||||
]),
|
||||
clients: new Map(),
|
||||
uninitializedClients: [],
|
||||
deepLinkPayload: null,
|
||||
staticView: WelcomeScreenStaticView,
|
||||
selectedAppPluginListRevision: 0,
|
||||
flipperServer: undefined,
|
||||
};
|
||||
|
||||
export default (state: State = INITAL_STATE, action: Actions): State => {
|
||||
switch (action.type) {
|
||||
case 'SET_FLIPPER_SERVER': {
|
||||
return {
|
||||
...state,
|
||||
flipperServer: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_STATIC_VIEW': {
|
||||
const {payload, deepLinkPayload} = action;
|
||||
return {
|
||||
...state,
|
||||
staticView: payload,
|
||||
deepLinkPayload: deepLinkPayload ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
case 'RESET_SUPPORT_FORM_V2_STATE': {
|
||||
return {
|
||||
...state,
|
||||
staticView: null,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SELECT_DEVICE': {
|
||||
const {payload} = action;
|
||||
return {
|
||||
...state,
|
||||
staticView: null,
|
||||
selectedDevice: payload,
|
||||
selectedAppId: null,
|
||||
userPreferredDevice: payload
|
||||
? payload.title
|
||||
: state.userPreferredDevice,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_MENU_ENTRIES': {
|
||||
return {...state, pluginMenuEntries: action.payload};
|
||||
}
|
||||
|
||||
case 'REGISTER_DEVICE': {
|
||||
const {payload} = action;
|
||||
|
||||
const newDevices = state.devices.slice();
|
||||
const existing = state.devices.findIndex(
|
||||
(device) => device.serial === payload.serial,
|
||||
);
|
||||
if (existing !== -1) {
|
||||
const d = newDevices[existing];
|
||||
if (d.connected.get()) {
|
||||
throw new Error(`Cannot register, '${d.serial}' is still connected`);
|
||||
}
|
||||
newDevices[existing] = payload;
|
||||
} else {
|
||||
newDevices.push(payload);
|
||||
}
|
||||
|
||||
const selectNewDevice =
|
||||
!state.selectedDevice ||
|
||||
!state.selectedDevice.isConnected ||
|
||||
state.userPreferredDevice === payload.title;
|
||||
let selectedAppId = state.selectedAppId;
|
||||
|
||||
if (selectNewDevice) {
|
||||
// need to select a different app
|
||||
selectedAppId =
|
||||
getAllClients(state).find(
|
||||
(c) =>
|
||||
c.device === payload && c.query.app === state.userPreferredApp,
|
||||
)?.id ?? null;
|
||||
// nothing found, try first app if any
|
||||
if (!selectedAppId) {
|
||||
selectedAppId =
|
||||
getAllClients(state).find((c) => c.device === payload)?.id ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
devices: newDevices,
|
||||
selectedDevice: selectNewDevice ? payload : state.selectedDevice,
|
||||
selectedAppId,
|
||||
};
|
||||
}
|
||||
|
||||
case 'SELECT_PLUGIN': {
|
||||
const {selectedPlugin, selectedAppId, deepLinkPayload} = action.payload;
|
||||
|
||||
if (selectedPlugin) {
|
||||
performance.mark(`activePlugin-${selectedPlugin}`);
|
||||
}
|
||||
|
||||
const client = state.clients.get(selectedAppId!);
|
||||
const device = action.payload.selectedDevice ?? client?.device;
|
||||
|
||||
if (!device) {
|
||||
console.warn(
|
||||
'No valid device / client provided when calling SELECT_PLUGIN',
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
staticView: null,
|
||||
selectedDevice: device,
|
||||
userPreferredDevice: canBeDefaultDevice(device)
|
||||
? device.title
|
||||
: state.userPreferredDevice,
|
||||
selectedAppId: selectedAppId ?? null,
|
||||
userPreferredApp:
|
||||
state.clients.get(selectedAppId!)?.query.app ??
|
||||
state.userPreferredApp,
|
||||
selectedPlugin,
|
||||
userPreferredPlugin: selectedPlugin,
|
||||
deepLinkPayload: deepLinkPayload,
|
||||
};
|
||||
}
|
||||
|
||||
case 'NEW_CLIENT': {
|
||||
const {payload} = action;
|
||||
|
||||
return produce(state, (draft) => {
|
||||
if (draft.clients.has(payload.id)) {
|
||||
console.error(
|
||||
`Received a new connection for client ${payload.id}, but the old connection was not cleaned up`,
|
||||
);
|
||||
}
|
||||
draft.clients.set(payload.id, payload);
|
||||
|
||||
// select new client if nothing select, this one is preferred, or the old one is offline
|
||||
const selectNewClient =
|
||||
!draft.selectedAppId ||
|
||||
draft.userPreferredApp === payload.query.app ||
|
||||
draft.clients.get(draft.selectedAppId!)?.connected.get() === false;
|
||||
|
||||
if (selectNewClient) {
|
||||
draft.selectedAppId = payload.id;
|
||||
draft.selectedDevice = payload.device;
|
||||
}
|
||||
|
||||
const unitialisedIndex = draft.uninitializedClients.findIndex(
|
||||
(c) =>
|
||||
c.deviceName === payload.query.device ||
|
||||
c.appName === payload.query.app,
|
||||
);
|
||||
if (unitialisedIndex !== -1)
|
||||
draft.uninitializedClients.splice(unitialisedIndex, 1);
|
||||
});
|
||||
}
|
||||
|
||||
case 'SELECT_CLIENT': {
|
||||
const {payload} = action;
|
||||
const client = state.clients.get(payload);
|
||||
|
||||
if (!client) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedAppId: payload,
|
||||
selectedDevice: client.device,
|
||||
userPreferredDevice: client.device.title,
|
||||
userPreferredApp: client.query.app,
|
||||
selectedPlugin:
|
||||
state.selectedPlugin && client.supportsPlugin(state.selectedPlugin)
|
||||
? state.selectedPlugin
|
||||
: state.userPreferredPlugin &&
|
||||
client.supportsPlugin(state.userPreferredPlugin)
|
||||
? state.userPreferredPlugin
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
case 'CLIENT_REMOVED': {
|
||||
const {payload} = action;
|
||||
|
||||
return produce(state, (draft) => {
|
||||
draft.clients.delete(payload);
|
||||
if (draft.selectedAppId === payload) {
|
||||
draft.selectedAppId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case 'START_CLIENT_SETUP': {
|
||||
const {payload} = action;
|
||||
return {
|
||||
...state,
|
||||
uninitializedClients: [
|
||||
...state.uninitializedClients.filter(
|
||||
(existing) => !shallowEqual(existing, payload),
|
||||
),
|
||||
payload,
|
||||
],
|
||||
};
|
||||
}
|
||||
case 'REGISTER_PLUGINS': {
|
||||
// plugins are registered after creating the base devices, so update them
|
||||
const plugins = action.payload;
|
||||
plugins.forEach((plugin) => {
|
||||
if (isDevicePluginDefinition(plugin)) {
|
||||
// smell: devices are mutable
|
||||
state.devices.forEach((device) => {
|
||||
device.loadDevicePlugin(plugin);
|
||||
});
|
||||
}
|
||||
});
|
||||
return state;
|
||||
}
|
||||
case 'SET_PLUGIN_ENABLED': {
|
||||
const {pluginId, selectedApp} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
if (!draft.enabledPlugins[selectedApp]) {
|
||||
draft.enabledPlugins[selectedApp] = [];
|
||||
}
|
||||
const plugins = draft.enabledPlugins[selectedApp];
|
||||
const idx = plugins.indexOf(pluginId);
|
||||
if (idx === -1) {
|
||||
plugins.push(pluginId);
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'SET_DEVICE_PLUGIN_ENABLED': {
|
||||
const {pluginId} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
draft.enabledDevicePlugins.add(pluginId);
|
||||
});
|
||||
}
|
||||
case 'SET_PLUGIN_DISABLED': {
|
||||
const {pluginId, selectedApp} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
if (!draft.enabledPlugins[selectedApp]) {
|
||||
draft.enabledPlugins[selectedApp] = [];
|
||||
}
|
||||
const plugins = draft.enabledPlugins[selectedApp];
|
||||
const idx = plugins.indexOf(pluginId);
|
||||
if (idx !== -1) {
|
||||
plugins.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'SET_DEVICE_PLUGIN_DISABLED': {
|
||||
const {pluginId} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
draft.enabledDevicePlugins.delete(pluginId);
|
||||
});
|
||||
}
|
||||
case 'APP_PLUGIN_LIST_CHANGED': {
|
||||
return produce(state, (draft) => {
|
||||
draft.selectedAppPluginListRevision++;
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectDevice = (payload: BaseDevice): Action => ({
|
||||
type: 'SELECT_DEVICE',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const setStaticView = (
|
||||
payload: StaticView,
|
||||
deepLinkPayload?: unknown,
|
||||
): Action => {
|
||||
if (!payload) {
|
||||
throw new Error('Cannot set empty static view');
|
||||
}
|
||||
return {
|
||||
type: 'SET_STATIC_VIEW',
|
||||
payload,
|
||||
deepLinkPayload,
|
||||
};
|
||||
};
|
||||
|
||||
export const selectPlugin = (payload: {
|
||||
selectedPlugin: string;
|
||||
selectedAppId?: null | string;
|
||||
selectedDevice?: null | BaseDevice;
|
||||
deepLinkPayload?: unknown;
|
||||
time?: number;
|
||||
}): Action => ({
|
||||
type: 'SELECT_PLUGIN',
|
||||
payload: {...payload, time: payload.time ?? Date.now()},
|
||||
});
|
||||
|
||||
export const setMenuEntries = (menuEntries: NormalizedMenuEntry[]): Action => ({
|
||||
type: 'SET_MENU_ENTRIES',
|
||||
payload: menuEntries,
|
||||
});
|
||||
|
||||
export const selectClient = (clientId: string): Action => ({
|
||||
type: 'SELECT_CLIENT',
|
||||
payload: clientId,
|
||||
});
|
||||
|
||||
export const setPluginEnabled = (pluginId: string, appId: string): Action => ({
|
||||
type: 'SET_PLUGIN_ENABLED',
|
||||
payload: {
|
||||
pluginId,
|
||||
selectedApp: appId,
|
||||
},
|
||||
});
|
||||
|
||||
export const setDevicePluginEnabled = (pluginId: string): Action => ({
|
||||
type: 'SET_DEVICE_PLUGIN_ENABLED',
|
||||
payload: {
|
||||
pluginId,
|
||||
},
|
||||
});
|
||||
|
||||
export const setDevicePluginDisabled = (pluginId: string): Action => ({
|
||||
type: 'SET_DEVICE_PLUGIN_DISABLED',
|
||||
payload: {
|
||||
pluginId,
|
||||
},
|
||||
});
|
||||
|
||||
export const setPluginDisabled = (pluginId: string, appId: string): Action => ({
|
||||
type: 'SET_PLUGIN_DISABLED',
|
||||
payload: {
|
||||
pluginId,
|
||||
selectedApp: appId,
|
||||
},
|
||||
});
|
||||
|
||||
export const appPluginListChanged = (): Action => ({
|
||||
type: 'APP_PLUGIN_LIST_CHANGED',
|
||||
});
|
||||
|
||||
export function getClientsByDevice(
|
||||
device: null | undefined | BaseDevice,
|
||||
clients: Map<string, Client>,
|
||||
): Client[] {
|
||||
if (!device) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(clients.values())
|
||||
.filter((client: Client) => client.query.device_id === device.serial)
|
||||
.sort((a, b) => (a.query.app || '').localeCompare(b.query.app));
|
||||
}
|
||||
|
||||
export function getClientsByAppName(
|
||||
clients: Map<string, Client>,
|
||||
appName: string | null | undefined,
|
||||
): Client[] {
|
||||
return Array.from(clients.values()).filter(
|
||||
(client) => client.query.app === appName,
|
||||
);
|
||||
}
|
||||
|
||||
export function getClientById(
|
||||
clients: Client[],
|
||||
clientId: string | null | undefined,
|
||||
): Client | undefined {
|
||||
return clients.find((client) => client.id === clientId);
|
||||
}
|
||||
|
||||
export function canBeDefaultDevice(device: BaseDevice) {
|
||||
return !DEFAULT_DEVICE_BLACKLIST.includes(device.os);
|
||||
}
|
||||
|
||||
export function getSelectedPluginKey(state: State): string | undefined {
|
||||
return state.selectedPlugin
|
||||
? getPluginKey(
|
||||
state.selectedAppId,
|
||||
state.selectedDevice,
|
||||
state.selectedPlugin,
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function isPluginEnabled(
|
||||
enabledPlugins: State['enabledPlugins'],
|
||||
enabledDevicePlugins: State['enabledDevicePlugins'],
|
||||
app: string | null,
|
||||
pluginId: string,
|
||||
): boolean {
|
||||
if (enabledDevicePlugins?.has(pluginId)) {
|
||||
return true;
|
||||
}
|
||||
if (!app || !enabledPlugins) {
|
||||
return false;
|
||||
}
|
||||
const appInfo = deconstructClientId(app);
|
||||
const enabledAppPlugins = enabledPlugins[appInfo.app];
|
||||
return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1;
|
||||
}
|
||||
|
||||
export function getAllClients(state: State): readonly Client[] {
|
||||
return Array.from(state.clients.values());
|
||||
}
|
||||
272
desktop/flipper-ui-core/src/reducers/healthchecks.tsx
Normal file
272
desktop/flipper-ui-core/src/reducers/healthchecks.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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 {Actions} from './';
|
||||
import {produce} from 'immer';
|
||||
import {Healthchecks} from 'flipper-doctor';
|
||||
|
||||
export type State = {
|
||||
healthcheckReport: HealthcheckReport;
|
||||
acknowledgedProblems: string[];
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'START_HEALTHCHECKS';
|
||||
payload: Healthchecks;
|
||||
}
|
||||
| {
|
||||
type: 'FINISH_HEALTHCHECKS';
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_HEALTHCHECK_RESULT';
|
||||
payload: {
|
||||
categoryKey: string;
|
||||
itemKey: string;
|
||||
result: HealthcheckResult;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'ACKNOWLEDGE_PROBLEMS';
|
||||
}
|
||||
| {
|
||||
type: 'RESET_ACKNOWLEDGED_PROBLEMS';
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
healthcheckReport: {
|
||||
result: {status: 'IN_PROGRESS'},
|
||||
categories: {},
|
||||
},
|
||||
acknowledgedProblems: [],
|
||||
};
|
||||
|
||||
type Dictionary<T> = {[key: string]: T};
|
||||
|
||||
export type HealthcheckStatus =
|
||||
| 'IN_PROGRESS'
|
||||
| 'SUCCESS'
|
||||
| 'FAILED'
|
||||
| 'SKIPPED'
|
||||
| 'WARNING';
|
||||
|
||||
export type HealthcheckResult = {
|
||||
status: HealthcheckStatus;
|
||||
isAcknowledged?: boolean;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type HealthcheckReportItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
result: HealthcheckResult;
|
||||
};
|
||||
|
||||
export type HealthcheckReportCategory = {
|
||||
key: string;
|
||||
label: string;
|
||||
result: HealthcheckResult;
|
||||
checks: Dictionary<HealthcheckReportItem>;
|
||||
};
|
||||
|
||||
export type HealthcheckReport = {
|
||||
result: HealthcheckResult;
|
||||
categories: Dictionary<HealthcheckReportCategory>;
|
||||
};
|
||||
|
||||
function recomputeHealthcheckStatus(draft: State): void {
|
||||
draft.healthcheckReport.result = computeAggregatedResult(
|
||||
Object.values(draft.healthcheckReport.categories).map((c) => c.result),
|
||||
);
|
||||
}
|
||||
|
||||
function computeAggregatedResult(
|
||||
results: HealthcheckResult[],
|
||||
): HealthcheckResult {
|
||||
return results.some((r) => r.status === 'IN_PROGRESS')
|
||||
? {status: 'IN_PROGRESS'}
|
||||
: results.every((r) => r.status === 'SUCCESS')
|
||||
? {status: 'SUCCESS'}
|
||||
: results.some((r) => r.status === 'FAILED' && !r.isAcknowledged)
|
||||
? {status: 'FAILED', isAcknowledged: false}
|
||||
: results.some((r) => r.status === 'FAILED')
|
||||
? {status: 'FAILED', isAcknowledged: true}
|
||||
: results.some((r) => r.status === 'WARNING' && !r.isAcknowledged)
|
||||
? {status: 'WARNING', isAcknowledged: false}
|
||||
: results.some((r) => r.status === 'WARNING')
|
||||
? {status: 'WARNING', isAcknowledged: true}
|
||||
: {status: 'SKIPPED'};
|
||||
}
|
||||
|
||||
const updateCheckResult = produce(
|
||||
(
|
||||
draft: State,
|
||||
{
|
||||
categoryKey,
|
||||
itemKey,
|
||||
result,
|
||||
}: {
|
||||
categoryKey: string;
|
||||
itemKey: string;
|
||||
result: HealthcheckResult;
|
||||
},
|
||||
) => {
|
||||
const category = draft.healthcheckReport.categories[categoryKey];
|
||||
const item = category.checks[itemKey];
|
||||
Object.assign(item.result, result);
|
||||
item.result.isAcknowledged = draft.acknowledgedProblems.includes(item.key);
|
||||
},
|
||||
);
|
||||
|
||||
function createDict<T>(pairs: [string, T][]): Dictionary<T> {
|
||||
const obj: Dictionary<T> = {};
|
||||
for (const pair of pairs) {
|
||||
obj[pair[0]] = pair[1];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const start = produce((draft: State, healthchecks: Healthchecks) => {
|
||||
draft.healthcheckReport = {
|
||||
result: {status: 'IN_PROGRESS'},
|
||||
categories: createDict<HealthcheckReportCategory>(
|
||||
Object.entries(healthchecks).map(([categoryKey, category]) => {
|
||||
if (category.isSkipped) {
|
||||
return [
|
||||
categoryKey,
|
||||
{
|
||||
key: categoryKey,
|
||||
result: {
|
||||
status: 'SKIPPED',
|
||||
message: category.skipReason,
|
||||
},
|
||||
label: category.label,
|
||||
checks: createDict<HealthcheckReportItem>([]),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
categoryKey,
|
||||
{
|
||||
key: categoryKey,
|
||||
result: {status: 'IN_PROGRESS'},
|
||||
label: category.label,
|
||||
checks: createDict<HealthcheckReportItem>(
|
||||
category.healthchecks.map((check) => [
|
||||
check.key,
|
||||
{
|
||||
key: check.key,
|
||||
result: {status: 'IN_PROGRESS'},
|
||||
label: check.label,
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const finish = produce((draft: State) => {
|
||||
Object.values(draft.healthcheckReport.categories)
|
||||
.filter((cat) => cat.result.status !== 'SKIPPED')
|
||||
.forEach((cat) => {
|
||||
cat.result.message = undefined;
|
||||
cat.result = computeAggregatedResult(
|
||||
Object.values(cat.checks).map((c) => c.result),
|
||||
);
|
||||
});
|
||||
recomputeHealthcheckStatus(draft);
|
||||
if (draft.healthcheckReport.result.status === 'SUCCESS') {
|
||||
setAcknowledgedProblemsToEmpty(draft);
|
||||
}
|
||||
});
|
||||
|
||||
const acknowledge = produce((draft: State) => {
|
||||
draft.acknowledgedProblems = ([] as string[]).concat(
|
||||
...Object.values(draft.healthcheckReport.categories).map((cat) =>
|
||||
Object.values(cat.checks)
|
||||
.filter(
|
||||
(chk) =>
|
||||
chk.result.status === 'FAILED' || chk.result.status === 'WARNING',
|
||||
)
|
||||
.map((chk) => chk.key),
|
||||
),
|
||||
);
|
||||
Object.values(draft.healthcheckReport.categories).forEach((cat) => {
|
||||
cat.result.isAcknowledged = true;
|
||||
Object.values(cat.checks).forEach((chk) => {
|
||||
chk.result.isAcknowledged = true;
|
||||
});
|
||||
});
|
||||
recomputeHealthcheckStatus(draft);
|
||||
});
|
||||
|
||||
function setAcknowledgedProblemsToEmpty(draft: State) {
|
||||
draft.acknowledgedProblems = [];
|
||||
Object.values(draft.healthcheckReport.categories).forEach((cat) => {
|
||||
cat.result.isAcknowledged = false;
|
||||
Object.values(cat.checks).forEach((chk) => {
|
||||
chk.result.isAcknowledged = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const resetAcknowledged = produce((draft: State) => {
|
||||
setAcknowledgedProblemsToEmpty(draft);
|
||||
recomputeHealthcheckStatus(draft);
|
||||
});
|
||||
|
||||
export default function reducer(
|
||||
draft: State | undefined = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
return action.type === 'START_HEALTHCHECKS'
|
||||
? start(draft, action.payload)
|
||||
: action.type === 'FINISH_HEALTHCHECKS'
|
||||
? finish(draft)
|
||||
: action.type === 'UPDATE_HEALTHCHECK_RESULT'
|
||||
? updateCheckResult(draft, action.payload)
|
||||
: action.type === 'ACKNOWLEDGE_PROBLEMS'
|
||||
? acknowledge(draft)
|
||||
: action.type === 'RESET_ACKNOWLEDGED_PROBLEMS'
|
||||
? resetAcknowledged(draft)
|
||||
: draft;
|
||||
}
|
||||
|
||||
export const updateHealthcheckResult = (
|
||||
categoryKey: string,
|
||||
itemKey: string,
|
||||
result: HealthcheckResult,
|
||||
): Action => ({
|
||||
type: 'UPDATE_HEALTHCHECK_RESULT',
|
||||
payload: {
|
||||
categoryKey,
|
||||
itemKey,
|
||||
result,
|
||||
},
|
||||
});
|
||||
|
||||
export const startHealthchecks = (healthchecks: Healthchecks): Action => ({
|
||||
type: 'START_HEALTHCHECKS',
|
||||
payload: healthchecks,
|
||||
});
|
||||
|
||||
export const finishHealthchecks = (): Action => ({
|
||||
type: 'FINISH_HEALTHCHECKS',
|
||||
});
|
||||
|
||||
export const acknowledgeProblems = (): Action => ({
|
||||
type: 'ACKNOWLEDGE_PROBLEMS',
|
||||
});
|
||||
|
||||
export const resetAcknowledgedProblems = (): Action => ({
|
||||
type: 'RESET_ACKNOWLEDGED_PROBLEMS',
|
||||
});
|
||||
209
desktop/flipper-ui-core/src/reducers/index.tsx
Normal file
209
desktop/flipper-ui-core/src/reducers/index.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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 {combineReducers, Dispatch} from 'redux';
|
||||
import application, {
|
||||
State as ApplicationState,
|
||||
Action as ApplicationAction,
|
||||
} from './application';
|
||||
import connections, {
|
||||
State as DevicesState,
|
||||
Action as DevicesAction,
|
||||
persistMigrations as devicesPersistMigrations,
|
||||
persistVersion as devicesPersistVersion,
|
||||
} from './connections';
|
||||
import pluginMessageQueue, {
|
||||
State as PluginMessageQueueState,
|
||||
Action as PluginMessageQueueAction,
|
||||
} from './pluginMessageQueue';
|
||||
import notifications, {
|
||||
State as NotificationsState,
|
||||
Action as NotificationsAction,
|
||||
} from './notifications';
|
||||
import plugins, {
|
||||
State as PluginsState,
|
||||
Action as PluginsAction,
|
||||
persistMigrations as pluginsPersistMigrations,
|
||||
persistVersion as pluginsPersistVersion,
|
||||
} from './plugins';
|
||||
import supportForm, {
|
||||
State as SupportFormState,
|
||||
Action as SupportFormAction,
|
||||
} from './supportForm';
|
||||
import settings, {
|
||||
Settings as SettingsState,
|
||||
Action as SettingsAction,
|
||||
} from './settings';
|
||||
import launcherSettings, {
|
||||
LauncherSettings as LauncherSettingsState,
|
||||
Action as LauncherSettingsAction,
|
||||
} from './launcherSettings';
|
||||
import pluginManager, {
|
||||
State as PluginManagerState,
|
||||
Action as PluginManagerAction,
|
||||
} from './pluginManager';
|
||||
import healthchecks, {
|
||||
Action as HealthcheckAction,
|
||||
State as HealthcheckState,
|
||||
} from './healthchecks';
|
||||
import pluginDownloads, {
|
||||
State as PluginDownloadsState,
|
||||
Action as PluginDownloadsAction,
|
||||
} from './pluginDownloads';
|
||||
import usageTracking, {
|
||||
Action as TrackingAction,
|
||||
State as TrackingState,
|
||||
} from './usageTracking';
|
||||
import user, {State as UserState, Action as UserAction} from './user';
|
||||
import JsonFileStorage from '../utils/jsonFileReduxPersistStorage';
|
||||
import LauncherSettingsStorage from '../utils/launcherSettingsStorage';
|
||||
import {launcherConfigDir} from '../utils/launcher';
|
||||
import os from 'os';
|
||||
import {resolve} from 'path';
|
||||
import xdg from 'xdg-basedir';
|
||||
import {createMigrate, createTransform, persistReducer} from 'redux-persist';
|
||||
import {PersistPartial} from 'redux-persist/es/persistReducer';
|
||||
|
||||
import {Store as ReduxStore, MiddlewareAPI as ReduxMiddlewareAPI} from 'redux';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import {TransformConfig} from 'redux-persist/es/createTransform';
|
||||
|
||||
export type Actions =
|
||||
| ApplicationAction
|
||||
| DevicesAction
|
||||
| PluginMessageQueueAction
|
||||
| NotificationsAction
|
||||
| PluginsAction
|
||||
| UserAction
|
||||
| SettingsAction
|
||||
| LauncherSettingsAction
|
||||
| SupportFormAction
|
||||
| PluginManagerAction
|
||||
| HealthcheckAction
|
||||
| TrackingAction
|
||||
| PluginDownloadsAction
|
||||
| {type: 'INIT'};
|
||||
|
||||
export type State = {
|
||||
application: ApplicationState;
|
||||
connections: DevicesState & PersistPartial;
|
||||
pluginMessageQueue: PluginMessageQueueState;
|
||||
notifications: NotificationsState & PersistPartial;
|
||||
plugins: PluginsState & PersistPartial;
|
||||
user: UserState & PersistPartial;
|
||||
settingsState: SettingsState & PersistPartial;
|
||||
launcherSettingsState: LauncherSettingsState & PersistPartial;
|
||||
supportForm: SupportFormState;
|
||||
pluginManager: PluginManagerState;
|
||||
healthchecks: HealthcheckState & PersistPartial;
|
||||
usageTracking: TrackingState;
|
||||
pluginDownloads: PluginDownloadsState;
|
||||
};
|
||||
|
||||
export type Store = ReduxStore<State, Actions>;
|
||||
export type MiddlewareAPI = ReduxMiddlewareAPI<Dispatch<Actions>, State>;
|
||||
|
||||
const settingsStorage = new JsonFileStorage(
|
||||
resolve(
|
||||
...(xdg.config ? [xdg.config] : [os.homedir(), '.config']),
|
||||
'flipper',
|
||||
'settings.json',
|
||||
),
|
||||
);
|
||||
|
||||
const setTransformer = (config: TransformConfig) =>
|
||||
createTransform(
|
||||
(set: Set<string>) => Array.from(set),
|
||||
(arrayString: string[]) => new Set(arrayString),
|
||||
config,
|
||||
);
|
||||
|
||||
const launcherSettingsStorage = new LauncherSettingsStorage(
|
||||
resolve(launcherConfigDir(), 'flipper-launcher.toml'),
|
||||
);
|
||||
|
||||
export function createRootReducer() {
|
||||
return combineReducers<State, Actions>({
|
||||
application,
|
||||
connections: persistReducer<DevicesState, Actions>(
|
||||
{
|
||||
key: 'connections',
|
||||
storage,
|
||||
whitelist: [
|
||||
'userPreferredDevice',
|
||||
'userPreferredPlugin',
|
||||
'userPreferredApp',
|
||||
'enabledPlugins',
|
||||
'enabledDevicePlugins',
|
||||
],
|
||||
transforms: [
|
||||
setTransformer({
|
||||
whitelist: ['enabledDevicePlugins', 'userStarredDevicePlugins'],
|
||||
}),
|
||||
],
|
||||
version: devicesPersistVersion,
|
||||
migrate: createMigrate(devicesPersistMigrations),
|
||||
},
|
||||
connections,
|
||||
),
|
||||
pluginMessageQueue: pluginMessageQueue as any,
|
||||
notifications: persistReducer(
|
||||
{
|
||||
key: 'notifications',
|
||||
storage,
|
||||
whitelist: ['blacklistedPlugins', 'blacklistedCategories'],
|
||||
},
|
||||
notifications,
|
||||
),
|
||||
plugins: persistReducer<PluginsState, Actions>(
|
||||
{
|
||||
key: 'plugins',
|
||||
storage,
|
||||
whitelist: ['marketplacePlugins', 'uninstalledPluginNames'],
|
||||
transforms: [setTransformer({whitelist: ['uninstalledPluginNames']})],
|
||||
version: pluginsPersistVersion,
|
||||
migrate: createMigrate(pluginsPersistMigrations),
|
||||
},
|
||||
plugins,
|
||||
),
|
||||
supportForm,
|
||||
pluginManager,
|
||||
user: persistReducer(
|
||||
{
|
||||
key: 'user',
|
||||
storage,
|
||||
},
|
||||
user,
|
||||
),
|
||||
settingsState: persistReducer(
|
||||
{key: 'settings', storage: settingsStorage},
|
||||
settings,
|
||||
),
|
||||
launcherSettingsState: persistReducer(
|
||||
{
|
||||
key: 'launcherSettings',
|
||||
storage: launcherSettingsStorage,
|
||||
serialize: false,
|
||||
// @ts-ignore: property is erroneously missing in redux-persist type definitions
|
||||
deserialize: false,
|
||||
},
|
||||
launcherSettings,
|
||||
),
|
||||
healthchecks: persistReducer<HealthcheckState, Actions>(
|
||||
{
|
||||
key: 'healthchecks',
|
||||
storage,
|
||||
whitelist: ['acknowledgedProblems'],
|
||||
},
|
||||
healthchecks,
|
||||
),
|
||||
usageTracking,
|
||||
pluginDownloads,
|
||||
});
|
||||
}
|
||||
43
desktop/flipper-ui-core/src/reducers/launcherSettings.tsx
Normal file
43
desktop/flipper-ui-core/src/reducers/launcherSettings.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 {Actions} from './index';
|
||||
import ReleaseChannel from '../ReleaseChannel';
|
||||
|
||||
export type LauncherSettings = {
|
||||
releaseChannel: ReleaseChannel;
|
||||
ignoreLocalPin: boolean;
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
type: 'UPDATE_LAUNCHER_SETTINGS';
|
||||
payload: LauncherSettings;
|
||||
};
|
||||
|
||||
export const defaultLauncherSettings: LauncherSettings = {
|
||||
releaseChannel: ReleaseChannel.DEFAULT,
|
||||
ignoreLocalPin: false,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: LauncherSettings = defaultLauncherSettings,
|
||||
action: Actions,
|
||||
): LauncherSettings {
|
||||
if (action.type === 'UPDATE_LAUNCHER_SETTINGS') {
|
||||
return action.payload;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function updateLauncherSettings(settings: LauncherSettings): Action {
|
||||
return {
|
||||
type: 'UPDATE_LAUNCHER_SETTINGS',
|
||||
payload: settings,
|
||||
};
|
||||
}
|
||||
259
desktop/flipper-ui-core/src/reducers/notifications.tsx
Normal file
259
desktop/flipper-ui-core/src/reducers/notifications.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* 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 {Notification} from 'flipper-plugin';
|
||||
import {Actions} from './';
|
||||
import React from 'react';
|
||||
import {getStringFromErrorLike} from 'flipper-common';
|
||||
|
||||
export const GLOBAL_NOTIFICATION_PLUGIN_ID = 'Flipper';
|
||||
|
||||
export type PluginNotification = {
|
||||
notification: Notification;
|
||||
pluginId: string;
|
||||
client: null | string; // id
|
||||
};
|
||||
|
||||
export type PluginNotificationReference = {
|
||||
notificationId: string;
|
||||
pluginId: string;
|
||||
client: null | string; // id
|
||||
};
|
||||
|
||||
export type State = {
|
||||
activeNotifications: Array<PluginNotification>;
|
||||
invalidatedNotifications: Array<PluginNotification>;
|
||||
blocklistedPlugins: Array<string>;
|
||||
blocklistedCategories: Array<string>;
|
||||
clearedNotifications: Set<string>;
|
||||
};
|
||||
|
||||
type ActiveNotificationsAction = {
|
||||
type: 'SET_ACTIVE_NOTIFICATIONS';
|
||||
payload: {
|
||||
notifications: Array<Notification>;
|
||||
client: null | string;
|
||||
pluginId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'CLEAR_ALL_NOTIFICATIONS';
|
||||
}
|
||||
| {
|
||||
type: 'SET_ACTIVE_NOTIFICATIONS';
|
||||
payload: {
|
||||
notifications: Array<Notification>;
|
||||
client: null | string;
|
||||
pluginId: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_PLUGIN_BLOCKLIST';
|
||||
payload: Array<string>;
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE_CATEGORY_BLOCKLIST';
|
||||
payload: Array<string>;
|
||||
}
|
||||
| {
|
||||
type: 'ADD_NOTIFICATION';
|
||||
payload: PluginNotification;
|
||||
}
|
||||
| {
|
||||
type: 'REMOVE_NOTIFICATION';
|
||||
payload: PluginNotificationReference;
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
activeNotifications: [],
|
||||
invalidatedNotifications: [],
|
||||
blocklistedPlugins: [],
|
||||
blocklistedCategories: [],
|
||||
clearedNotifications: new Set(),
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'SET_ACTIVE_NOTIFICATIONS': {
|
||||
return activeNotificationsReducer(state, action);
|
||||
}
|
||||
case 'CLEAR_ALL_NOTIFICATIONS':
|
||||
const markAsCleared = ({
|
||||
pluginId,
|
||||
notification: {id},
|
||||
}: PluginNotification) =>
|
||||
state.clearedNotifications.add(`${pluginId}#${id}`);
|
||||
|
||||
state.activeNotifications.forEach(markAsCleared);
|
||||
state.invalidatedNotifications.forEach(markAsCleared);
|
||||
// Q: Should this actually delete them, or just invalidate them?
|
||||
return {
|
||||
...state,
|
||||
activeNotifications: [],
|
||||
invalidatedNotifications: [],
|
||||
};
|
||||
case 'UPDATE_PLUGIN_BLOCKLIST':
|
||||
return {
|
||||
...state,
|
||||
blocklistedPlugins: action.payload,
|
||||
};
|
||||
case 'UPDATE_CATEGORY_BLOCKLIST':
|
||||
return {
|
||||
...state,
|
||||
blocklistedCategories: action.payload,
|
||||
};
|
||||
case 'ADD_NOTIFICATION':
|
||||
return {
|
||||
...state,
|
||||
// while adding notifications, remove old duplicates
|
||||
activeNotifications: [
|
||||
...state.activeNotifications.filter(
|
||||
(notif) =>
|
||||
notif.client !== action.payload.client ||
|
||||
notif.pluginId !== action.payload.pluginId ||
|
||||
notif.notification.id !== action.payload.notification.id,
|
||||
),
|
||||
action.payload,
|
||||
],
|
||||
};
|
||||
case 'REMOVE_NOTIFICATION':
|
||||
return {
|
||||
...state,
|
||||
activeNotifications: [
|
||||
...state.activeNotifications.filter(
|
||||
(notif) =>
|
||||
notif.client !== action.payload.client ||
|
||||
notif.pluginId !== action.payload.pluginId ||
|
||||
notif.notification.id !== action.payload.notificationId,
|
||||
),
|
||||
],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function activeNotificationsReducer(
|
||||
state: State,
|
||||
action: ActiveNotificationsAction,
|
||||
): State {
|
||||
const {payload} = action;
|
||||
const newActiveNotifications = [];
|
||||
const newInactivatedNotifications = state.invalidatedNotifications.slice();
|
||||
|
||||
const newIDs = new Set(payload.notifications.map((n: Notification) => n.id));
|
||||
|
||||
for (const activeNotification of state.activeNotifications) {
|
||||
if (activeNotification.pluginId !== payload.pluginId) {
|
||||
newActiveNotifications.push(activeNotification);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!newIDs.has(activeNotification.notification.id)) {
|
||||
newInactivatedNotifications.push(activeNotification);
|
||||
}
|
||||
}
|
||||
|
||||
payload.notifications
|
||||
.filter(
|
||||
({id}: Notification) =>
|
||||
!state.clearedNotifications.has(`${payload.pluginId}#${id}`),
|
||||
)
|
||||
.forEach((notification: Notification) => {
|
||||
newActiveNotifications.push({
|
||||
pluginId: payload.pluginId,
|
||||
client: payload.client,
|
||||
notification,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
activeNotifications: newActiveNotifications,
|
||||
invalidatedNotifications: newInactivatedNotifications,
|
||||
};
|
||||
}
|
||||
|
||||
export function addNotification(payload: PluginNotification): Action {
|
||||
return {
|
||||
type: 'ADD_NOTIFICATION',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeNotification(
|
||||
payload: PluginNotificationReference,
|
||||
): Action {
|
||||
return {
|
||||
type: 'REMOVE_NOTIFICATION',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function addErrorNotification(
|
||||
title: string,
|
||||
message: string | React.ReactNode,
|
||||
error?: any,
|
||||
): Action {
|
||||
// TODO: use this method for https://github.com/facebook/flipper/pull/1478/files as well
|
||||
console.warn(title, message, error);
|
||||
return addNotification({
|
||||
client: null,
|
||||
pluginId: GLOBAL_NOTIFICATION_PLUGIN_ID,
|
||||
notification: {
|
||||
id: title,
|
||||
title,
|
||||
message: error ? (
|
||||
<>
|
||||
<p>{message}</p>
|
||||
<p>{getStringFromErrorLike(error)}</p>
|
||||
</>
|
||||
) : (
|
||||
message
|
||||
),
|
||||
severity: 'error',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function setActiveNotifications(payload: {
|
||||
notifications: Array<Notification>;
|
||||
client: null | string;
|
||||
pluginId: string;
|
||||
}): Action {
|
||||
return {
|
||||
type: 'SET_ACTIVE_NOTIFICATIONS',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearAllNotifications(): Action {
|
||||
return {
|
||||
type: 'CLEAR_ALL_NOTIFICATIONS',
|
||||
};
|
||||
}
|
||||
|
||||
export function updatePluginBlocklist(payload: Array<string>): Action {
|
||||
return {
|
||||
type: 'UPDATE_PLUGIN_BLOCKLIST',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCategoryBlocklist(payload: Array<string>): Action {
|
||||
return {
|
||||
type: 'UPDATE_CATEGORY_BLOCKLIST',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
146
desktop/flipper-ui-core/src/reducers/pluginDownloads.tsx
Normal file
146
desktop/flipper-ui-core/src/reducers/pluginDownloads.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 {
|
||||
DownloadablePluginDetails,
|
||||
getPluginVersionInstallationDir,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {Actions} from '.';
|
||||
import produce from 'immer';
|
||||
import {Canceler} from 'axios';
|
||||
|
||||
export enum PluginDownloadStatus {
|
||||
QUEUED = 'Queued',
|
||||
STARTED = 'Started',
|
||||
FAILED = 'Failed',
|
||||
}
|
||||
|
||||
export type DownloadablePluginState = {
|
||||
plugin: DownloadablePluginDetails;
|
||||
startedByUser: boolean;
|
||||
} & (
|
||||
| {status: PluginDownloadStatus.QUEUED}
|
||||
| {status: PluginDownloadStatus.STARTED; cancel: Canceler}
|
||||
);
|
||||
|
||||
// We use plugin installation path as key as it is unique for each plugin version.
|
||||
export type State = Record<string, DownloadablePluginState>;
|
||||
|
||||
export type PluginDownloadStart = {
|
||||
type: 'PLUGIN_DOWNLOAD_START';
|
||||
payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
startedByUser: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type PluginDownloadStarted = {
|
||||
type: 'PLUGIN_DOWNLOAD_STARTED';
|
||||
payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
cancel: Canceler;
|
||||
};
|
||||
};
|
||||
|
||||
export type PluginDownloadFinished = {
|
||||
type: 'PLUGIN_DOWNLOAD_FINISHED';
|
||||
payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
};
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| PluginDownloadStart
|
||||
| PluginDownloadStarted
|
||||
| PluginDownloadFinished;
|
||||
|
||||
const INITIAL_STATE: State = {};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'PLUGIN_DOWNLOAD_START': {
|
||||
const {plugin, startedByUser} = action.payload;
|
||||
const installationDir = getPluginVersionInstallationDir(
|
||||
plugin.name,
|
||||
plugin.version,
|
||||
);
|
||||
const downloadState = state[installationDir];
|
||||
if (downloadState) {
|
||||
// If download is already in progress - re-use the existing state.
|
||||
return produce(state, (draft) => {
|
||||
draft[installationDir] = {
|
||||
...downloadState,
|
||||
startedByUser: startedByUser || downloadState.startedByUser,
|
||||
};
|
||||
});
|
||||
}
|
||||
return produce(state, (draft) => {
|
||||
draft[installationDir] = {
|
||||
plugin,
|
||||
startedByUser: startedByUser,
|
||||
status: PluginDownloadStatus.QUEUED,
|
||||
};
|
||||
});
|
||||
}
|
||||
case 'PLUGIN_DOWNLOAD_STARTED': {
|
||||
const {plugin, cancel} = action.payload;
|
||||
const installationDir = getPluginVersionInstallationDir(
|
||||
plugin.name,
|
||||
plugin.version,
|
||||
);
|
||||
const downloadState = state[installationDir];
|
||||
if (downloadState?.status !== PluginDownloadStatus.QUEUED) {
|
||||
console.warn(
|
||||
`Invalid state transition PLUGIN_DOWNLOAD_STARTED in status ${downloadState?.status} for download to directory ${installationDir}.`,
|
||||
);
|
||||
return state;
|
||||
}
|
||||
return produce(state, (draft) => {
|
||||
draft[installationDir] = {
|
||||
status: PluginDownloadStatus.STARTED,
|
||||
plugin,
|
||||
startedByUser: downloadState.startedByUser,
|
||||
cancel,
|
||||
};
|
||||
});
|
||||
}
|
||||
case 'PLUGIN_DOWNLOAD_FINISHED': {
|
||||
const {plugin} = action.payload;
|
||||
const installationDir = getPluginVersionInstallationDir(
|
||||
plugin.name,
|
||||
plugin.version,
|
||||
);
|
||||
return produce(state, (draft) => {
|
||||
delete draft[installationDir];
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const startPluginDownload = (payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
startedByUser: boolean;
|
||||
}): Action => ({
|
||||
type: 'PLUGIN_DOWNLOAD_START',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginDownloadStarted = (payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
cancel: Canceler;
|
||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_STARTED', payload});
|
||||
|
||||
export const pluginDownloadFinished = (payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_FINISHED', payload});
|
||||
124
desktop/flipper-ui-core/src/reducers/pluginManager.tsx
Normal file
124
desktop/flipper-ui-core/src/reducers/pluginManager.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 type {Actions} from './';
|
||||
import type {ActivatablePluginDetails} from 'flipper-plugin-lib';
|
||||
import type {PluginDefinition} from '../plugin';
|
||||
import {produce} from 'immer';
|
||||
|
||||
export type State = {
|
||||
pluginCommandsQueue: PluginCommand[];
|
||||
};
|
||||
|
||||
export type PluginCommand =
|
||||
| LoadPluginAction
|
||||
| UninstallPluginAction
|
||||
| UpdatePluginAction
|
||||
| SwitchPluginAction;
|
||||
|
||||
export type LoadPluginActionPayload = {
|
||||
plugin: ActivatablePluginDetails;
|
||||
enable: boolean;
|
||||
notifyIfFailed: boolean;
|
||||
};
|
||||
|
||||
export type LoadPluginAction = {
|
||||
type: 'LOAD_PLUGIN';
|
||||
payload: LoadPluginActionPayload;
|
||||
};
|
||||
|
||||
export type UninstallPluginActionPayload = {
|
||||
plugin: PluginDefinition;
|
||||
};
|
||||
|
||||
export type UninstallPluginAction = {
|
||||
type: 'UNINSTALL_PLUGIN';
|
||||
payload: UninstallPluginActionPayload;
|
||||
};
|
||||
|
||||
export type UpdatePluginActionPayload = {
|
||||
plugin: PluginDefinition;
|
||||
enablePlugin: boolean;
|
||||
};
|
||||
|
||||
export type UpdatePluginAction = {
|
||||
type: 'UPDATE_PLUGIN';
|
||||
payload: UpdatePluginActionPayload;
|
||||
};
|
||||
|
||||
export type SwitchPluginActionPayload = {
|
||||
plugin: PluginDefinition;
|
||||
selectedApp?: string;
|
||||
};
|
||||
|
||||
export type SwitchPluginAction = {
|
||||
type: 'SWITCH_PLUGIN';
|
||||
payload: SwitchPluginActionPayload;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'PLUGIN_COMMANDS_PROCESSED';
|
||||
payload: number;
|
||||
}
|
||||
| PluginCommand;
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
pluginCommandsQueue: [],
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'LOAD_PLUGIN':
|
||||
case 'UNINSTALL_PLUGIN':
|
||||
case 'UPDATE_PLUGIN':
|
||||
case 'SWITCH_PLUGIN':
|
||||
return produce(state, (draft) => {
|
||||
draft.pluginCommandsQueue.push(action);
|
||||
});
|
||||
case 'PLUGIN_COMMANDS_PROCESSED':
|
||||
return produce(state, (draft) => {
|
||||
draft.pluginCommandsQueue.splice(0, action.payload);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const uninstallPlugin = (
|
||||
payload: UninstallPluginActionPayload,
|
||||
): Action => ({
|
||||
type: 'UNINSTALL_PLUGIN',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const loadPlugin = (payload: LoadPluginActionPayload): Action => ({
|
||||
type: 'LOAD_PLUGIN',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginCommandsProcessed = (payload: number): Action => ({
|
||||
type: 'PLUGIN_COMMANDS_PROCESSED',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerPluginUpdate = (
|
||||
payload: UpdatePluginActionPayload,
|
||||
): Action => ({
|
||||
type: 'UPDATE_PLUGIN',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const switchPlugin = (payload: SwitchPluginActionPayload): Action => ({
|
||||
type: 'SWITCH_PLUGIN',
|
||||
payload,
|
||||
});
|
||||
141
desktop/flipper-ui-core/src/reducers/pluginMessageQueue.tsx
Normal file
141
desktop/flipper-ui-core/src/reducers/pluginMessageQueue.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 produce from 'immer';
|
||||
import {deconstructPluginKey} from 'flipper-common';
|
||||
|
||||
export const DEFAULT_MAX_QUEUE_SIZE = 10000;
|
||||
|
||||
export type Message = {
|
||||
method: string;
|
||||
params?: any;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
[pluginKey: string]: Message[];
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'QUEUE_MESSAGES';
|
||||
payload: {
|
||||
pluginKey: string; // client + plugin
|
||||
maxQueueSize: number;
|
||||
messages: Message[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR_MESSAGE_QUEUE';
|
||||
payload: {
|
||||
pluginKey: string; // client + plugin
|
||||
amount?: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR_CLIENT_PLUGINS_STATE';
|
||||
payload: {clientId: string; devicePlugins: Set<string>};
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR_PLUGIN_STATE';
|
||||
payload: {pluginId: string};
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {};
|
||||
|
||||
export default function reducer(
|
||||
state: State | undefined = INITIAL_STATE,
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'QUEUE_MESSAGES': {
|
||||
const {pluginKey, messages, maxQueueSize} = action.payload;
|
||||
// this is hit very often, so try to do it a bit optimal
|
||||
const currentMessages = state[pluginKey] || [];
|
||||
let newMessages = currentMessages.concat(messages);
|
||||
if (newMessages.length > maxQueueSize) {
|
||||
// only keep last 90% of max queue size
|
||||
newMessages = newMessages.slice(
|
||||
newMessages.length - 1 - Math.ceil(maxQueueSize * 0.9),
|
||||
);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
[pluginKey]: newMessages,
|
||||
};
|
||||
}
|
||||
|
||||
case 'CLEAR_MESSAGE_QUEUE': {
|
||||
const {pluginKey, amount} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
const messages = draft[pluginKey];
|
||||
if (messages) {
|
||||
if (amount === undefined) {
|
||||
delete draft[pluginKey];
|
||||
} else {
|
||||
messages.splice(0, amount);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case 'CLEAR_CLIENT_PLUGINS_STATE': {
|
||||
const {payload} = action;
|
||||
return Object.keys(state).reduce((newState: State, pluginKey) => {
|
||||
// Only add the pluginState, if its from a plugin other than the one that
|
||||
// was removed. pluginKeys are in the form of ${clientID}#${pluginID}.
|
||||
const plugin = deconstructPluginKey(pluginKey);
|
||||
const clientId = plugin.client;
|
||||
const pluginId = plugin.pluginName;
|
||||
if (
|
||||
clientId !== payload.clientId ||
|
||||
(pluginId && payload.devicePlugins.has(pluginId))
|
||||
) {
|
||||
newState[pluginKey] = state[pluginKey];
|
||||
}
|
||||
return newState;
|
||||
}, {});
|
||||
}
|
||||
|
||||
case 'CLEAR_PLUGIN_STATE': {
|
||||
const {pluginId} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
Object.keys(draft).forEach((pluginKey) => {
|
||||
const pluginKeyParts = deconstructPluginKey(pluginKey);
|
||||
if (pluginKeyParts.pluginName === pluginId) {
|
||||
delete draft[pluginKey];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const queueMessages = (
|
||||
pluginKey: string,
|
||||
messages: Message[],
|
||||
maxQueueSize: number | undefined,
|
||||
): Action => ({
|
||||
type: 'QUEUE_MESSAGES',
|
||||
payload: {
|
||||
pluginKey,
|
||||
messages,
|
||||
maxQueueSize: maxQueueSize || DEFAULT_MAX_QUEUE_SIZE,
|
||||
},
|
||||
});
|
||||
|
||||
export const clearMessageQueue = (
|
||||
pluginKey: string,
|
||||
amount?: number,
|
||||
): Action => ({
|
||||
type: 'CLEAR_MESSAGE_QUEUE',
|
||||
payload: {pluginKey, amount},
|
||||
});
|
||||
309
desktop/flipper-ui-core/src/reducers/plugins.tsx
Normal file
309
desktop/flipper-ui-core/src/reducers/plugins.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 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 type {
|
||||
DevicePluginMap,
|
||||
ClientPluginMap,
|
||||
PluginDefinition,
|
||||
} from '../plugin';
|
||||
import type {
|
||||
DownloadablePluginDetails,
|
||||
ActivatablePluginDetails,
|
||||
BundledPluginDetails,
|
||||
InstalledPluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import type {Actions} from '.';
|
||||
import produce from 'immer';
|
||||
import {isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||
import semver from 'semver';
|
||||
|
||||
export interface MarketplacePluginDetails extends DownloadablePluginDetails {
|
||||
availableVersions?: DownloadablePluginDetails[];
|
||||
}
|
||||
|
||||
export type State = StateV1;
|
||||
|
||||
type StateV1 = {
|
||||
devicePlugins: DevicePluginMap;
|
||||
clientPlugins: ClientPluginMap;
|
||||
loadedPlugins: Map<string, ActivatablePluginDetails>;
|
||||
bundledPlugins: Map<string, BundledPluginDetails>;
|
||||
gatekeepedPlugins: Array<ActivatablePluginDetails>;
|
||||
disabledPlugins: Array<ActivatablePluginDetails>;
|
||||
failedPlugins: Array<[ActivatablePluginDetails, string]>;
|
||||
selectedPlugins: Array<string>;
|
||||
marketplacePlugins: Array<MarketplacePluginDetails>;
|
||||
uninstalledPluginNames: Set<string>;
|
||||
installedPlugins: Map<string, InstalledPluginDetails>;
|
||||
initialized: boolean;
|
||||
};
|
||||
|
||||
type StateV0 = Omit<StateV1, 'uninstalledPluginNames'> & {
|
||||
uninstalledPlugins: Set<string>;
|
||||
};
|
||||
|
||||
export const persistVersion = 1;
|
||||
export const persistMigrations = {
|
||||
1: (state: any) => {
|
||||
const stateV0 = state as StateV0;
|
||||
const stateV1: StateV1 = {
|
||||
...stateV0,
|
||||
uninstalledPluginNames: new Set(stateV0.uninstalledPlugins),
|
||||
};
|
||||
return stateV1 as any;
|
||||
},
|
||||
};
|
||||
|
||||
export type RegisterPluginAction = {
|
||||
type: 'REGISTER_PLUGINS';
|
||||
payload: PluginDefinition[];
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| RegisterPluginAction
|
||||
| {
|
||||
type: 'GATEKEEPED_PLUGINS';
|
||||
payload: Array<ActivatablePluginDetails>;
|
||||
}
|
||||
| {
|
||||
type: 'DISABLED_PLUGINS';
|
||||
payload: Array<ActivatablePluginDetails>;
|
||||
}
|
||||
| {
|
||||
type: 'FAILED_PLUGINS';
|
||||
payload: Array<[ActivatablePluginDetails, string]>;
|
||||
}
|
||||
| {
|
||||
type: 'SELECTED_PLUGINS';
|
||||
payload: Array<string>;
|
||||
}
|
||||
| {
|
||||
type: 'MARKETPLACE_PLUGINS';
|
||||
payload: Array<DownloadablePluginDetails>;
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_LOADED_PLUGINS';
|
||||
payload: Array<ActivatablePluginDetails>;
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_BUNDLED_PLUGINS';
|
||||
payload: Array<BundledPluginDetails>;
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||
payload: InstalledPluginDetails[];
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_INSTALLED';
|
||||
payload: InstalledPluginDetails;
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_UNINSTALLED';
|
||||
payload: ActivatablePluginDetails;
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_LOADED';
|
||||
payload: PluginDefinition;
|
||||
}
|
||||
| {
|
||||
type: 'PLUGINS_INITIALIZED';
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
failedPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPluginNames: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State | undefined = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
if (action.type === 'REGISTER_PLUGINS') {
|
||||
return produce(state, (draft) => {
|
||||
const {devicePlugins, clientPlugins} = draft;
|
||||
action.payload.forEach((p) => {
|
||||
if (devicePlugins.has(p.id) || clientPlugins.has(p.id)) {
|
||||
return;
|
||||
}
|
||||
if (isDevicePluginDefinition(p)) {
|
||||
devicePlugins.set(p.id, p);
|
||||
} else {
|
||||
clientPlugins.set(p.id, p);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (action.type === 'GATEKEEPED_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
gatekeepedPlugins: state.gatekeepedPlugins.concat(action.payload),
|
||||
};
|
||||
} else if (action.type === 'DISABLED_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
disabledPlugins: state.disabledPlugins.concat(action.payload),
|
||||
};
|
||||
} else if (action.type === 'FAILED_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
failedPlugins: state.failedPlugins.concat(action.payload),
|
||||
};
|
||||
} else if (action.type === 'SELECTED_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
selectedPlugins: action.payload,
|
||||
};
|
||||
} else if (action.type === 'MARKETPLACE_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
marketplacePlugins: action.payload,
|
||||
};
|
||||
} else if (action.type === 'REGISTER_LOADED_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
loadedPlugins: new Map(action.payload.map((p) => [p.id, p])),
|
||||
};
|
||||
} else if (action.type === 'REGISTER_BUNDLED_PLUGINS') {
|
||||
return {
|
||||
...state,
|
||||
bundledPlugins: new Map(action.payload.map((p) => [p.id, p])),
|
||||
};
|
||||
} else if (action.type === 'REGISTER_INSTALLED_PLUGINS') {
|
||||
return produce(state, (draft) => {
|
||||
draft.installedPlugins.clear();
|
||||
action.payload.forEach((p) => {
|
||||
if (!draft.uninstalledPluginNames.has(p.name)) {
|
||||
draft.installedPlugins.set(p.id, p);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_INSTALLED') {
|
||||
const plugin = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
const existing = draft.installedPlugins.get(plugin.name);
|
||||
if (!existing || semver.gt(plugin.version, existing.version)) {
|
||||
draft.installedPlugins.set(plugin.name, plugin);
|
||||
}
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_UNINSTALLED') {
|
||||
const plugin = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
draft.clientPlugins.delete(plugin.id);
|
||||
draft.devicePlugins.delete(plugin.id);
|
||||
draft.loadedPlugins.delete(plugin.id);
|
||||
draft.uninstalledPluginNames.add(plugin.name);
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_LOADED') {
|
||||
const plugin = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
if (isDevicePluginDefinition(plugin)) {
|
||||
draft.devicePlugins.set(plugin.id, plugin);
|
||||
} else {
|
||||
draft.clientPlugins.set(plugin.id, plugin);
|
||||
}
|
||||
draft.uninstalledPluginNames.delete(plugin.details.name);
|
||||
draft.loadedPlugins.set(plugin.id, plugin.details);
|
||||
});
|
||||
} else if (action.type === 'PLUGINS_INITIALIZED') {
|
||||
return produce(state, (draft) => {
|
||||
draft.initialized = true;
|
||||
});
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const selectedPlugins = (payload: Array<string>): Action => ({
|
||||
type: 'SELECTED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerPlugins = (payload: PluginDefinition[]): Action => ({
|
||||
type: 'REGISTER_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const addGatekeepedPlugins = (
|
||||
payload: Array<ActivatablePluginDetails>,
|
||||
): Action => ({
|
||||
type: 'GATEKEEPED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const addDisabledPlugins = (
|
||||
payload: Array<ActivatablePluginDetails>,
|
||||
): Action => ({
|
||||
type: 'DISABLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const addFailedPlugins = (
|
||||
payload: Array<[ActivatablePluginDetails, string]>,
|
||||
): Action => ({
|
||||
type: 'FAILED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerMarketplacePlugins = (
|
||||
payload: Array<DownloadablePluginDetails>,
|
||||
): Action => ({
|
||||
type: 'MARKETPLACE_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerLoadedPlugins = (
|
||||
payload: Array<ActivatablePluginDetails>,
|
||||
): Action => ({
|
||||
type: 'REGISTER_LOADED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerBundledPlugins = (
|
||||
payload: Array<BundledPluginDetails>,
|
||||
): Action => ({
|
||||
type: 'REGISTER_BUNDLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerInstalledPlugins = (
|
||||
payload: InstalledPluginDetails[],
|
||||
): Action => ({
|
||||
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({
|
||||
type: 'PLUGIN_INSTALLED',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginUninstalled = (
|
||||
payload: ActivatablePluginDetails,
|
||||
): Action => ({
|
||||
type: 'PLUGIN_UNINSTALLED',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginLoaded = (payload: PluginDefinition): Action => ({
|
||||
type: 'PLUGIN_LOADED',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginsInitialized = (): Action => ({
|
||||
type: 'PLUGINS_INITIALIZED',
|
||||
});
|
||||
111
desktop/flipper-ui-core/src/reducers/settings.tsx
Normal file
111
desktop/flipper-ui-core/src/reducers/settings.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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 {Actions} from './index';
|
||||
import os from 'os';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
export enum Tristate {
|
||||
True,
|
||||
False,
|
||||
Unset,
|
||||
}
|
||||
|
||||
export type Settings = {
|
||||
androidHome: string;
|
||||
enableAndroid: boolean;
|
||||
enableIOS: boolean;
|
||||
enablePhysicalIOS: boolean;
|
||||
/**
|
||||
* If unset, this will assume the value of the GK setting.
|
||||
* Note that this setting has no effect in the open source version
|
||||
* of Flipper.
|
||||
*/
|
||||
enablePrefetching: Tristate;
|
||||
idbPath: string;
|
||||
jsApps: {
|
||||
webAppLauncher: {
|
||||
url: string;
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
reactNative: {
|
||||
shortcuts: {
|
||||
enabled: boolean;
|
||||
reload: string;
|
||||
openDevMenu: string;
|
||||
};
|
||||
};
|
||||
darkMode: 'dark' | 'light' | 'system';
|
||||
showWelcomeAtStartup: boolean;
|
||||
suppressPluginErrors: boolean;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {type: 'INIT'}
|
||||
| {
|
||||
type: 'UPDATE_SETTINGS';
|
||||
payload: Settings;
|
||||
};
|
||||
|
||||
export const DEFAULT_ANDROID_SDK_PATH = getDefaultAndroidSdkPath();
|
||||
|
||||
const initialState: Settings = {
|
||||
androidHome: getDefaultAndroidSdkPath(),
|
||||
enableAndroid: true,
|
||||
enableIOS: os.platform() === 'darwin',
|
||||
enablePhysicalIOS: os.platform() === 'darwin',
|
||||
enablePrefetching: Tristate.Unset,
|
||||
idbPath: '/usr/local/bin/idb',
|
||||
jsApps: {
|
||||
webAppLauncher: {
|
||||
url: 'http://localhost:8888',
|
||||
height: 600,
|
||||
width: 800,
|
||||
},
|
||||
},
|
||||
reactNative: {
|
||||
shortcuts: {
|
||||
enabled: false,
|
||||
reload: 'Alt+Shift+R',
|
||||
openDevMenu: 'Alt+Shift+D',
|
||||
},
|
||||
},
|
||||
darkMode: 'light',
|
||||
showWelcomeAtStartup: true,
|
||||
suppressPluginErrors: false,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: Settings = initialState,
|
||||
action: Actions,
|
||||
): Settings {
|
||||
if (action.type === 'UPDATE_SETTINGS') {
|
||||
return action.payload;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function updateSettings(settings: Settings): Action {
|
||||
return {
|
||||
type: 'UPDATE_SETTINGS',
|
||||
payload: settings,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultAndroidSdkPath() {
|
||||
return os.platform() === 'win32' ? getWindowsSdkPath() : '/opt/android_sdk';
|
||||
}
|
||||
|
||||
function getWindowsSdkPath() {
|
||||
return `${
|
||||
getRenderHostInstance().paths.homePath
|
||||
}\\AppData\\Local\\android\\sdk`;
|
||||
}
|
||||
367
desktop/flipper-ui-core/src/reducers/supportForm.tsx
Normal file
367
desktop/flipper-ui-core/src/reducers/supportForm.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* 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 {Actions, Store} from './';
|
||||
import {setStaticView} from './connections';
|
||||
import {deconstructClientId} from 'flipper-common';
|
||||
import {switchPlugin} from './pluginManager';
|
||||
import {showStatusUpdatesForDuration} from '../utils/promiseTimeout';
|
||||
import {selectedPlugins as setSelectedPlugins} from './plugins';
|
||||
import {addStatusMessage, removeStatusMessage} from './application';
|
||||
import constants from '../fb-stubs/constants';
|
||||
import {getLogger} from 'flipper-common';
|
||||
import {logPlatformSuccessRate} from 'flipper-common';
|
||||
export const SUPPORT_FORM_PREFIX = 'support-form-v2';
|
||||
import {getExportablePlugins} from '../selectors/connections';
|
||||
import {DeviceOS} from 'flipper-plugin';
|
||||
|
||||
const {DEFAULT_SUPPORT_GROUP} = constants;
|
||||
|
||||
type SubmediaType =
|
||||
| {uploadID: string; status: 'Uploaded'}
|
||||
| {status: 'NotUploaded' | 'Uploading'};
|
||||
type MediaObject = SubmediaType & {
|
||||
description: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type GroupValidationErrors = {
|
||||
plugins: string | null;
|
||||
os: string | null;
|
||||
};
|
||||
|
||||
export class Group {
|
||||
constructor(
|
||||
name: string,
|
||||
workplaceGroupID: number,
|
||||
requiredPlugins: Array<string>,
|
||||
defaultPlugins: Array<string>,
|
||||
supportedOS: Array<DeviceOS>,
|
||||
deeplinkSuffix: string,
|
||||
papercuts?: string,
|
||||
) {
|
||||
this.name = name;
|
||||
this.requiredPlugins = requiredPlugins;
|
||||
this.defaultPlugins = defaultPlugins;
|
||||
this.workplaceGroupID = workplaceGroupID;
|
||||
this.supportedOS = supportedOS;
|
||||
this.deeplinkSuffix = deeplinkSuffix;
|
||||
this.papercuts = papercuts;
|
||||
}
|
||||
readonly name: string;
|
||||
requiredPlugins: Array<string>;
|
||||
defaultPlugins: Array<string>;
|
||||
workplaceGroupID: number;
|
||||
supportedOS: Array<DeviceOS>;
|
||||
deeplinkSuffix: string;
|
||||
papercuts?: string;
|
||||
|
||||
getPluginsToSelect(): Array<string> {
|
||||
return Array.from(
|
||||
new Set([...this.defaultPlugins, ...this.requiredPlugins]),
|
||||
);
|
||||
}
|
||||
|
||||
getValidationMessage(
|
||||
selectedPlugins: Array<string>,
|
||||
selectedOS: DeviceOS | null,
|
||||
): GroupValidationErrors {
|
||||
const nonSelectedPlugin: Array<string> = [];
|
||||
for (const plugin of this.requiredPlugins) {
|
||||
if (!selectedPlugins.includes(plugin)) {
|
||||
nonSelectedPlugin.push(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin validation
|
||||
let str: string | null =
|
||||
'should be exported if you want to submit to this group. Make sure, if your selected app supports those plugins, if so then enable it and select it from the plugin selection.';
|
||||
if (nonSelectedPlugin.length == 1) {
|
||||
str = `the ${nonSelectedPlugin.pop()} plugin ${str}`;
|
||||
} else if (nonSelectedPlugin.length > 1) {
|
||||
const lastPlugin = nonSelectedPlugin.pop();
|
||||
str = `the ${nonSelectedPlugin.join(',')} and ${lastPlugin} ${str}`;
|
||||
} else {
|
||||
// nonSelectedPlugin is empty
|
||||
str = null;
|
||||
}
|
||||
|
||||
// OS validation
|
||||
let osError: string | null = null;
|
||||
if (this.name !== 'Flipper') {
|
||||
if (!selectedOS) {
|
||||
osError = 'Please select an app from the drop down.';
|
||||
} else if (!this.supportedOS.includes(selectedOS)) {
|
||||
osError = `The group ${
|
||||
this.name
|
||||
} supports exports from ${this.supportedOS.join(
|
||||
', ',
|
||||
)}. But your selected device's OS is ${selectedOS}, which is unsupported.`;
|
||||
}
|
||||
}
|
||||
return {plugins: str, os: osError};
|
||||
}
|
||||
|
||||
handleSupportFormDeeplinks(store: Store) {
|
||||
getLogger().track('usage', 'support-form-source', {
|
||||
source: 'deeplink',
|
||||
group: this.name,
|
||||
});
|
||||
store.dispatch(
|
||||
setStaticView(require('../fb-stubs/SupportRequestFormV2').default),
|
||||
);
|
||||
const selectedAppId = store.getState().connections.selectedAppId;
|
||||
const selectedClient = store
|
||||
.getState()
|
||||
.connections.clients.get(selectedAppId!);
|
||||
let errorMessage: string | undefined = undefined;
|
||||
if (selectedAppId) {
|
||||
const {app} = deconstructClientId(selectedAppId);
|
||||
const enabledPlugins: Array<string> | null =
|
||||
store.getState().connections.enabledPlugins[app];
|
||||
const unsupportedPlugins = [];
|
||||
for (const requiredPlugin of this.requiredPlugins) {
|
||||
const requiredPluginEnabled =
|
||||
enabledPlugins != null && enabledPlugins.includes(requiredPlugin);
|
||||
if (
|
||||
selectedClient &&
|
||||
selectedClient.plugins.has(requiredPlugin) &&
|
||||
!requiredPluginEnabled
|
||||
) {
|
||||
const plugin =
|
||||
store.getState().plugins.clientPlugins.get(requiredPlugin) ||
|
||||
store.getState().plugins.devicePlugins.get(requiredPlugin)!;
|
||||
store.dispatch(
|
||||
switchPlugin({
|
||||
selectedApp: app,
|
||||
plugin,
|
||||
}),
|
||||
);
|
||||
} else if (
|
||||
!selectedClient ||
|
||||
!selectedClient.plugins.has(requiredPlugin)
|
||||
) {
|
||||
unsupportedPlugins.push(requiredPlugin);
|
||||
}
|
||||
}
|
||||
if (unsupportedPlugins.length > 0) {
|
||||
errorMessage = `The current client does not support ${unsupportedPlugins.join(
|
||||
', ',
|
||||
)}. Please change the app from the dropdown in the support form.`;
|
||||
logPlatformSuccessRate(`${SUPPORT_FORM_PREFIX}-deeplink`, {
|
||||
kind: 'failure',
|
||||
supportedOperation: true,
|
||||
error: errorMessage,
|
||||
});
|
||||
showStatusUpdatesForDuration(
|
||||
errorMessage,
|
||||
'Deeplink',
|
||||
10000,
|
||||
(payload) => {
|
||||
store.dispatch(addStatusMessage(payload));
|
||||
},
|
||||
(payload) => {
|
||||
store.dispatch(removeStatusMessage(payload));
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
errorMessage =
|
||||
'Selected app is null, thus the deeplink failed to enable required plugin.';
|
||||
showStatusUpdatesForDuration(
|
||||
'Please select an app and the device from the dropdown.',
|
||||
'Deeplink',
|
||||
10000,
|
||||
(payload) => {
|
||||
store.dispatch(addStatusMessage(payload));
|
||||
},
|
||||
(payload) => {
|
||||
store.dispatch(removeStatusMessage(payload));
|
||||
},
|
||||
);
|
||||
}
|
||||
store.dispatch(
|
||||
setSupportFormV2State({
|
||||
...store.getState().supportForm.supportFormV2,
|
||||
selectedGroup: this,
|
||||
}),
|
||||
);
|
||||
const pluginsList = getExportablePlugins(store.getState());
|
||||
|
||||
store.dispatch(
|
||||
setSelectedPlugins(
|
||||
this.getPluginsToSelect().filter((s) => {
|
||||
return pluginsList.map((s) => s.id).includes(s);
|
||||
}),
|
||||
),
|
||||
);
|
||||
logPlatformSuccessRate(
|
||||
`${SUPPORT_FORM_PREFIX}-deeplink`,
|
||||
errorMessage
|
||||
? {
|
||||
kind: 'failure',
|
||||
supportedOperation: true,
|
||||
error: errorMessage,
|
||||
}
|
||||
: {kind: 'success'},
|
||||
);
|
||||
}
|
||||
|
||||
getWarningMessage(
|
||||
state: Parameters<typeof getExportablePlugins>[0],
|
||||
): string | null {
|
||||
const activePersistentPlugins = getExportablePlugins(state);
|
||||
const emptyPlugins: Array<string> = [];
|
||||
for (const plugin of this.requiredPlugins) {
|
||||
if (
|
||||
!activePersistentPlugins.find((o) => {
|
||||
return o.id === plugin;
|
||||
})
|
||||
) {
|
||||
emptyPlugins.push(plugin);
|
||||
}
|
||||
}
|
||||
const commonStr = 'Are you sure you want to submit?';
|
||||
if (emptyPlugins.length == 1) {
|
||||
return `There is no data in ${emptyPlugins.pop()} plugin. ${commonStr}`;
|
||||
} else if (emptyPlugins.length > 1) {
|
||||
return `The following plugins have no data: ${emptyPlugins.join(
|
||||
',',
|
||||
)}. ${commonStr}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_GROUP = new Group(
|
||||
DEFAULT_SUPPORT_GROUP.name,
|
||||
DEFAULT_SUPPORT_GROUP.workplaceGroupID,
|
||||
DEFAULT_SUPPORT_GROUP.requiredPlugins,
|
||||
DEFAULT_SUPPORT_GROUP.defaultPlugins,
|
||||
DEFAULT_SUPPORT_GROUP.supportedOS,
|
||||
DEFAULT_SUPPORT_GROUP.deeplinkSuffix,
|
||||
DEFAULT_SUPPORT_GROUP.papercuts,
|
||||
);
|
||||
|
||||
export const SUPPORTED_GROUPS: Array<Group> = [
|
||||
DEFAULT_GROUP,
|
||||
...constants.SUPPORT_GROUPS.map(
|
||||
({
|
||||
name,
|
||||
workplaceGroupID,
|
||||
requiredPlugins,
|
||||
defaultPlugins,
|
||||
supportedOS,
|
||||
deeplinkSuffix,
|
||||
papercuts,
|
||||
}) => {
|
||||
return new Group(
|
||||
name,
|
||||
workplaceGroupID,
|
||||
requiredPlugins,
|
||||
defaultPlugins,
|
||||
supportedOS,
|
||||
deeplinkSuffix,
|
||||
papercuts,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
export type MediaType = Array<MediaObject>;
|
||||
export type SupportFormV2State = {
|
||||
title: string;
|
||||
description: string;
|
||||
commitHash: string;
|
||||
screenshots?: MediaType;
|
||||
videos?: MediaType;
|
||||
selectedGroup: Group;
|
||||
};
|
||||
|
||||
export type SupportFormRequestDetailsState = SupportFormV2State & {
|
||||
appName: string;
|
||||
};
|
||||
export type State = {
|
||||
webState: NTUsersFormData | null;
|
||||
supportFormV2: SupportFormV2State;
|
||||
};
|
||||
export type Action =
|
||||
| {
|
||||
type: 'SET_SUPPORT_FORM_STATE';
|
||||
payload: NTUsersFormData | null;
|
||||
}
|
||||
| {
|
||||
type: 'SET_SUPPORT_FORM_V2_STATE';
|
||||
payload: SupportFormV2State;
|
||||
}
|
||||
| {
|
||||
type: 'RESET_SUPPORT_FORM_V2_STATE';
|
||||
};
|
||||
|
||||
export type NTUsersFormData = {
|
||||
flipper_trace: string | null;
|
||||
};
|
||||
|
||||
export const initialState: () => State = () => ({
|
||||
webState: null,
|
||||
supportFormV2: {
|
||||
title: '',
|
||||
description: [
|
||||
'## Context',
|
||||
'What are you trying to accomplish at a high level? Feel free to include mocks and tasks.',
|
||||
'',
|
||||
'## Problem',
|
||||
"What's blocking you?",
|
||||
'',
|
||||
"## Workarounds I've Tried",
|
||||
'',
|
||||
].join('\n'),
|
||||
commitHash: '',
|
||||
appName: '',
|
||||
selectedGroup: DEFAULT_GROUP,
|
||||
},
|
||||
});
|
||||
export default function reducer(
|
||||
state: State | undefined,
|
||||
action: Actions,
|
||||
): State {
|
||||
state = state || initialState();
|
||||
if (action.type === 'SET_SUPPORT_FORM_STATE') {
|
||||
return {
|
||||
...state,
|
||||
webState: action.payload,
|
||||
};
|
||||
} else if (action.type === 'SET_SUPPORT_FORM_V2_STATE') {
|
||||
return {
|
||||
...state,
|
||||
supportFormV2: action.payload,
|
||||
};
|
||||
} else if (action.type === 'RESET_SUPPORT_FORM_V2_STATE') {
|
||||
return initialState();
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const setSupportFormState = (
|
||||
payload: NTUsersFormData | null,
|
||||
): Action => ({
|
||||
type: 'SET_SUPPORT_FORM_STATE',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const setSupportFormV2State = (payload: SupportFormV2State): Action => ({
|
||||
type: 'SET_SUPPORT_FORM_V2_STATE',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const resetSupportFormV2State = (): Action => ({
|
||||
type: 'RESET_SUPPORT_FORM_V2_STATE',
|
||||
});
|
||||
108
desktop/flipper-ui-core/src/reducers/usageTracking.tsx
Normal file
108
desktop/flipper-ui-core/src/reducers/usageTracking.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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 {produce} from 'immer';
|
||||
import {Actions} from './';
|
||||
import {SelectionInfo} from '../utils/info';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
export type TrackingEvent =
|
||||
| {
|
||||
type: 'WINDOW_FOCUS_CHANGE';
|
||||
time: number;
|
||||
isFocused: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'SELECTION_CHANGED';
|
||||
selectionKey: string | null;
|
||||
selection: SelectionInfo | null;
|
||||
time: number;
|
||||
}
|
||||
| {type: 'TIMELINE_START'; time: number; isFocused: boolean};
|
||||
|
||||
export type State = {
|
||||
timeline: TrackingEvent[];
|
||||
};
|
||||
const INITAL_STATE: () => State = () => ({
|
||||
timeline: [
|
||||
{
|
||||
type: 'TIMELINE_START',
|
||||
time: Date.now(),
|
||||
isFocused: getRenderHostInstance().hasFocus(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'windowIsFocused';
|
||||
payload: {isFocused: boolean; time: number};
|
||||
}
|
||||
| {type: 'CLEAR_TIMELINE'; payload: {time: number; isFocused: boolean}}
|
||||
| {
|
||||
type: 'SELECTION_CHANGED';
|
||||
payload: {selection: SelectionInfo; time: number};
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITAL_STATE(),
|
||||
action: Actions,
|
||||
): State {
|
||||
if (action.type === 'CLEAR_TIMELINE') {
|
||||
return {
|
||||
...state,
|
||||
timeline: [
|
||||
{
|
||||
type: 'TIMELINE_START',
|
||||
time: action.payload.time,
|
||||
isFocused: action.payload.isFocused,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (action.type === 'windowIsFocused') {
|
||||
return produce(state, (draft) => {
|
||||
draft.timeline.push({
|
||||
type: 'WINDOW_FOCUS_CHANGE',
|
||||
time: action.payload.time,
|
||||
isFocused: action.payload.isFocused,
|
||||
});
|
||||
});
|
||||
} else if (action.type === 'SELECTION_CHANGED') {
|
||||
const {selection, time} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
draft.timeline.push({
|
||||
type: 'SELECTION_CHANGED',
|
||||
time,
|
||||
selectionKey: selection?.plugin ? JSON.stringify(selection) : null,
|
||||
selection,
|
||||
});
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function clearTimeline(time: number): Action {
|
||||
return {
|
||||
type: 'CLEAR_TIMELINE',
|
||||
payload: {
|
||||
time,
|
||||
isFocused: getRenderHostInstance().hasFocus(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function selectionChanged(payload: {
|
||||
selection: SelectionInfo;
|
||||
time: number;
|
||||
}): Action {
|
||||
return {
|
||||
type: 'SELECTION_CHANGED',
|
||||
payload,
|
||||
};
|
||||
}
|
||||
56
desktop/flipper-ui-core/src/reducers/user.tsx
Normal file
56
desktop/flipper-ui-core/src/reducers/user.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 {Actions} from './';
|
||||
|
||||
export type User = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
profile_picture?: {
|
||||
uri: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type State = User;
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'LOGIN';
|
||||
payload: User;
|
||||
}
|
||||
| {
|
||||
type: 'LOGOUT';
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {};
|
||||
|
||||
export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
if (action.type === 'LOGOUT') {
|
||||
return {};
|
||||
} else if (action.type === 'LOGIN') {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const login = (payload: User): Action => ({
|
||||
type: 'LOGIN',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const logout = (): Action => ({
|
||||
type: 'LOGOUT',
|
||||
});
|
||||
Reference in New Issue
Block a user