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,646 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import {Dialog, getFlipperLib} from 'flipper-plugin';
|
||||
import {isTest} from 'flipper-common';
|
||||
import {getUser} from '../fb-stubs/user';
|
||||
import {State, Store} from '../reducers/index';
|
||||
import {checkForUpdate} from '../fb-stubs/checkForUpdate';
|
||||
import {getAppVersion} from '../utils/info';
|
||||
import {UserNotSignedInError} from 'flipper-common';
|
||||
import {
|
||||
canBeDefaultDevice,
|
||||
selectPlugin,
|
||||
setPluginEnabled,
|
||||
} from '../reducers/connections';
|
||||
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
|
||||
import {Typography} from 'antd';
|
||||
import {getPluginStatus, PluginStatus} from '../utils/pluginUtils';
|
||||
import {loadPluginsFromMarketplace} from './fb-stubs/pluginMarketplace';
|
||||
import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
|
||||
import {startPluginDownload} from '../reducers/pluginDownloads';
|
||||
import isProduction from '../utils/isProduction';
|
||||
import BaseDevice from '../devices/BaseDevice';
|
||||
import Client from '../Client';
|
||||
import {RocketOutlined} from '@ant-design/icons';
|
||||
import {showEmulatorLauncher} from '../sandy-chrome/appinspect/LaunchEmulator';
|
||||
import {getAllClients} from '../reducers/connections';
|
||||
import {showLoginDialog} from '../chrome/fb-stubs/SignInSheet';
|
||||
import {
|
||||
DeeplinkInteraction,
|
||||
DeeplinkInteractionState,
|
||||
OpenPluginParams,
|
||||
} from '../deeplinkTracking';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
export function parseOpenPluginParams(query: string): OpenPluginParams {
|
||||
// 'flipper://open-plugin?plugin-id=graphql&client=facebook&devices=android,ios&chrome=1&payload='
|
||||
const url = new URL(query);
|
||||
const params = new Map<string, string>(url.searchParams as any);
|
||||
if (!params.has('plugin-id')) {
|
||||
throw new Error('Missing plugin-id param');
|
||||
}
|
||||
return {
|
||||
pluginId: params.get('plugin-id')!,
|
||||
client: params.get('client'),
|
||||
devices: params.get('devices')?.split(',') ?? [],
|
||||
payload: params.get('payload')
|
||||
? decodeURIComponent(params.get('payload')!)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleOpenPluginDeeplink(
|
||||
store: Store,
|
||||
query: string,
|
||||
trackInteraction: (interaction: DeeplinkInteraction) => void,
|
||||
) {
|
||||
const params = parseOpenPluginParams(query);
|
||||
const title = `Opening plugin ${params.pluginId}…`;
|
||||
console.debug(`[deeplink] ${title} for with params`, params);
|
||||
|
||||
if (!(await verifyLighthouseAndUserLoggedIn(store, title))) {
|
||||
trackInteraction({
|
||||
state: 'PLUGIN_LIGHTHOUSE_BAIL',
|
||||
plugin: params,
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.debug('[deeplink] Cleared Lighthouse and log-in check.');
|
||||
await verifyFlipperIsUpToDate(title);
|
||||
console.debug('[deeplink] Cleared up-to-date check.');
|
||||
const [pluginStatusResult, pluginStatus] = await verifyPluginStatus(
|
||||
store,
|
||||
params.pluginId,
|
||||
title,
|
||||
);
|
||||
if (!pluginStatusResult) {
|
||||
trackInteraction({
|
||||
state: 'PLUGIN_STATUS_BAIL',
|
||||
plugin: params,
|
||||
extra: {pluginStatus},
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.debug('[deeplink] Cleared plugin status check:', pluginStatusResult);
|
||||
|
||||
const isDevicePlugin = store
|
||||
.getState()
|
||||
.plugins.devicePlugins.has(params.pluginId);
|
||||
const pluginDefinition = isDevicePlugin
|
||||
? store.getState().plugins.devicePlugins.get(params.pluginId)!
|
||||
: store.getState().plugins.clientPlugins.get(params.pluginId)!;
|
||||
const deviceOrClient = await selectDevicesAndClient(
|
||||
store,
|
||||
params,
|
||||
title,
|
||||
isDevicePlugin,
|
||||
);
|
||||
console.debug('[deeplink] Selected device and client:', deviceOrClient);
|
||||
if ('errorState' in deviceOrClient) {
|
||||
trackInteraction({
|
||||
state: deviceOrClient.errorState,
|
||||
plugin: params,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const client: Client | undefined = isDevicePlugin
|
||||
? undefined
|
||||
: (deviceOrClient as Client);
|
||||
const device: BaseDevice = isDevicePlugin
|
||||
? (deviceOrClient as BaseDevice)
|
||||
: (deviceOrClient as Client).device;
|
||||
console.debug('[deeplink] Client: ', client);
|
||||
console.debug('[deeplink] Device: ', device);
|
||||
|
||||
// verify plugin supported by selected device / client
|
||||
if (isDevicePlugin && !device.supportsPlugin(pluginDefinition)) {
|
||||
await Dialog.alert({
|
||||
title,
|
||||
type: 'error',
|
||||
message: `This plugin is not supported by device ${device.displayTitle()}`,
|
||||
});
|
||||
trackInteraction({
|
||||
state: 'PLUGIN_DEVICE_UNSUPPORTED',
|
||||
plugin: params,
|
||||
extra: {device: device.displayTitle()},
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.debug('[deeplink] Cleared device plugin support check.');
|
||||
if (!isDevicePlugin && !client!.plugins.has(params.pluginId)) {
|
||||
await Dialog.alert({
|
||||
title,
|
||||
type: 'error',
|
||||
message: `This plugin is not supported by client ${client!.query.app}`,
|
||||
});
|
||||
trackInteraction({
|
||||
state: 'PLUGIN_CLIENT_UNSUPPORTED',
|
||||
plugin: params,
|
||||
extra: {client: client!.query.app},
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.debug('[deeplink] Cleared client plugin support check.');
|
||||
|
||||
// verify plugin enabled
|
||||
if (isDevicePlugin) {
|
||||
// for the device plugins enabling is a bit more complication and should go through the pluginManager
|
||||
if (
|
||||
!store.getState().connections.enabledDevicePlugins.has(params.pluginId)
|
||||
) {
|
||||
store.dispatch(switchPlugin({plugin: pluginDefinition}));
|
||||
}
|
||||
} else {
|
||||
store.dispatch(setPluginEnabled(params.pluginId, client!.query.app));
|
||||
}
|
||||
console.debug('[deeplink] Cleared plugin enabling.');
|
||||
|
||||
// open the plugin
|
||||
if (isDevicePlugin) {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: params.pluginId,
|
||||
selectedAppId: null,
|
||||
selectedDevice: device,
|
||||
deepLinkPayload: params.payload,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: params.pluginId,
|
||||
selectedAppId: client!.id,
|
||||
selectedDevice: device,
|
||||
deepLinkPayload: params.payload,
|
||||
}),
|
||||
);
|
||||
}
|
||||
trackInteraction({
|
||||
state: 'PLUGIN_OPEN_SUCCESS',
|
||||
plugin: params,
|
||||
});
|
||||
}
|
||||
|
||||
// check if user is connected to VPN and logged in. Returns true if OK, or false if aborted
|
||||
async function verifyLighthouseAndUserLoggedIn(
|
||||
store: Store,
|
||||
title: string,
|
||||
): Promise<boolean> {
|
||||
if (!getFlipperLib().isFB || process.env.NODE_ENV === 'test') {
|
||||
return true; // ok, continue
|
||||
}
|
||||
|
||||
// repeat until connection succeeded
|
||||
while (true) {
|
||||
const spinnerDialog = Dialog.loading({
|
||||
title,
|
||||
message: 'Checking connection to Facebook Intern',
|
||||
});
|
||||
|
||||
try {
|
||||
const user = await getUser();
|
||||
spinnerDialog.close();
|
||||
// User is logged in
|
||||
if (user) {
|
||||
return true;
|
||||
} else {
|
||||
// Connected, but not logged in or no valid profile object returned
|
||||
return await showPleaseLoginDialog(store, title);
|
||||
}
|
||||
} catch (e) {
|
||||
spinnerDialog.close();
|
||||
if (e instanceof UserNotSignedInError) {
|
||||
// connection, but user is not logged in
|
||||
return await showPleaseLoginDialog(store, title);
|
||||
}
|
||||
// General connection error.
|
||||
// Not connected (to presumably) intern at all
|
||||
if (
|
||||
!(await Dialog.confirm({
|
||||
title,
|
||||
message:
|
||||
'It looks you are currently not connected to Lighthouse / VPN. Please connect and retry.',
|
||||
okText: 'Retry',
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function showPleaseLoginDialog(
|
||||
store: Store,
|
||||
title: string,
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!(await Dialog.confirm({
|
||||
title,
|
||||
message: 'You are currently not logged in, please login.',
|
||||
okText: 'Login',
|
||||
}))
|
||||
) {
|
||||
// cancelled login
|
||||
return false;
|
||||
}
|
||||
|
||||
await showLoginDialog();
|
||||
// wait until login succeeded
|
||||
await waitForLogin(store);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function waitForLogin(store: Store) {
|
||||
return waitFor(store, (state) => !!state.user?.id);
|
||||
}
|
||||
|
||||
// make this more reusable?
|
||||
function waitFor(
|
||||
store: Store,
|
||||
predicate: (state: State) => boolean,
|
||||
): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
const unsub = store.subscribe(() => {
|
||||
if (predicate(store.getState())) {
|
||||
unsub();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyFlipperIsUpToDate(title: string) {
|
||||
if (!isProduction() || isTest()) {
|
||||
return;
|
||||
}
|
||||
const currentVersion = getAppVersion();
|
||||
const handle = Dialog.loading({
|
||||
title,
|
||||
message: 'Checking if Flipper is up-to-date',
|
||||
});
|
||||
try {
|
||||
const result = await checkForUpdate(currentVersion);
|
||||
handle.close();
|
||||
switch (result.kind) {
|
||||
case 'error':
|
||||
// if we can't tell if we're up to date, we don't want to halt the process on that.
|
||||
console.warn('Failed to verify Flipper version', result);
|
||||
return;
|
||||
case 'up-to-date':
|
||||
return;
|
||||
case 'update-available':
|
||||
await Dialog.confirm({
|
||||
title,
|
||||
message: (
|
||||
<Typography.Text>
|
||||
{getUpdateAvailableMessage(result)}
|
||||
</Typography.Text>
|
||||
),
|
||||
okText: 'Skip',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// if we can't tell if we're up to date, we don't want to halt the process on that.
|
||||
console.warn('Failed to verify Flipper version', e);
|
||||
handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyPluginStatus(
|
||||
store: Store,
|
||||
pluginId: string,
|
||||
title: string,
|
||||
): Promise<[boolean, PluginStatus]> {
|
||||
// make sure we have marketplace plugin data present
|
||||
if (!isTest() && !store.getState().plugins.marketplacePlugins.length) {
|
||||
// plugins not yet fetched
|
||||
// updates plugins from marketplace (if logged in), and stores them
|
||||
await loadPluginsFromMarketplace();
|
||||
}
|
||||
// while true loop; after pressing install or add GK, we want to check again if plugin is available
|
||||
while (true) {
|
||||
const [status, reason] = getPluginStatus(store, pluginId);
|
||||
switch (status) {
|
||||
case 'ready':
|
||||
return [true, status];
|
||||
case 'unknown':
|
||||
await Dialog.alert({
|
||||
type: 'warning',
|
||||
title,
|
||||
message: `No plugin with id '${pluginId}' is known to Flipper. Please correct the deeplink, or install the plugin from NPM using the plugin manager.`,
|
||||
});
|
||||
return [false, status];
|
||||
case 'failed':
|
||||
await Dialog.alert({
|
||||
type: 'error',
|
||||
title,
|
||||
message: `We found plugin '${pluginId}', but failed to load it: ${reason}. Please check the logs for more details`,
|
||||
});
|
||||
return [false, status];
|
||||
case 'gatekeeped':
|
||||
if (
|
||||
!(await Dialog.confirm({
|
||||
title,
|
||||
message: (
|
||||
<p>
|
||||
{`To use plugin '${pluginId}', it is necessary to be a member of the GK '${reason}'. Click `}
|
||||
<Typography.Link
|
||||
href={`https://www.internalfb.com/intern/gatekeeper/projects/${reason}`}>
|
||||
here
|
||||
</Typography.Link>{' '}
|
||||
to enroll, restart Flipper, and click the link again.
|
||||
</p>
|
||||
),
|
||||
okText: 'Restart',
|
||||
onConfirm: async () => {
|
||||
getRenderHostInstance().restartFlipper();
|
||||
// intentionally forever pending, we're restarting...
|
||||
return new Promise(() => {});
|
||||
},
|
||||
}))
|
||||
) {
|
||||
return [false, status];
|
||||
}
|
||||
break;
|
||||
case 'bundle_installable': {
|
||||
// For convenience, don't ask user to install bundled plugins, handle it directly
|
||||
await installBundledPlugin(store, pluginId, title);
|
||||
break;
|
||||
}
|
||||
case 'marketplace_installable': {
|
||||
if (!(await installMarketPlacePlugin(store, pluginId, title))) {
|
||||
return [false, status];
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unhandled state: ' + status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function installBundledPlugin(
|
||||
store: Store,
|
||||
pluginId: string,
|
||||
title: string,
|
||||
) {
|
||||
const plugin = store.getState().plugins.bundledPlugins.get(pluginId);
|
||||
if (!plugin || !plugin.isBundled) {
|
||||
throw new Error(`Failed to find bundled plugin '${pluginId}'`);
|
||||
}
|
||||
const loadingDialog = Dialog.loading({
|
||||
title,
|
||||
message: `Loading plugin '${pluginId}'...`,
|
||||
});
|
||||
store.dispatch(loadPlugin({plugin, enable: true, notifyIfFailed: true}));
|
||||
try {
|
||||
await waitFor(
|
||||
store,
|
||||
() => getPluginStatus(store, pluginId)[0] !== 'bundle_installable',
|
||||
);
|
||||
} finally {
|
||||
loadingDialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function installMarketPlacePlugin(
|
||||
store: Store,
|
||||
pluginId: string,
|
||||
title: string,
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!(await Dialog.confirm({
|
||||
title,
|
||||
message: `The requested plugin '${pluginId}' is currently not installed, but can be downloaded from the Flipper plugin Marketplace. If you trust the source of the current link, press 'Install' to continue`,
|
||||
okText: 'Install',
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const plugin = store
|
||||
.getState()
|
||||
.plugins.marketplacePlugins.find((p) => p.id === pluginId);
|
||||
if (!plugin) {
|
||||
throw new Error(`Failed to find marketplace plugin '${pluginId}'`);
|
||||
}
|
||||
const loadingDialog = Dialog.loading({
|
||||
title,
|
||||
message: `Installing plugin '${pluginId}'...`,
|
||||
});
|
||||
try {
|
||||
store.dispatch(startPluginDownload({plugin, startedByUser: true}));
|
||||
await waitFor(
|
||||
store,
|
||||
() => getPluginStatus(store, pluginId)[0] !== 'marketplace_installable',
|
||||
);
|
||||
} finally {
|
||||
loadingDialog.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
type DeeplinkError = {
|
||||
errorState: DeeplinkInteractionState;
|
||||
};
|
||||
|
||||
async function selectDevicesAndClient(
|
||||
store: Store,
|
||||
params: OpenPluginParams,
|
||||
title: string,
|
||||
isDevicePlugin: boolean,
|
||||
): Promise<DeeplinkError | BaseDevice | Client> {
|
||||
function findValidDevices() {
|
||||
// find connected devices with the right OS.
|
||||
return (
|
||||
store
|
||||
.getState()
|
||||
.connections.devices.filter((d) => d.connected.get())
|
||||
.filter(
|
||||
(d) => params.devices.length === 0 || params.devices.includes(d.os),
|
||||
)
|
||||
// This filters out OS-level devices which are causing more confusion than good
|
||||
// when used with deeplinks.
|
||||
.filter(canBeDefaultDevice)
|
||||
);
|
||||
}
|
||||
|
||||
// loop until we have devices (or abort)
|
||||
while (!findValidDevices().length) {
|
||||
if (!(await launchDeviceDialog(store, params, title))) {
|
||||
return {errorState: 'PLUGIN_DEVICE_BAIL'};
|
||||
}
|
||||
}
|
||||
|
||||
// at this point we have 1 or more valid devices
|
||||
const availableDevices = findValidDevices();
|
||||
console.debug(
|
||||
'[deeplink] selectDevicesAndClient found at least one more valid device:',
|
||||
availableDevices,
|
||||
);
|
||||
// device plugin
|
||||
if (isDevicePlugin) {
|
||||
if (availableDevices.length === 1) {
|
||||
return availableDevices[0];
|
||||
}
|
||||
const selectedDevice = await selectDeviceDialog(availableDevices, title);
|
||||
if (!selectedDevice) {
|
||||
return {errorState: 'PLUGIN_DEVICE_SELECTION_BAIL'};
|
||||
}
|
||||
return selectedDevice;
|
||||
}
|
||||
|
||||
console.debug('[deeplink] Not a device plugin. Waiting for valid client.');
|
||||
// wait for valid client
|
||||
while (true) {
|
||||
const origClients = store.getState().connections.clients;
|
||||
const validClients = getAllClients(store.getState().connections)
|
||||
.filter(
|
||||
// correct app name, or, if not set, an app that at least supports this plugin
|
||||
(c) =>
|
||||
params.client
|
||||
? c.query.app === params.client
|
||||
: c.plugins.has(params.pluginId),
|
||||
)
|
||||
.filter((c) => c.connected.get())
|
||||
.filter((c) => availableDevices.includes(c.device));
|
||||
|
||||
if (validClients.length === 1) {
|
||||
return validClients[0];
|
||||
}
|
||||
if (validClients.length > 1) {
|
||||
const selectedClient = await selectClientDialog(validClients, title);
|
||||
if (!selectedClient) {
|
||||
return {errorState: 'PLUGIN_CLIENT_SELECTION_BAIL'};
|
||||
}
|
||||
return selectedClient;
|
||||
}
|
||||
|
||||
// no valid client yet
|
||||
const result = await new Promise<boolean>((resolve) => {
|
||||
const dialog = Dialog.alert({
|
||||
title,
|
||||
type: 'warning',
|
||||
message: params.client
|
||||
? `Application '${params.client}' doesn't seem to be connected yet. Please start a debug version of the app to continue.`
|
||||
: `No application that supports plugin '${params.pluginId}' seems to be running. Please start a debug application that supports the plugin to continue.`,
|
||||
okText: 'Cancel',
|
||||
});
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
dialog.then(() => resolve(false));
|
||||
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
waitFor(store, (state) => state.connections.clients !== origClients).then(
|
||||
() => {
|
||||
dialog.close();
|
||||
resolve(true);
|
||||
},
|
||||
);
|
||||
|
||||
// We also want to react to changes in the available plugins and refresh.
|
||||
origClients.forEach((c) =>
|
||||
c.on('plugins-change', () => {
|
||||
dialog.close();
|
||||
resolve(true);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {errorState: 'PLUGIN_CLIENT_BAIL'}; // User cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a warning that no device was found, with button to launch emulator.
|
||||
* Resolves false if cancelled, or true if new devices were detected.
|
||||
*/
|
||||
async function launchDeviceDialog(
|
||||
store: Store,
|
||||
params: OpenPluginParams,
|
||||
title: string,
|
||||
) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const currentDevices = store.getState().connections.devices;
|
||||
const waitForNewDevice = async () =>
|
||||
await waitFor(
|
||||
store,
|
||||
(state) => state.connections.devices !== currentDevices,
|
||||
);
|
||||
const dialog = Dialog.confirm({
|
||||
title,
|
||||
message: (
|
||||
<p>
|
||||
To open the current deeplink for plugin {params.pluginId} a device{' '}
|
||||
{params.devices.length ? ' of type ' + params.devices.join(', ') : ''}{' '}
|
||||
should be up and running. No device was found. Please connect a device
|
||||
or launch an emulator / simulator.
|
||||
</p>
|
||||
),
|
||||
cancelText: 'Cancel',
|
||||
okText: 'Launch Device',
|
||||
onConfirm: async () => {
|
||||
showEmulatorLauncher(store);
|
||||
await waitForNewDevice();
|
||||
return true;
|
||||
},
|
||||
okButtonProps: {
|
||||
icon: <RocketOutlined />,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
dialog.then(() => {
|
||||
// dialog was cancelled
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// new devices were found
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
waitForNewDevice().then(() => {
|
||||
dialog.close();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function selectDeviceDialog(
|
||||
devices: BaseDevice[],
|
||||
title: string,
|
||||
): Promise<undefined | BaseDevice> {
|
||||
const selectedId = await Dialog.options({
|
||||
title,
|
||||
message: 'Select the device to open:',
|
||||
options: devices.map((d) => ({
|
||||
value: d.serial,
|
||||
label: d.displayTitle(),
|
||||
})),
|
||||
});
|
||||
// might find nothing if id === false
|
||||
return devices.find((d) => d.serial === selectedId);
|
||||
}
|
||||
|
||||
async function selectClientDialog(
|
||||
clients: Client[],
|
||||
title: string,
|
||||
): Promise<undefined | Client> {
|
||||
const selectedId = await Dialog.options({
|
||||
title,
|
||||
message:
|
||||
'Multiple applications running this plugin were found, please select one:',
|
||||
options: clients.map((c) => ({
|
||||
value: c.id,
|
||||
label: `${c.query.app} on ${c.device.displayTitle()}`,
|
||||
})),
|
||||
});
|
||||
// might find nothing if id === false
|
||||
return clients.find((c) => c.id === selectedId);
|
||||
}
|
||||
Reference in New Issue
Block a user