Plugin download progress and error handling
Summary: Show progress reporting in sidebar during plugin download. Also handle plugin download errors and show them to user as notifications. Reviewed By: mweststrate Differential Revision: D25530385 fbshipit-source-id: 42bf0e65b4434d00c1465705ce9ec5c723df6841
This commit is contained in:
committed by
Facebook GitHub Bot
parent
c3d61cc32d
commit
efb82e80b5
@@ -15,8 +15,7 @@ import {Store} from '../reducers/index';
|
|||||||
import {
|
import {
|
||||||
PluginDownloadStatus,
|
PluginDownloadStatus,
|
||||||
pluginDownloadStarted,
|
pluginDownloadStarted,
|
||||||
pluginDownloadFailed,
|
pluginDownloadFinished,
|
||||||
pluginDownloadSucceeded,
|
|
||||||
} from '../reducers/pluginDownloads';
|
} from '../reducers/pluginDownloads';
|
||||||
import {sideEffect} from '../utils/sideEffect';
|
import {sideEffect} from '../utils/sideEffect';
|
||||||
import {default as axios} from 'axios';
|
import {default as axios} from 'axios';
|
||||||
@@ -25,7 +24,12 @@ import path from 'path';
|
|||||||
import tmp from 'tmp';
|
import tmp from 'tmp';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {requirePlugin} from './plugins';
|
import {requirePlugin} from './plugins';
|
||||||
import {registerPluginUpdate} from '../reducers/connections';
|
import {registerPluginUpdate, setStaticView} from '../reducers/connections';
|
||||||
|
import {notification, Typography} from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import {ConsoleLogs} from '../chrome/ConsoleLogs';
|
||||||
|
|
||||||
|
const {Text, Link} = Typography;
|
||||||
|
|
||||||
// Adapter which forces node.js implementation for axios instead of browser implementation
|
// Adapter which forces node.js implementation for axios instead of browser implementation
|
||||||
// used by default in Electron. Node.js implementation is better, because it
|
// used by default in Electron. Node.js implementation is better, because it
|
||||||
@@ -44,11 +48,7 @@ export default (store: Store) => {
|
|||||||
(state, store) => {
|
(state, store) => {
|
||||||
for (const download of Object.values(state)) {
|
for (const download of Object.values(state)) {
|
||||||
if (download.status === PluginDownloadStatus.QUEUED) {
|
if (download.status === PluginDownloadStatus.QUEUED) {
|
||||||
handlePluginDownload(
|
handlePluginDownload(download.plugin, download.startedByUser, store);
|
||||||
download.plugin,
|
|
||||||
download.enableDownloadedPlugin,
|
|
||||||
store,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -58,7 +58,7 @@ export default (store: Store) => {
|
|||||||
|
|
||||||
async function handlePluginDownload(
|
async function handlePluginDownload(
|
||||||
plugin: DownloadablePluginDetails,
|
plugin: DownloadablePluginDetails,
|
||||||
enableDownloadedPlugin: boolean,
|
startedByUser: boolean,
|
||||||
store: Store,
|
store: Store,
|
||||||
) {
|
) {
|
||||||
const dispatch = store.dispatch;
|
const dispatch = store.dispatch;
|
||||||
@@ -115,21 +115,35 @@ async function handlePluginDownload(
|
|||||||
dispatch(
|
dispatch(
|
||||||
registerPluginUpdate({
|
registerPluginUpdate({
|
||||||
plugin: pluginDefinition,
|
plugin: pluginDefinition,
|
||||||
enablePlugin: enableDownloadedPlugin,
|
enablePlugin: startedByUser,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
|
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
|
||||||
);
|
);
|
||||||
dispatch(pluginDownloadSucceeded({plugin}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
|
`Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
dispatch(pluginDownloadFailed({plugin, error}));
|
if (startedByUser) {
|
||||||
|
notification.error({
|
||||||
|
message: `Failed to install plugin "${title}".`,
|
||||||
|
description: (
|
||||||
|
<Text>
|
||||||
|
See{' '}
|
||||||
|
<Link onClick={() => dispatch(setStaticView(ConsoleLogs))}>
|
||||||
|
logs
|
||||||
|
</Link>{' '}
|
||||||
|
for details.
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
placement: 'bottomLeft',
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
dispatch(pluginDownloadFinished({plugin}));
|
||||||
await fs.remove(targetDir);
|
await fs.remove(targetDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ export enum PluginDownloadStatus {
|
|||||||
|
|
||||||
export type DownloadablePluginState = {
|
export type DownloadablePluginState = {
|
||||||
plugin: DownloadablePluginDetails;
|
plugin: DownloadablePluginDetails;
|
||||||
enableDownloadedPlugin: boolean;
|
startedByUser: boolean;
|
||||||
} & (
|
} & (
|
||||||
| {status: PluginDownloadStatus.QUEUED}
|
| {status: PluginDownloadStatus.QUEUED}
|
||||||
| {status: PluginDownloadStatus.STARTED; cancel: Canceler}
|
| {status: PluginDownloadStatus.STARTED; cancel: Canceler}
|
||||||
| {status: PluginDownloadStatus.FAILED; error: Error}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// We use plugin installation path as key as it is unique for each plugin version.
|
// We use plugin installation path as key as it is unique for each plugin version.
|
||||||
@@ -34,7 +33,7 @@ export type PluginDownloadStart = {
|
|||||||
type: 'PLUGIN_DOWNLOAD_START';
|
type: 'PLUGIN_DOWNLOAD_START';
|
||||||
payload: {
|
payload: {
|
||||||
plugin: DownloadablePluginDetails;
|
plugin: DownloadablePluginDetails;
|
||||||
enableDownloadedPlugin: boolean;
|
startedByUser: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,26 +45,17 @@ export type PluginDownloadStarted = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginDownloadSucceeded = {
|
export type PluginDownloadFinished = {
|
||||||
type: 'PLUGIN_DOWNLOAD_SUCCEEDED';
|
type: 'PLUGIN_DOWNLOAD_FINISHED';
|
||||||
payload: {
|
payload: {
|
||||||
plugin: DownloadablePluginDetails;
|
plugin: DownloadablePluginDetails;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginDownloadFailed = {
|
|
||||||
type: 'PLUGIN_DOWNLOAD_FAILED';
|
|
||||||
payload: {
|
|
||||||
plugin: DownloadablePluginDetails;
|
|
||||||
error: Error;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| PluginDownloadStart
|
| PluginDownloadStart
|
||||||
| PluginDownloadStarted
|
| PluginDownloadStarted
|
||||||
| PluginDownloadSucceeded
|
| PluginDownloadFinished;
|
||||||
| PluginDownloadFailed;
|
|
||||||
|
|
||||||
const INITIAL_STATE: State = {};
|
const INITIAL_STATE: State = {};
|
||||||
|
|
||||||
@@ -75,24 +65,21 @@ export default function reducer(
|
|||||||
): State {
|
): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'PLUGIN_DOWNLOAD_START': {
|
case 'PLUGIN_DOWNLOAD_START': {
|
||||||
const {plugin, enableDownloadedPlugin} = action.payload;
|
const {plugin, startedByUser} = action.payload;
|
||||||
const downloadState = state[plugin.dir];
|
const downloadState = state[plugin.dir];
|
||||||
if (
|
if (downloadState) {
|
||||||
downloadState && // If download is already in progress - re-use the existing state.
|
// If download is already in progress - re-use the existing state.
|
||||||
downloadState.status !== PluginDownloadStatus.FAILED // Note that for failed downloads we want to retry downloads.
|
|
||||||
) {
|
|
||||||
return produce(state, (draft) => {
|
return produce(state, (draft) => {
|
||||||
draft[plugin.dir] = {
|
draft[plugin.dir] = {
|
||||||
...downloadState,
|
...downloadState,
|
||||||
enableDownloadedPlugin:
|
startedByUser: startedByUser || downloadState.startedByUser,
|
||||||
enableDownloadedPlugin || downloadState.enableDownloadedPlugin,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return produce(state, (draft) => {
|
return produce(state, (draft) => {
|
||||||
draft[plugin.dir] = {
|
draft[plugin.dir] = {
|
||||||
plugin,
|
plugin,
|
||||||
enableDownloadedPlugin: enableDownloadedPlugin,
|
startedByUser: startedByUser,
|
||||||
status: PluginDownloadStatus.QUEUED,
|
status: PluginDownloadStatus.QUEUED,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -110,29 +97,12 @@ export default function reducer(
|
|||||||
draft[plugin.dir] = {
|
draft[plugin.dir] = {
|
||||||
status: PluginDownloadStatus.STARTED,
|
status: PluginDownloadStatus.STARTED,
|
||||||
plugin,
|
plugin,
|
||||||
enableDownloadedPlugin: downloadState.enableDownloadedPlugin,
|
startedByUser: downloadState.startedByUser,
|
||||||
cancel,
|
cancel,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case 'PLUGIN_DOWNLOAD_FAILED': {
|
case 'PLUGIN_DOWNLOAD_FINISHED': {
|
||||||
const {plugin, error} = action.payload;
|
|
||||||
const downloadState = state[plugin.dir];
|
|
||||||
if (!downloadState) {
|
|
||||||
console.warn(
|
|
||||||
`Invalid state transition PLUGIN_DOWNLOAD_FAILED when there is no download in progress to directory ${plugin.dir}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return produce(state, (draft) => {
|
|
||||||
draft[plugin.dir] = {
|
|
||||||
status: PluginDownloadStatus.FAILED,
|
|
||||||
plugin: downloadState.plugin,
|
|
||||||
enableDownloadedPlugin: downloadState.enableDownloadedPlugin,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case 'PLUGIN_DOWNLOAD_SUCCEEDED': {
|
|
||||||
const {plugin} = action.payload;
|
const {plugin} = action.payload;
|
||||||
return produce(state, (draft) => {
|
return produce(state, (draft) => {
|
||||||
delete draft[plugin.dir];
|
delete draft[plugin.dir];
|
||||||
@@ -145,7 +115,7 @@ export default function reducer(
|
|||||||
|
|
||||||
export const startPluginDownload = (payload: {
|
export const startPluginDownload = (payload: {
|
||||||
plugin: DownloadablePluginDetails;
|
plugin: DownloadablePluginDetails;
|
||||||
enableDownloadedPlugin: boolean;
|
startedByUser: boolean;
|
||||||
}): Action => ({
|
}): Action => ({
|
||||||
type: 'PLUGIN_DOWNLOAD_START',
|
type: 'PLUGIN_DOWNLOAD_START',
|
||||||
payload,
|
payload,
|
||||||
@@ -156,11 +126,6 @@ export const pluginDownloadStarted = (payload: {
|
|||||||
cancel: Canceler;
|
cancel: Canceler;
|
||||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_STARTED', payload});
|
}): Action => ({type: 'PLUGIN_DOWNLOAD_STARTED', payload});
|
||||||
|
|
||||||
export const pluginDownloadSucceeded = (payload: {
|
export const pluginDownloadFinished = (payload: {
|
||||||
plugin: DownloadablePluginDetails;
|
plugin: DownloadablePluginDetails;
|
||||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_SUCCEEDED', payload});
|
}): Action => ({type: 'PLUGIN_DOWNLOAD_FINISHED', payload});
|
||||||
|
|
||||||
export const pluginDownloadFailed = (payload: {
|
|
||||||
plugin: DownloadablePluginDetails;
|
|
||||||
error: Error;
|
|
||||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_FAILED', payload});
|
|
||||||
|
|||||||
@@ -10,7 +10,13 @@
|
|||||||
import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
|
import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
|
import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
|
||||||
import {InfoIcon, SidebarTitle} from '../LeftSidebar';
|
import {InfoIcon, SidebarTitle} from '../LeftSidebar';
|
||||||
import {PlusOutlined, MinusOutlined, DeleteOutlined} from '@ant-design/icons';
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
MinusOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import {Glyph, Layout, styled} from '../../ui';
|
import {Glyph, Layout, styled} from '../../ui';
|
||||||
import {theme, NUX, Tracked} from 'flipper-plugin';
|
import {theme, NUX, Tracked} from 'flipper-plugin';
|
||||||
import {useDispatch, useStore} from '../../utils/useStore';
|
import {useDispatch, useStore} from '../../utils/useStore';
|
||||||
@@ -21,11 +27,14 @@ import Client from '../../Client';
|
|||||||
import {State} from '../../reducers';
|
import {State} from '../../reducers';
|
||||||
import BaseDevice from '../../devices/BaseDevice';
|
import BaseDevice from '../../devices/BaseDevice';
|
||||||
import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
|
import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
|
||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
import {PluginDetails, DownloadablePluginDetails} from 'flipper-plugin-lib';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
import {useMemoize} from '../../utils/useMemoize';
|
||||||
import MetroDevice from '../../devices/MetroDevice';
|
import MetroDevice from '../../devices/MetroDevice';
|
||||||
import {DownloadablePluginDetails} from 'plugin-lib/lib';
|
import {
|
||||||
import {startPluginDownload} from '../../reducers/pluginDownloads';
|
DownloadablePluginState,
|
||||||
|
PluginDownloadStatus,
|
||||||
|
startPluginDownload,
|
||||||
|
} from '../../reducers/pluginDownloads';
|
||||||
import {uninstallPlugin} from '../../reducers/pluginManager';
|
import {uninstallPlugin} from '../../reducers/pluginManager';
|
||||||
|
|
||||||
const {SubMenu} = Menu;
|
const {SubMenu} = Menu;
|
||||||
@@ -43,6 +52,7 @@ export const PluginList = memo(function PluginList({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const connections = useStore((state) => state.connections);
|
const connections = useStore((state) => state.connections);
|
||||||
const plugins = useStore((state) => state.plugins);
|
const plugins = useStore((state) => state.plugins);
|
||||||
|
const downloads = useStore((state) => state.pluginDownloads);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
devicePlugins,
|
devicePlugins,
|
||||||
@@ -50,7 +60,7 @@ export const PluginList = memo(function PluginList({
|
|||||||
enabledPlugins,
|
enabledPlugins,
|
||||||
disabledPlugins,
|
disabledPlugins,
|
||||||
unavailablePlugins,
|
unavailablePlugins,
|
||||||
uninstalledPlugins,
|
downloadablePlugins,
|
||||||
} = useMemoize(computePluginLists, [
|
} = useMemoize(computePluginLists, [
|
||||||
activeDevice,
|
activeDevice,
|
||||||
metroDevice,
|
metroDevice,
|
||||||
@@ -60,6 +70,22 @@ export const PluginList = memo(function PluginList({
|
|||||||
]);
|
]);
|
||||||
const isArchived = !!activeDevice?.isArchived;
|
const isArchived = !!activeDevice?.isArchived;
|
||||||
|
|
||||||
|
const annotatedDownloadablePlugins = useMemoize<
|
||||||
|
[Record<string, DownloadablePluginState>, DownloadablePluginDetails[]],
|
||||||
|
[plugin: DownloadablePluginDetails, downloadStatus?: PluginDownloadStatus][]
|
||||||
|
>(
|
||||||
|
(downloads, downloadablePlugins) => {
|
||||||
|
const downloadMap = new Map(
|
||||||
|
Object.values(downloads).map((x) => [x.plugin.id, x]),
|
||||||
|
);
|
||||||
|
return downloadablePlugins.map((plugin) => [
|
||||||
|
plugin,
|
||||||
|
downloadMap.get(plugin.id)?.status,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[downloads, downloadablePlugins],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAppPluginClick = useCallback(
|
const handleAppPluginClick = useCallback(
|
||||||
(pluginId) => {
|
(pluginId) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -99,15 +125,15 @@ export const PluginList = memo(function PluginList({
|
|||||||
);
|
);
|
||||||
const handleInstallPlugin = useCallback(
|
const handleInstallPlugin = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const plugin = uninstalledPlugins.find((p) => p.id === id)!;
|
const plugin = downloadablePlugins.find((p) => p.id === id)!;
|
||||||
dispatch(
|
dispatch(
|
||||||
startPluginDownload({
|
startPluginDownload({
|
||||||
plugin,
|
plugin,
|
||||||
enableDownloadedPlugin: true,
|
startedByUser: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[uninstalledPlugins, dispatch],
|
[downloadablePlugins, dispatch],
|
||||||
);
|
);
|
||||||
const handleUninstallPlugin = useCallback(
|
const handleUninstallPlugin = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
@@ -234,8 +260,9 @@ export const PluginList = memo(function PluginList({
|
|||||||
<PluginGroup
|
<PluginGroup
|
||||||
key="uninstalled"
|
key="uninstalled"
|
||||||
title="Detected in App"
|
title="Detected in App"
|
||||||
hint="The plugins below are supported by the selected device / application, but not installed in Flipper.">
|
hint="The plugins below are supported by the selected device / application, but not installed in Flipper.
|
||||||
{uninstalledPlugins.map((plugin) => (
|
To install plugin, hover it and click to the 'Download' icon.">
|
||||||
|
{annotatedDownloadablePlugins.map(([plugin, downloadStatus]) => (
|
||||||
<PluginEntry
|
<PluginEntry
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
plugin={plugin}
|
plugin={plugin}
|
||||||
@@ -244,9 +271,15 @@ export const PluginList = memo(function PluginList({
|
|||||||
actions={
|
actions={
|
||||||
<ActionButton
|
<ActionButton
|
||||||
id={plugin.id}
|
id={plugin.id}
|
||||||
title="Install and enable plugin"
|
title="Download and install plugin"
|
||||||
onClick={handleInstallPlugin}
|
onClick={handleInstallPlugin}
|
||||||
icon={<PlusOutlined size={16} style={{marginRight: 0}} />}
|
icon={
|
||||||
|
downloadStatus ? (
|
||||||
|
<LoadingOutlined size={16} style={{marginRight: 0}} />
|
||||||
|
) : (
|
||||||
|
<DownloadOutlined size={16} style={{marginRight: 0}} />
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
disabled
|
disabled
|
||||||
@@ -432,7 +465,7 @@ export function computePluginLists(
|
|||||||
const enabledPlugins: ClientPluginDefinition[] = [];
|
const enabledPlugins: ClientPluginDefinition[] = [];
|
||||||
const disabledPlugins: ClientPluginDefinition[] = [];
|
const disabledPlugins: ClientPluginDefinition[] = [];
|
||||||
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
|
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
|
||||||
const uninstalledPlugins: DownloadablePluginDetails[] = [];
|
const downloadablePlugins: DownloadablePluginDetails[] = [];
|
||||||
|
|
||||||
if (device) {
|
if (device) {
|
||||||
// find all device plugins that aren't part of the current device / metro
|
// find all device plugins that aren't part of the current device / metro
|
||||||
@@ -504,7 +537,7 @@ export function computePluginLists(
|
|||||||
);
|
);
|
||||||
uninstalledMarketplacePlugins.forEach((plugin) => {
|
uninstalledMarketplacePlugins.forEach((plugin) => {
|
||||||
if (client.supportsPlugin(plugin.id)) {
|
if (client.supportsPlugin(plugin.id)) {
|
||||||
uninstalledPlugins.push(plugin);
|
downloadablePlugins.push(plugin);
|
||||||
} else {
|
} else {
|
||||||
unavailablePlugins.push([
|
unavailablePlugins.push([
|
||||||
plugin,
|
plugin,
|
||||||
@@ -521,7 +554,7 @@ export function computePluginLists(
|
|||||||
unavailablePlugins.sort(([a], [b]) => {
|
unavailablePlugins.sort(([a], [b]) => {
|
||||||
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
||||||
});
|
});
|
||||||
uninstalledPlugins.sort((a, b) => {
|
downloadablePlugins.sort((a, b) => {
|
||||||
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -531,7 +564,7 @@ export function computePluginLists(
|
|||||||
enabledPlugins,
|
enabledPlugins,
|
||||||
disabledPlugins,
|
disabledPlugins,
|
||||||
unavailablePlugins,
|
unavailablePlugins,
|
||||||
uninstalledPlugins,
|
downloadablePlugins,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ describe('basic findBestDevice with metro present', () => {
|
|||||||
state.connections.userStarredPlugins,
|
state.connections.userStarredPlugins,
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
uninstalledPlugins: [],
|
downloadablePlugins: [],
|
||||||
devicePlugins: [logsPlugin],
|
devicePlugins: [logsPlugin],
|
||||||
metroPlugins: [logsPlugin],
|
metroPlugins: [logsPlugin],
|
||||||
enabledPlugins: [],
|
enabledPlugins: [],
|
||||||
@@ -237,12 +237,12 @@ describe('basic findBestDevice with metro present', () => {
|
|||||||
noopPlugin,
|
noopPlugin,
|
||||||
);
|
);
|
||||||
|
|
||||||
const supportedUninstalledPlugin = createMockDownloadablePluginDetails({
|
const supportedDownloadablePlugin = createMockDownloadablePluginDetails({
|
||||||
id: 'supportedUninstalledPlugin',
|
id: 'supportedUninstalledPlugin',
|
||||||
title: 'Supported Uninstalled Plugin',
|
title: 'Supported Uninstalled Plugin',
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsupportedUninstalledPlugin = createMockDownloadablePluginDetails({
|
const unsupportedDownloadablePlugin = createMockDownloadablePluginDetails({
|
||||||
id: 'unsupportedUninstalledPlugin',
|
id: 'unsupportedUninstalledPlugin',
|
||||||
title: 'Unsupported Uninstalled Plugin',
|
title: 'Unsupported Uninstalled Plugin',
|
||||||
});
|
});
|
||||||
@@ -258,8 +258,8 @@ describe('basic findBestDevice with metro present', () => {
|
|||||||
flipper.store.dispatch(addGatekeepedPlugins([gateKeepedPlugin]));
|
flipper.store.dispatch(addGatekeepedPlugins([gateKeepedPlugin]));
|
||||||
flipper.store.dispatch(
|
flipper.store.dispatch(
|
||||||
registerMarketplacePlugins([
|
registerMarketplacePlugins([
|
||||||
supportedUninstalledPlugin,
|
supportedDownloadablePlugin,
|
||||||
unsupportedUninstalledPlugin,
|
unsupportedDownloadablePlugin,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -297,11 +297,11 @@ describe('basic findBestDevice with metro present', () => {
|
|||||||
"Plugin 'Unsupported Plugin' is installed in Flipper, but not supported by the client application",
|
"Plugin 'Unsupported Plugin' is installed in Flipper, but not supported by the client application",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
unsupportedUninstalledPlugin,
|
unsupportedDownloadablePlugin,
|
||||||
"Plugin 'Unsupported Uninstalled Plugin' is not installed in Flipper and not supported by the client application",
|
"Plugin 'Unsupported Uninstalled Plugin' is not installed in Flipper and not supported by the client application",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
uninstalledPlugins: [supportedUninstalledPlugin],
|
downloadablePlugins: [supportedDownloadablePlugin],
|
||||||
});
|
});
|
||||||
|
|
||||||
flipper.store.dispatch(
|
flipper.store.dispatch(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function Link(props: {
|
|||||||
href: string;
|
href: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
onClick?: ((event: React.MouseEvent<any>) => void) | undefined;
|
||||||
}) {
|
}) {
|
||||||
const isSandy = useIsSandy();
|
const isSandy = useIsSandy();
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
@@ -42,9 +43,9 @@ export default function Link(props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return isSandy ? (
|
return isSandy ? (
|
||||||
<AntOriginalLink {...props} onClick={onClick} />
|
<AntOriginalLink {...props} onClick={props.onClick ?? onClick} />
|
||||||
) : (
|
) : (
|
||||||
<StyledLink onClick={onClick} style={props.style}>
|
<StyledLink onClick={props.onClick ?? onClick} style={props.style}>
|
||||||
{props.children || props.href}
|
{props.children || props.href}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user