Sandy-based plugin auto-update UI
Summary: New UX/UI for plugin auto-updates based on Sandy: - disabled plugins auto-updated silently without any notifications as there is no active state for them so there is nothing to loose. - enabled plugins can have some state and user can actually work with them, so we cannot reload them automatically. Instead, we show notification in the top of the plugin container asking user to reload the plugin when she is ready. - if the auto-updated plugin failed to reload - show error notification. - for non-sandy we continue using notifications as before. Reviewed By: mweststrate Differential Revision: D25530384 fbshipit-source-id: de3d0565ef0b930c9343b9e0ed07a4acb51885be
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5383017299
commit
bd01b58566
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export default () => {
|
||||
// Auto-updates of plugins not implemented in public version of Flipper
|
||||
};
|
||||
@@ -20,7 +20,6 @@ import plugins from './plugins';
|
||||
import user from './user';
|
||||
import pluginManager from './pluginManager';
|
||||
import reactNative from './reactNative';
|
||||
import pluginAutoUpdate from './fb-stubs/pluginAutoUpdate';
|
||||
import pluginMarketplace from './fb-stubs/pluginMarketplace';
|
||||
import pluginDownloads from './pluginDownloads';
|
||||
|
||||
@@ -48,7 +47,6 @@ export default function (store: Store, logger: Logger): () => Promise<void> {
|
||||
user,
|
||||
pluginManager,
|
||||
reactNative,
|
||||
pluginAutoUpdate,
|
||||
pluginMarketplace,
|
||||
pluginDownloads,
|
||||
].filter(notNull);
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
DownloadablePluginDetails,
|
||||
getInstalledPluginDetails,
|
||||
getPluginVersionInstallationDir,
|
||||
InstalledPluginDetails,
|
||||
installPluginFromFile,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {Store} from '../reducers/index';
|
||||
import {Actions, State, Store} from '../reducers/index';
|
||||
import {
|
||||
PluginDownloadStatus,
|
||||
pluginDownloadStarted,
|
||||
@@ -26,12 +27,16 @@ import path from 'path';
|
||||
import tmp from 'tmp';
|
||||
import {promisify} from 'util';
|
||||
import {requirePlugin} from './plugins';
|
||||
import {registerPluginUpdate, setStaticView} from '../reducers/connections';
|
||||
import {notification, Typography} from 'antd';
|
||||
import {registerPluginUpdate, selectPlugin} from '../reducers/connections';
|
||||
import {Button} from 'antd';
|
||||
import React from 'react';
|
||||
import {ConsoleLogs} from '../chrome/ConsoleLogs';
|
||||
|
||||
const {Text, Link} = Typography;
|
||||
import {reportUsage} from '../utils/metrics';
|
||||
import {addNotification, removeNotification} from '../reducers/notifications';
|
||||
import reloadFlipper from '../utils/reloadFlipper';
|
||||
import {activatePlugin, pluginInstalled} from '../reducers/pluginManager';
|
||||
import {Dispatch} from 'redux';
|
||||
import {showErrorNotification} from '../utils/notifications';
|
||||
import isSandyEnabled from '../utils/isSandyEnabled';
|
||||
|
||||
// Adapter which forces node.js implementation for axios instead of browser implementation
|
||||
// used by default in Electron. Node.js implementation is better, because it
|
||||
@@ -71,6 +76,7 @@ async function handlePluginDownload(
|
||||
);
|
||||
const tmpDir = await getTempDirName();
|
||||
const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`);
|
||||
let installedPlugin: InstalledPluginDetails | undefined;
|
||||
try {
|
||||
const cancellationSource = axios.CancelToken.source();
|
||||
dispatch(
|
||||
@@ -80,6 +86,7 @@ async function handlePluginDownload(
|
||||
console.log(
|
||||
`Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`,
|
||||
);
|
||||
installedPlugin = await getInstalledPluginDetails(installationDir);
|
||||
} else {
|
||||
await fs.ensureDir(tmpDir);
|
||||
let percentCompleted = 0;
|
||||
@@ -111,17 +118,19 @@ async function handlePluginDownload(
|
||||
await new Promise((resolve, reject) =>
|
||||
writeStream.once('finish', resolve).once('error', reject),
|
||||
);
|
||||
await installPluginFromFile(tmpFile);
|
||||
installedPlugin = await installPluginFromFile(tmpFile);
|
||||
dispatch(pluginInstalled(installedPlugin));
|
||||
}
|
||||
const installedPlugin = await getInstalledPluginDetails(installationDir);
|
||||
if (!store.getState().plugins.clientPlugins.has(plugin.id)) {
|
||||
const pluginDefinition = requirePlugin(installedPlugin);
|
||||
if (pluginIsDisabledForAllConnectedClients(store.getState(), plugin)) {
|
||||
dispatch(
|
||||
registerPluginUpdate({
|
||||
plugin: pluginDefinition,
|
||||
enablePlugin: startedByUser,
|
||||
activatePlugin({
|
||||
plugin: installedPlugin,
|
||||
enable: startedByUser,
|
||||
notifyIfFailed: startedByUser,
|
||||
}),
|
||||
);
|
||||
} else if (!isSandyEnabled()) {
|
||||
notifyAboutUpdatedPluginNonSandy(installedPlugin, store.dispatch);
|
||||
}
|
||||
console.log(
|
||||
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
|
||||
@@ -132,22 +141,80 @@ async function handlePluginDownload(
|
||||
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',
|
||||
});
|
||||
showErrorNotification(
|
||||
`Failed to download plugin "${title}" v${version}.`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
dispatch(pluginDownloadFinished({plugin}));
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
function pluginIsDisabledForAllConnectedClients(
|
||||
state: State,
|
||||
plugin: DownloadablePluginDetails,
|
||||
) {
|
||||
return (
|
||||
!state.plugins.clientPlugins.has(plugin.id) ||
|
||||
!state.connections.clients.some((c) =>
|
||||
state.connections.userStarredPlugins[c.query.app]?.includes(plugin.id),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function notifyAboutUpdatedPluginNonSandy(
|
||||
plugin: InstalledPluginDetails,
|
||||
dispatch: Dispatch<Actions>,
|
||||
) {
|
||||
const {name, version, title, id} = plugin;
|
||||
const reloadPluginAndRemoveNotification = () => {
|
||||
reportUsage('plugin-auto-update:notification:reloadClicked', undefined, id);
|
||||
dispatch(
|
||||
registerPluginUpdate({
|
||||
plugin: requirePlugin(plugin),
|
||||
enablePlugin: false,
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
removeNotification({
|
||||
pluginId: 'plugin-auto-update',
|
||||
client: null,
|
||||
notificationId: `auto-update.${name}.${version}`,
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: id,
|
||||
deepLinkPayload: null,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const reloadAll = () => {
|
||||
reportUsage('plugin-auto-update:notification:reloadAllClicked');
|
||||
reloadFlipper();
|
||||
};
|
||||
dispatch(
|
||||
addNotification({
|
||||
pluginId: 'plugin-auto-update',
|
||||
client: null,
|
||||
notification: {
|
||||
id: `auto-update.${name}.${version}`,
|
||||
title: `${title} ${version} is ready to install`,
|
||||
message: (
|
||||
<div>
|
||||
{title} {version} has been downloaded. Reload is required to apply
|
||||
the update.{' '}
|
||||
<Button onClick={reloadPluginAndRemoveNotification}>
|
||||
Reload Plugin
|
||||
</Button>
|
||||
<Button onClick={reloadAll}>Reload Flipper</Button>
|
||||
</div>
|
||||
),
|
||||
severity: 'warning',
|
||||
timestamp: Date.now(),
|
||||
category: `Plugin Auto Update`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,19 @@
|
||||
|
||||
import {Store} from '../reducers/index';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
||||
import {
|
||||
pluginActivationHandled,
|
||||
registerInstalledPlugins,
|
||||
} from '../reducers/pluginManager';
|
||||
import {
|
||||
getInstalledPlugins,
|
||||
cleanupOldInstalledPluginVersions,
|
||||
removePlugins,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {sideEffect} from '../utils/sideEffect';
|
||||
import {requirePlugin} from './plugins';
|
||||
import {registerPluginUpdate} from '../reducers/connections';
|
||||
import {showErrorNotification} from '../utils/notifications';
|
||||
|
||||
const maxInstalledPluginVersionsToKeep = 2;
|
||||
|
||||
@@ -32,4 +39,35 @@ export default (store: Store, _logger: Logger) => {
|
||||
window.requestIdleCallback(() => {
|
||||
refreshInstalledPlugins(store);
|
||||
});
|
||||
|
||||
sideEffect(
|
||||
store,
|
||||
{name: 'handlePluginActivation', throttleMs: 1000, fireImmediately: true},
|
||||
(state) => state.pluginManager.pluginActivationQueue,
|
||||
(queue, store) => {
|
||||
for (const request of queue) {
|
||||
try {
|
||||
const plugin = requirePlugin(request.plugin);
|
||||
const enablePlugin = request.enable;
|
||||
store.dispatch(
|
||||
registerPluginUpdate({
|
||||
plugin,
|
||||
enablePlugin,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to activate plugin ${request.plugin.title} v${request.plugin.version}`,
|
||||
err,
|
||||
);
|
||||
if (request.notifyIfFailed) {
|
||||
showErrorNotification(
|
||||
`Failed to load plugin "${request.plugin.title}" v${request.plugin.version}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
store.dispatch(pluginActivationHandled(queue.length));
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user