From bd01b58566d91c8e39299d76fbaec020127327c5 Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Tue, 15 Dec 2020 09:28:58 -0800 Subject: [PATCH] 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 --- desktop/app/src/PluginContainer.tsx | 105 ++++++++++++++-- desktop/app/src/chrome/SettingsSheet.tsx | 34 ++--- .../chrome/plugin-manager/PluginInstaller.tsx | 6 +- .../dispatcher/fb-stubs/pluginAutoUpdate.tsx | 12 -- desktop/app/src/dispatcher/index.tsx | 2 - .../app/src/dispatcher/pluginDownloads.tsx | 119 ++++++++++++++---- desktop/app/src/dispatcher/pluginManager.tsx | 40 +++++- desktop/app/src/init.tsx | 8 +- .../reducers/__tests__/pluginManager.node.tsx | 8 +- desktop/app/src/reducers/pluginManager.tsx | 67 +++++++++- desktop/app/src/utils/isSandyEnabled.tsx | 21 ++++ desktop/app/src/utils/notifications.tsx | 32 +++++ desktop/app/src/utils/useIsDarkMode.tsx | 12 +- desktop/plugin-lib/src/pluginInstaller.ts | 1 + desktop/scripts/start-dev-server.ts | 9 ++ 15 files changed, 381 insertions(+), 95 deletions(-) delete mode 100644 desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx create mode 100644 desktop/app/src/utils/isSandyEnabled.tsx create mode 100644 desktop/app/src/utils/notifications.tsx diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index 8d5baf8de..3fd1b9669 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -46,10 +46,17 @@ import {Message} from './reducers/pluginMessageQueue'; import {Idler} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; import {ToggleButton, SmallText, Layout} from './ui'; -import {TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; +import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; import {isDevicePluginDefinition} from './utils/pluginUtils'; import ArchivedDevice from './devices/ArchivedDevice'; import {ContentContainer} from './sandy-chrome/ContentContainer'; +import {Alert, Typography} from 'antd'; +import {InstalledPluginDetails} from 'plugin-lib'; +import semver from 'semver'; +import {activatePlugin} from './reducers/pluginManager'; +import {produce} from 'immer'; + +const {Text, Link} = Typography; const Container = styled(FlexColumn)({ width: 0, @@ -109,6 +116,7 @@ type StateFromProps = { pendingMessages: Message[] | undefined; pluginIsEnabled: boolean; settingsState: Settings; + latestInstalledVersion: InstalledPluginDetails | undefined; }; type DispatchFromProps = { @@ -120,17 +128,24 @@ type DispatchFromProps = { setPluginState: (payload: {pluginKey: string; state: any}) => void; setStaticView: (payload: StaticView) => void; starPlugin: typeof starPlugin; + activatePlugin: typeof activatePlugin; }; type Props = StateFromProps & DispatchFromProps & OwnProps; type State = { progress: {current: number; total: number}; + autoUpdateAlertSuppressed: Set; }; class PluginContainer extends PureComponent { static contextType = ReactReduxContext; + constructor(props: Props) { + super(props); + this.reloadPlugin = this.reloadPlugin.bind(this); + } + plugin: | FlipperPlugin | FlipperDevicePlugin @@ -160,7 +175,10 @@ class PluginContainer extends PureComponent { idler?: Idler; pluginBeingProcessed: string | null = null; - state = {progress: {current: 0, total: 0}}; + state = { + progress: {current: 0, total: 0}, + autoUpdateAlertSuppressed: new Set(), + }; get store(): MiddlewareAPI { return this.context.store; @@ -200,7 +218,11 @@ class PluginContainer extends PureComponent { if (pluginKey !== this.pluginBeingProcessed) { this.pluginBeingProcessed = pluginKey; this.cancelCurrentQueue(); - this.setState({progress: {current: 0, total: 0}}); + this.setState((state) => + produce(state, (draft) => { + draft.progress = {current: 0, total: 0}; + }), + ); // device plugins don't have connections so no message queues if (!activePlugin || isDevicePluginDefinition(activePlugin)) { return; @@ -222,7 +244,11 @@ class PluginContainer extends PureComponent { pluginKey, this.store, (progress) => { - this.setState({progress}); + this.setState((state) => + produce(state, (draft) => { + draft.progress = progress; + }), + ); }, this.idler, ).then((completed) => { @@ -353,6 +379,17 @@ class PluginContainer extends PureComponent { ); } + reloadPlugin() { + const {activatePlugin, latestInstalledVersion} = this.props; + if (latestInstalledVersion) { + activatePlugin({ + plugin: latestInstalledVersion, + enable: false, + notifyIfFailed: true, + }); + } + } + renderPlugin() { const { pluginState, @@ -364,12 +401,20 @@ class PluginContainer extends PureComponent { selectedApp, settingsState, isSandy, + latestInstalledVersion, } = this.props; if (!activePlugin || !target || !pluginKey) { console.warn(`No selected plugin. Rendering empty!`); return this.renderNoPluginActive(); } let pluginElement: null | React.ReactElement; + const showUpdateAlert = + latestInstalledVersion && + activePlugin && + !this.state.autoUpdateAlertSuppressed.has( + `${latestInstalledVersion.name}@${latestInstalledVersion.version}`, + ) && + semver.gt(latestInstalledVersion.version, activePlugin.version); if (isSandyPlugin(activePlugin)) { // Make sure we throw away the container for different pluginKey! const instance = target.sandyPluginStates.get(activePlugin.id); @@ -438,15 +483,44 @@ class PluginContainer extends PureComponent { ); } return isSandy ? ( - - - {pluginElement} - - - + +
+ {showUpdateAlert && ( + + Plugin "{activePlugin.title}" v + {latestInstalledVersion?.version} downloaded and ready to + install. Reload to + start using new version. + + } + type="info" + onClose={() => + this.setState((state) => + produce(state, (draft) => { + draft.autoUpdateAlertSuppressed.add( + `${latestInstalledVersion?.name}@${latestInstalledVersion?.version}`, + ); + }), + ) + } + style={{marginBottom: theme.space.large}} + showIcon + closable + /> + )} +
+ + + {pluginElement} + + + +
) : ( @@ -475,6 +549,7 @@ export default connect( }, pluginStates, plugins: {devicePlugins, clientPlugins}, + pluginManager: {installedPlugins}, pluginMessageQueue, settingsState, }) => { @@ -525,6 +600,9 @@ export default connect( pendingMessages, pluginIsEnabled, settingsState, + latestInstalledVersion: installedPlugins.get( + activePlugin?.packageName ?? '', + ), }; return s; }, @@ -533,5 +611,6 @@ export default connect( selectPlugin, setStaticView, starPlugin, + activatePlugin, }, )(PluginContainer); diff --git a/desktop/app/src/chrome/SettingsSheet.tsx b/desktop/app/src/chrome/SettingsSheet.tsx index fba40db33..af083cb0b 100644 --- a/desktop/app/src/chrome/SettingsSheet.tsx +++ b/desktop/app/src/chrome/SettingsSheet.tsx @@ -30,6 +30,7 @@ import {reportUsage} from '../utils/metrics'; import {Modal, message} from 'antd'; import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin'; import GK from '../fb-stubs/GK'; +import ReleaseChannel from '../ReleaseChannel'; const Container = styled(FlexColumn)({ padding: 20, @@ -137,6 +138,9 @@ class SettingsSheet extends Component { disableSandy, darkMode, } = this.state.updatedSettings; + + const {releaseChannel} = this.state.updatedLauncherSettings; + const {useSandy} = this.props; const settingsPristine = @@ -255,20 +259,22 @@ class SettingsSheet extends Component { }); }} /> - {GK.get('flipper_sandy') && !disableSandy && ( - { - this.setState((prevState) => ({ - updatedSettings: { - ...prevState.updatedSettings, - darkMode: enabled, - }, - })); - }} - /> - )} + {(GK.get('flipper_sandy') || + releaseChannel == ReleaseChannel.INSIDERS) && + !disableSandy && ( + { + this.setState((prevState) => ({ + updatedSettings: { + ...prevState.updatedSettings, + darkMode: enabled, + }, + })); + }} + /> + )} ; }; type DispatchFromProps = { @@ -289,7 +289,7 @@ function InstallButton(props: { function useNPMSearch( query: string, onInstall: () => void, - installedPlugins: PluginDetails[], + installedPlugins: Map, ): TableRows_immutable { useEffect(() => { reportUsage(`${TAG}:open`); diff --git a/desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx b/desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx deleted file mode 100644 index 129d408f6..000000000 --- a/desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx +++ /dev/null @@ -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 -}; diff --git a/desktop/app/src/dispatcher/index.tsx b/desktop/app/src/dispatcher/index.tsx index b66a95f2f..e0d4f77aa 100644 --- a/desktop/app/src/dispatcher/index.tsx +++ b/desktop/app/src/dispatcher/index.tsx @@ -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 { user, pluginManager, reactNative, - pluginAutoUpdate, pluginMarketplace, pluginDownloads, ].filter(notNull); diff --git a/desktop/app/src/dispatcher/pluginDownloads.tsx b/desktop/app/src/dispatcher/pluginDownloads.tsx index 16f52533a..b6977cc0a 100644 --- a/desktop/app/src/dispatcher/pluginDownloads.tsx +++ b/desktop/app/src/dispatcher/pluginDownloads.tsx @@ -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: ( - - See{' '} - dispatch(setStaticView(ConsoleLogs))}> - logs - {' '} - for details. - - ), - 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, +) { + 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: ( +
+ {title} {version} has been downloaded. Reload is required to apply + the update.{' '} + + +
+ ), + severity: 'warning', + timestamp: Date.now(), + category: `Plugin Auto Update`, + }, + }), + ); +} diff --git a/desktop/app/src/dispatcher/pluginManager.tsx b/desktop/app/src/dispatcher/pluginManager.tsx index 48a53931a..06d4e9257 100644 --- a/desktop/app/src/dispatcher/pluginManager.tsx +++ b/desktop/app/src/dispatcher/pluginManager.tsx @@ -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)); + }, + ); }; diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index 30e9ff4d5..ce480b759 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -20,7 +20,6 @@ import {Store} from './reducers/index'; import dispatcher from './dispatcher/index'; import TooltipProvider from './ui/components/TooltipProvider'; import config from './utils/processConfig'; -import appConfig from '../src/fb-stubs/config'; import {initLauncherHooks} from './utils/launcher'; import {setPersistor} from './utils/persistor'; import React from 'react'; @@ -43,7 +42,7 @@ import { _LoggerContext, } from 'flipper-plugin'; import isProduction from './utils/isProduction'; -import ReleaseChannel from './ReleaseChannel'; +import isSandyEnabled from './utils/isSandyEnabled'; if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { // By default Node.JS has its internal certificate storage and doesn't use @@ -125,10 +124,7 @@ function init() { store, {name: 'loadTheme', fireImmediately: true, throttleMs: 500}, (state) => ({ - sandy: - (GK.get('flipper_sandy') || - appConfig.getReleaseChannel() === ReleaseChannel.INSIDERS) && - !state.settingsState.disableSandy, + sandy: isSandyEnabled(), dark: state.settingsState.darkMode, }), (theme) => { diff --git a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx index bd2778650..4d7f5dd08 100644 --- a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx @@ -12,7 +12,7 @@ import {InstalledPluginDetails} from 'flipper-plugin-lib'; test('reduce empty registerInstalledPlugins', () => { const result = reducer(undefined, registerInstalledPlugins([])); - expect(result.installedPlugins).toEqual([]); + expect(result.installedPlugins).toEqual(new Map()); }); const EXAMPLE_PLUGIN = { @@ -32,7 +32,9 @@ const EXAMPLE_PLUGIN = { test('reduce registerInstalledPlugins, clear again', () => { const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN])); - expect(result.installedPlugins).toEqual([EXAMPLE_PLUGIN]); + expect(result.installedPlugins).toEqual( + new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]), + ); const result2 = reducer(result, registerInstalledPlugins([])); - expect(result2.installedPlugins).toEqual([]); + expect(result2.installedPlugins).toEqual(new Map()); }); diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index 7d3664b88..b1130c118 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -8,13 +8,24 @@ */ import {Actions} from './'; -import {InstalledPluginDetails} from 'flipper-plugin-lib'; +import { + ActivatablePluginDetails, + InstalledPluginDetails, +} from 'flipper-plugin-lib'; import {PluginDefinition} from '../plugin'; import {produce} from 'immer'; +import semver from 'semver'; export type State = { - installedPlugins: InstalledPluginDetails[]; + installedPlugins: Map; uninstalledPlugins: Set; + pluginActivationQueue: PluginActivationRequest[]; +}; + +export type PluginActivationRequest = { + plugin: ActivatablePluginDetails; + enable: boolean; + notifyIfFailed: boolean; }; export type Action = @@ -26,11 +37,24 @@ export type Action = // Implemented by rootReducer in `store.tsx` type: 'UNINSTALL_PLUGIN'; payload: PluginDefinition; + } + | { + type: 'PLUGIN_INSTALLED'; + payload: InstalledPluginDetails; + } + | { + type: 'ACTIVATE_PLUGINS'; + payload: PluginActivationRequest[]; + } + | { + type: 'PLUGIN_ACTIVATION_HANDLED'; + payload: number; }; const INITIAL_STATE: State = { - installedPlugins: [], + installedPlugins: new Map(), uninstalledPlugins: new Set(), + pluginActivationQueue: [], }; export default function reducer( @@ -39,10 +63,28 @@ export default function reducer( ): State { if (action.type === 'REGISTER_INSTALLED_PLUGINS') { return produce(state, (draft) => { - draft.installedPlugins = action.payload.filter( - (p) => !state.uninstalledPlugins?.has(p.name), + draft.installedPlugins = new Map( + action.payload + .filter((p) => !state.uninstalledPlugins?.has(p.name)) + .map((p) => [p.name, p]), ); }); + } else if (action.type === 'PLUGIN_INSTALLED') { + const plugin = action.payload; + return produce(state, (draft) => { + const existing = draft.installedPlugins.get(plugin.name); + if (!existing || semver.gt(plugin.version, existing.version)) { + draft.installedPlugins.set(plugin.name, plugin); + } + }); + } else if (action.type === 'ACTIVATE_PLUGINS') { + return produce(state, (draft) => { + draft.pluginActivationQueue.push(...action.payload); + }); + } else if (action.type === 'PLUGIN_ACTIVATION_HANDLED') { + return produce(state, (draft) => { + draft.pluginActivationQueue.splice(0, action.payload); + }); } else { return {...state}; } @@ -59,3 +101,18 @@ export const uninstallPlugin = (payload: PluginDefinition): Action => ({ type: 'UNINSTALL_PLUGIN', payload, }); + +export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({ + type: 'PLUGIN_INSTALLED', + payload, +}); + +export const activatePlugin = (payload: PluginActivationRequest): Action => ({ + type: 'ACTIVATE_PLUGINS', + payload: [payload], +}); + +export const pluginActivationHandled = (payload: number): Action => ({ + type: 'PLUGIN_ACTIVATION_HANDLED', + payload, +}); diff --git a/desktop/app/src/utils/isSandyEnabled.tsx b/desktop/app/src/utils/isSandyEnabled.tsx new file mode 100644 index 000000000..5249c9ba4 --- /dev/null +++ b/desktop/app/src/utils/isSandyEnabled.tsx @@ -0,0 +1,21 @@ +/** + * 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 config from '../fb-stubs/config'; +import GK from '../fb-stubs/GK'; +import ReleaseChannel from '../ReleaseChannel'; +import {store} from '../store'; + +export default function isSandyEnabled() { + return ( + (GK.get('flipper_sandy') || + config.getReleaseChannel() === ReleaseChannel.INSIDERS) && + !store.getState().settingsState.disableSandy + ); +} diff --git a/desktop/app/src/utils/notifications.tsx b/desktop/app/src/utils/notifications.tsx new file mode 100644 index 000000000..4e1317133 --- /dev/null +++ b/desktop/app/src/utils/notifications.tsx @@ -0,0 +1,32 @@ +/** + * 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 {notification, Typography} from 'antd'; +import React from 'react'; +import {ConsoleLogs} from '../chrome/ConsoleLogs'; +import {setStaticView} from '../reducers/connections'; +import {store} from '../store'; + +const {Text, Link} = Typography; + +export function showErrorNotification(message: string) { + notification.error({ + message, + description: ( + + See{' '} + store.dispatch(setStaticView(ConsoleLogs))}> + logs + {' '} + for details. + + ), + placement: 'bottomLeft', + }); +} diff --git a/desktop/app/src/utils/useIsDarkMode.tsx b/desktop/app/src/utils/useIsDarkMode.tsx index 1dba64626..20abaf33e 100644 --- a/desktop/app/src/utils/useIsDarkMode.tsx +++ b/desktop/app/src/utils/useIsDarkMode.tsx @@ -8,20 +8,12 @@ */ import {useStore} from '../../../app/src/utils/useStore'; -import config from '../fb-stubs/config'; -import GK from '../fb-stubs/GK'; -import ReleaseChannel from '../ReleaseChannel'; +import isSandyEnabled from './isSandyEnabled'; /** * This hook returns whether dark mode is currently being used. * Generally should be avoided in favor of using the above theme object, * which will provide colors that reflect the theme */ export function useIsDarkMode(): boolean { - return useStore( - (state) => - (GK.get('flipper_sandy') || - config.getReleaseChannel() === ReleaseChannel.INSIDERS) && - !state.settingsState.disableSandy && - state.settingsState.darkMode, - ); + return useStore((state) => isSandyEnabled() && state.settingsState.darkMode); } diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index 49daa678e..11ce2ec95 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -230,6 +230,7 @@ async function getInstalledPluginVersionDirs(): Promise< pmap(dirs, (dir) => fs .readdir(dir) + .then((versionDirs) => versionDirs.filter((d) => semver.valid(d))) .then((versionDirs) => versionDirs.sort((v1, v2) => semver.compare(v2, v1, true)), ) diff --git a/desktop/scripts/start-dev-server.ts b/desktop/scripts/start-dev-server.ts index eeca20985..6de165085 100644 --- a/desktop/scripts/start-dev-server.ts +++ b/desktop/scripts/start-dev-server.ts @@ -50,6 +50,11 @@ const argv = yargs '[FB-internal only] Enable plugin auto-updates. The flag is disabled by default in dev mode. Env var FLIPPER_NO_PLUGIN_AUTO_UPDATE is equivalent to the command-line option "--no-plugin-auto-update"', type: 'boolean', }, + 'plugin-auto-update-interval': { + describe: + '[FB-internal only] Set custom interval in milliseconds for plugin auto-update checks. Env var FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL is equivalent to this command-line option.', + type: 'number', + }, 'enabled-plugins': { describe: 'Load only specified plugins and skip loading rest. This is useful when you are developing only one or few plugins. Plugins to load can be specified as a comma-separated list with either plugin id or name used as identifier, e.g. "--enabled-plugins network,inspector". The flag is not provided by default which means that all plugins loaded.', @@ -115,6 +120,10 @@ if ( process.env.FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE = 'true'; } +if (argv['plugin-auto-update-interval']) { + process.env.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL = `${argv['plugin-auto-update-interval']}`; +} + // Force participating in all GKs. Mostly intersting for Flipper team members. if (argv['enable-all-gks'] === true) { process.env.FLIPPER_ENABLE_ALL_GKS = 'true';