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
457
desktop/flipper-ui-core/src/utils/pluginUtils.tsx
Normal file
457
desktop/flipper-ui-core/src/utils/pluginUtils.tsx
Normal file
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* 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 {PluginDefinition} from '../plugin';
|
||||
import type {State, Store} from '../reducers';
|
||||
import type {State as PluginsState} from '../reducers/plugins';
|
||||
import type BaseDevice from '../devices/BaseDevice';
|
||||
import type Client from '../Client';
|
||||
import type {
|
||||
ActivatablePluginDetails,
|
||||
BundledPluginDetails,
|
||||
DownloadablePluginDetails,
|
||||
PluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {getLatestCompatibleVersionOfEachPlugin} from '../dispatcher/plugins';
|
||||
import {getPluginKey} from './pluginKey';
|
||||
|
||||
export type PluginLists = {
|
||||
devicePlugins: PluginDefinition[];
|
||||
metroPlugins: PluginDefinition[];
|
||||
enabledPlugins: PluginDefinition[];
|
||||
disabledPlugins: PluginDefinition[];
|
||||
unavailablePlugins: [plugin: PluginDetails, reason: string][];
|
||||
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
|
||||
};
|
||||
|
||||
export type ActivePluginListItem =
|
||||
| {
|
||||
status: 'enabled';
|
||||
details: ActivatablePluginDetails;
|
||||
definition: PluginDefinition;
|
||||
}
|
||||
| {
|
||||
status: 'disabled';
|
||||
details: ActivatablePluginDetails;
|
||||
definition: PluginDefinition;
|
||||
}
|
||||
| {
|
||||
status: 'uninstalled';
|
||||
details: DownloadablePluginDetails | BundledPluginDetails;
|
||||
}
|
||||
| {
|
||||
status: 'unavailable';
|
||||
details: PluginDetails;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type ActivePluginList = Record<string, ActivePluginListItem | undefined>;
|
||||
|
||||
export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works
|
||||
|
||||
export function pluginsClassMap(
|
||||
plugins: PluginsState,
|
||||
): Map<string, PluginDefinition> {
|
||||
return new Map<string, PluginDefinition>([
|
||||
...plugins.clientPlugins.entries(),
|
||||
...plugins.devicePlugins.entries(),
|
||||
]);
|
||||
}
|
||||
|
||||
export function computeExportablePlugins(
|
||||
state: Pick<State, 'plugins' | 'connections' | 'pluginMessageQueue'>,
|
||||
device: BaseDevice | null,
|
||||
client: Client | null,
|
||||
availablePlugins: PluginLists,
|
||||
): {id: string; label: string}[] {
|
||||
return [
|
||||
...availablePlugins.devicePlugins.filter((plugin) => {
|
||||
return isExportablePlugin(state, device, client, plugin);
|
||||
}),
|
||||
...availablePlugins.enabledPlugins.filter((plugin) => {
|
||||
return isExportablePlugin(state, device, client, plugin);
|
||||
}),
|
||||
].map((p) => ({
|
||||
id: p.id,
|
||||
label: getPluginTitle(p),
|
||||
}));
|
||||
}
|
||||
|
||||
function isExportablePlugin(
|
||||
{pluginMessageQueue}: Pick<State, 'pluginMessageQueue'>,
|
||||
device: BaseDevice | null,
|
||||
client: Client | null,
|
||||
plugin: PluginDefinition,
|
||||
): boolean {
|
||||
const pluginKey = isDevicePluginDefinition(plugin)
|
||||
? getPluginKey(undefined, device, plugin.id)
|
||||
: getPluginKey(client?.id, undefined, plugin.id);
|
||||
// plugin has exportable sandy state
|
||||
if (client?.sandyPluginStates.get(plugin.id)?.isPersistable()) {
|
||||
return true;
|
||||
}
|
||||
if (device?.sandyPluginStates.get(plugin.id)?.isPersistable()) {
|
||||
return true;
|
||||
}
|
||||
// plugin has pending messages and a persisted state reducer or isSandy
|
||||
if (pluginMessageQueue[pluginKey]) {
|
||||
return true;
|
||||
}
|
||||
// nothing to serialize
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPluginTitle(pluginClass: {
|
||||
title?: string | null;
|
||||
id: string;
|
||||
}) {
|
||||
return pluginClass.title || pluginClass.id;
|
||||
}
|
||||
|
||||
export function sortPluginsByName(
|
||||
a: PluginDefinition,
|
||||
b: PluginDefinition,
|
||||
): number {
|
||||
// make sure Device plugins are sorted before normal plugins
|
||||
if (isDevicePluginDefinition(a) && !isDevicePluginDefinition(b)) {
|
||||
return -1;
|
||||
}
|
||||
if (isDevicePluginDefinition(b) && !isDevicePluginDefinition(a)) {
|
||||
return 1;
|
||||
}
|
||||
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
||||
}
|
||||
|
||||
export function isDevicePlugin(activePlugin: ActivePluginListItem) {
|
||||
if (activePlugin.details.pluginType === 'device') {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
(activePlugin.status === 'enabled' || activePlugin.status === 'disabled') &&
|
||||
isDevicePluginDefinition(activePlugin.definition)
|
||||
);
|
||||
}
|
||||
|
||||
export function isDevicePluginDefinition(
|
||||
definition: PluginDefinition,
|
||||
): boolean {
|
||||
return definition.isDevicePlugin;
|
||||
}
|
||||
|
||||
export function getPluginTooltip(details: PluginDetails): string {
|
||||
return `${getPluginTitle(details)} (${details.id}@${details.version}) ${
|
||||
details.description ?? ''
|
||||
}`;
|
||||
}
|
||||
|
||||
export function computePluginLists(
|
||||
connections: Pick<
|
||||
State['connections'],
|
||||
'enabledDevicePlugins' | 'enabledPlugins'
|
||||
>,
|
||||
plugins: Pick<
|
||||
State['plugins'],
|
||||
| 'bundledPlugins'
|
||||
| 'marketplacePlugins'
|
||||
| 'loadedPlugins'
|
||||
| 'devicePlugins'
|
||||
| 'disabledPlugins'
|
||||
| 'gatekeepedPlugins'
|
||||
| 'failedPlugins'
|
||||
| 'clientPlugins'
|
||||
>,
|
||||
device: BaseDevice | null,
|
||||
metroDevice: BaseDevice | null,
|
||||
client: Client | null,
|
||||
): {
|
||||
devicePlugins: PluginDefinition[];
|
||||
metroPlugins: PluginDefinition[];
|
||||
enabledPlugins: PluginDefinition[];
|
||||
disabledPlugins: PluginDefinition[];
|
||||
unavailablePlugins: [plugin: PluginDetails, reason: string][];
|
||||
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
|
||||
} {
|
||||
const enabledDevicePluginsState = connections.enabledDevicePlugins;
|
||||
const enabledPluginsState = connections.enabledPlugins;
|
||||
const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin([
|
||||
...plugins.bundledPlugins.values(),
|
||||
...plugins.marketplacePlugins,
|
||||
]).filter((p) => !plugins.loadedPlugins.has(p.id));
|
||||
const devicePlugins: PluginDefinition[] = [...plugins.devicePlugins.values()]
|
||||
.filter((p) => device?.supportsPlugin(p))
|
||||
.filter((p) => enabledDevicePluginsState.has(p.id));
|
||||
const metroPlugins: PluginDefinition[] = [...plugins.devicePlugins.values()]
|
||||
.filter((p) => metroDevice?.supportsPlugin(p))
|
||||
.filter((p) => enabledDevicePluginsState.has(p.id));
|
||||
const enabledPlugins: PluginDefinition[] = [];
|
||||
const disabledPlugins: PluginDefinition[] = [
|
||||
...plugins.devicePlugins.values(),
|
||||
]
|
||||
.filter(
|
||||
(p) =>
|
||||
device?.supportsPlugin(p.details) ||
|
||||
metroDevice?.supportsPlugin(p.details),
|
||||
)
|
||||
.filter((p) => !enabledDevicePluginsState.has(p.id));
|
||||
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
|
||||
const downloadablePlugins: (
|
||||
| DownloadablePluginDetails
|
||||
| BundledPluginDetails
|
||||
)[] = [];
|
||||
|
||||
if (device) {
|
||||
// find all device plugins that aren't part of the current device / metro
|
||||
for (const p of plugins.devicePlugins.values()) {
|
||||
if (!device.supportsPlugin(p) && !metroDevice?.supportsPlugin(p)) {
|
||||
unavailablePlugins.push([
|
||||
p.details,
|
||||
`Device plugin '${getPluginTitle(
|
||||
p.details,
|
||||
)}' is not supported by the selected device '${device.title}' (${
|
||||
device.os
|
||||
})`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
for (const plugin of uninstalledMarketplacePlugins.filter(
|
||||
(d) => d.pluginType === 'device',
|
||||
)) {
|
||||
if (
|
||||
device.supportsPlugin(plugin) ||
|
||||
metroDevice?.supportsPlugin(plugin)
|
||||
) {
|
||||
downloadablePlugins.push(plugin);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const p of plugins.devicePlugins.values()) {
|
||||
unavailablePlugins.push([
|
||||
p.details,
|
||||
`Device plugin '${getPluginTitle(
|
||||
p.details,
|
||||
)}' is not available because no device is currently selected`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// process problematic plugins
|
||||
plugins.disabledPlugins.forEach((plugin) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`Plugin '${plugin.title}' is disabled by configuration`,
|
||||
]);
|
||||
});
|
||||
plugins.gatekeepedPlugins.forEach((plugin) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`Plugin '${plugin.title}' is only available to members of gatekeeper '${plugin.gatekeeper}'`,
|
||||
]);
|
||||
});
|
||||
plugins.failedPlugins.forEach(([plugin, error]) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`Plugin '${plugin.title}' failed to load: '${error}'`,
|
||||
]);
|
||||
});
|
||||
|
||||
const clientPlugins = Array.from(plugins.clientPlugins.values()).sort(
|
||||
sortPluginsByName,
|
||||
);
|
||||
|
||||
// process all client plugins
|
||||
if (device && client) {
|
||||
const favoritePlugins = getFavoritePlugins(
|
||||
device,
|
||||
client,
|
||||
clientPlugins,
|
||||
client && enabledPluginsState[client.query.app],
|
||||
true,
|
||||
);
|
||||
clientPlugins.forEach((plugin) => {
|
||||
if (!client.supportsPlugin(plugin.id)) {
|
||||
unavailablePlugins.push([
|
||||
plugin.details,
|
||||
`Plugin '${getPluginTitle(
|
||||
plugin.details,
|
||||
)}' is not supported by the selected application '${
|
||||
client.query.app
|
||||
}' (${client.query.os})`,
|
||||
]);
|
||||
} else if (favoritePlugins.includes(plugin)) {
|
||||
enabledPlugins.push(plugin);
|
||||
} else {
|
||||
disabledPlugins.push(plugin);
|
||||
}
|
||||
});
|
||||
uninstalledMarketplacePlugins.forEach((plugin) => {
|
||||
if (plugin.pluginType !== 'device' && client.supportsPlugin(plugin.id)) {
|
||||
downloadablePlugins.push(plugin);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
clientPlugins.forEach((plugin) => {
|
||||
unavailablePlugins.push([
|
||||
plugin.details,
|
||||
`Plugin '${getPluginTitle(
|
||||
plugin.details,
|
||||
)}' is not available because no application is currently selected`,
|
||||
]);
|
||||
});
|
||||
}
|
||||
const downloadablePluginSet = new Set<string>(
|
||||
downloadablePlugins.map((p) => p.id),
|
||||
);
|
||||
uninstalledMarketplacePlugins
|
||||
.filter((p) => !downloadablePluginSet.has(p.id))
|
||||
.forEach((plugin) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`Plugin '${getPluginTitle(plugin)}' is not supported by the selected ${
|
||||
plugin.pluginType === 'device' ? 'device' : 'application'
|
||||
} '${
|
||||
(plugin.pluginType === 'device'
|
||||
? device?.title
|
||||
: client?.query.app) ?? 'unknown'
|
||||
}' (${
|
||||
plugin.pluginType === 'device' ? device?.os : client?.query.os
|
||||
}) and not installed in Flipper`,
|
||||
]);
|
||||
});
|
||||
|
||||
enabledPlugins.sort(sortPluginsByName);
|
||||
devicePlugins.sort(sortPluginsByName);
|
||||
disabledPlugins.sort(sortPluginsByName);
|
||||
metroPlugins.sort(sortPluginsByName);
|
||||
unavailablePlugins.sort(([a], [b]) => {
|
||||
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
||||
});
|
||||
downloadablePlugins.sort((a, b) => {
|
||||
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
||||
});
|
||||
|
||||
return {
|
||||
devicePlugins,
|
||||
metroPlugins,
|
||||
enabledPlugins,
|
||||
disabledPlugins,
|
||||
unavailablePlugins,
|
||||
downloadablePlugins,
|
||||
};
|
||||
}
|
||||
|
||||
function getFavoritePlugins(
|
||||
device: BaseDevice,
|
||||
client: Client,
|
||||
allPlugins: PluginDefinition[],
|
||||
enabledPlugins: undefined | string[],
|
||||
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
|
||||
): PluginDefinition[] {
|
||||
if (device.isArchived) {
|
||||
if (!returnFavoredPlugins) {
|
||||
return [];
|
||||
}
|
||||
// for *imported* devices, all stored plugins are enabled
|
||||
return allPlugins.filter((plugin) => client.plugins.has(plugin.id));
|
||||
}
|
||||
if (!enabledPlugins || !enabledPlugins.length) {
|
||||
return returnFavoredPlugins ? [] : allPlugins;
|
||||
}
|
||||
return allPlugins.filter((plugin) => {
|
||||
const idx = enabledPlugins.indexOf(plugin.id);
|
||||
return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins;
|
||||
});
|
||||
}
|
||||
|
||||
export function computeActivePluginList({
|
||||
enabledPlugins,
|
||||
devicePlugins,
|
||||
metroPlugins,
|
||||
disabledPlugins,
|
||||
downloadablePlugins,
|
||||
unavailablePlugins,
|
||||
}: PluginLists) {
|
||||
const pluginList: ActivePluginList = {};
|
||||
for (const plugin of enabledPlugins) {
|
||||
pluginList[plugin.id] = {
|
||||
status: 'enabled',
|
||||
details: plugin.details,
|
||||
definition: plugin,
|
||||
};
|
||||
}
|
||||
for (const plugin of devicePlugins) {
|
||||
pluginList[plugin.id] = {
|
||||
status: 'enabled',
|
||||
details: plugin.details,
|
||||
definition: plugin,
|
||||
};
|
||||
}
|
||||
for (const plugin of metroPlugins) {
|
||||
pluginList[plugin.id] = {
|
||||
status: 'enabled',
|
||||
details: plugin.details,
|
||||
definition: plugin,
|
||||
};
|
||||
}
|
||||
for (const plugin of disabledPlugins) {
|
||||
pluginList[plugin.id] = {
|
||||
status: 'disabled',
|
||||
details: plugin.details,
|
||||
definition: plugin,
|
||||
};
|
||||
}
|
||||
for (const plugin of downloadablePlugins) {
|
||||
pluginList[plugin.id] = {
|
||||
status: 'uninstalled',
|
||||
details: plugin,
|
||||
};
|
||||
}
|
||||
for (const [plugin, reason] of unavailablePlugins) {
|
||||
pluginList[plugin.id] = {
|
||||
status: 'unavailable',
|
||||
details: plugin,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
return pluginList;
|
||||
}
|
||||
|
||||
export type PluginStatus =
|
||||
| 'ready'
|
||||
| 'unknown'
|
||||
| 'failed'
|
||||
| 'gatekeeped'
|
||||
| 'bundle_installable'
|
||||
| 'marketplace_installable';
|
||||
|
||||
export function getPluginStatus(
|
||||
store: Store,
|
||||
id: string,
|
||||
): [state: PluginStatus, reason?: string] {
|
||||
const state: PluginsState = store.getState().plugins;
|
||||
if (state.devicePlugins.has(id) || state.clientPlugins.has(id)) {
|
||||
return ['ready'];
|
||||
}
|
||||
const gateKeepedDetails = state.gatekeepedPlugins.find((d) => d.id === id);
|
||||
if (gateKeepedDetails) {
|
||||
return ['gatekeeped', gateKeepedDetails.gatekeeper];
|
||||
}
|
||||
const failedPluginEntry = state.failedPlugins.find(
|
||||
([details]) => details.id === id,
|
||||
);
|
||||
if (failedPluginEntry) {
|
||||
return ['failed', failedPluginEntry[1]];
|
||||
}
|
||||
if (state.bundledPlugins.has(id)) {
|
||||
return ['bundle_installable'];
|
||||
}
|
||||
if (state.marketplacePlugins.find((d) => d.id === id)) {
|
||||
return ['marketplace_installable'];
|
||||
}
|
||||
return ['unknown'];
|
||||
}
|
||||
Reference in New Issue
Block a user