Summary: All functionality was about selecting plugins Reviewed By: lblasa Differential Revision: D51200129 fbshipit-source-id: edc33c2b989eabec2ca4a7e285f92c50950977ed
431 lines
12 KiB
TypeScript
431 lines
12 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {
|
|
BaseDevice,
|
|
getLatestCompatibleVersionOfEachPlugin,
|
|
} from 'flipper-frontend-core';
|
|
import type Client from '../Client';
|
|
import type {
|
|
ActivatablePluginDetails,
|
|
DownloadablePluginDetails,
|
|
PluginDetails,
|
|
} from 'flipper-common';
|
|
import {getPluginKey} from './pluginKey';
|
|
import {getAppVersion} from './info';
|
|
|
|
export type PluginLists = {
|
|
devicePlugins: PluginDefinition[];
|
|
enabledPlugins: PluginDefinition[];
|
|
disabledPlugins: PluginDefinition[];
|
|
unavailablePlugins: [plugin: PluginDetails, reason: string][];
|
|
downloadablePlugins: DownloadablePluginDetails[];
|
|
};
|
|
|
|
export type ActivePluginListItem =
|
|
| {
|
|
status: 'enabled';
|
|
details: ActivatablePluginDetails;
|
|
definition: PluginDefinition;
|
|
}
|
|
| {
|
|
status: 'disabled';
|
|
details: ActivatablePluginDetails;
|
|
definition: PluginDefinition;
|
|
}
|
|
| {
|
|
status: 'uninstalled';
|
|
details: DownloadablePluginDetails;
|
|
}
|
|
| {
|
|
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).toLowerCase() > getPluginTitle(b).toLocaleLowerCase()
|
|
? 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'],
|
|
| 'marketplacePlugins'
|
|
| 'loadedPlugins'
|
|
| 'devicePlugins'
|
|
| 'disabledPlugins'
|
|
| 'gatekeepedPlugins'
|
|
| 'failedPlugins'
|
|
| 'clientPlugins'
|
|
>,
|
|
device: BaseDevice | null,
|
|
client: Client | null,
|
|
): {
|
|
devicePlugins: PluginDefinition[];
|
|
enabledPlugins: PluginDefinition[];
|
|
disabledPlugins: PluginDefinition[];
|
|
unavailablePlugins: [plugin: PluginDetails, reason: string][];
|
|
downloadablePlugins: DownloadablePluginDetails[];
|
|
} {
|
|
const enabledDevicePluginsState = connections.enabledDevicePlugins;
|
|
const enabledPluginsState = connections.enabledPlugins;
|
|
const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin(
|
|
[...plugins.marketplacePlugins],
|
|
getAppVersion(),
|
|
).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 enabledPlugins: PluginDefinition[] = [];
|
|
const disabledPlugins: PluginDefinition[] = [
|
|
...plugins.devicePlugins.values(),
|
|
]
|
|
.filter((p) => device?.supportsPlugin(p.details))
|
|
.filter((p) => !enabledDevicePluginsState.has(p.id));
|
|
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
|
|
const downloadablePlugins: DownloadablePluginDetails[] = [];
|
|
|
|
if (device) {
|
|
// find all device plugins that aren't part of the current device
|
|
for (const p of plugins.devicePlugins.values()) {
|
|
if (!device.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)) {
|
|
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);
|
|
unavailablePlugins.sort(([a], [b]) => {
|
|
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
|
});
|
|
downloadablePlugins.sort((a, b) => {
|
|
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
|
});
|
|
|
|
return {
|
|
devicePlugins,
|
|
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,
|
|
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 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'
|
|
| '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.marketplacePlugins.find((d) => d.id === id)) {
|
|
return ['marketplace_installable'];
|
|
}
|
|
return ['unknown'];
|
|
}
|