/** * 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 {Layout, theme} from 'flipper-plugin'; import {LoadingIndicator, TableRows, ManagedTable, Glyph} from '../../ui'; import React, {useCallback, useState, useEffect} from 'react'; import { reportPlatformFailures, reportUsage, InstalledPluginDetails, } from 'flipper-common'; import reloadFlipper from '../../utils/reloadFlipper'; import {registerInstalledPlugins} from '../../reducers/plugins'; import { UpdateResult, getInstalledPlugins, getUpdatablePlugins, removePlugin, UpdatablePluginDetails, } from 'flipper-plugin-lib'; import {installPluginFromNpm} from 'flipper-plugin-lib'; import {State as AppState} from '../../reducers'; import {connect} from 'react-redux'; import {Dispatch, Action} from 'redux'; import PluginPackageInstaller from './PluginPackageInstaller'; import {Toolbar} from 'flipper-plugin'; import {Alert, Button, Input, Tooltip, Typography} from 'antd'; const {Text, Link} = Typography; const TAG = 'PluginInstaller'; const columnSizes = { name: '25%', version: '10%', description: 'flex', install: '15%', }; const columns = { name: { value: 'Name', }, version: { value: 'Version', }, description: { value: 'Description', }, install: { value: '', }, }; type PropsFromState = { installedPlugins: Map; }; type DispatchFromProps = { refreshInstalledPlugins: () => void; }; type OwnProps = { autoHeight: boolean; }; type Props = OwnProps & PropsFromState & DispatchFromProps; const defaultProps: OwnProps = { autoHeight: false, }; const PluginInstaller = function ({ refreshInstalledPlugins, installedPlugins, autoHeight, }: Props) { const [restartRequired, setRestartRequired] = useState(false); const [query, setQuery] = useState(''); const onInstall = useCallback(async () => { refreshInstalledPlugins(); setRestartRequired(true); }, [refreshInstalledPlugins]); const rows = useNPMSearch(query, onInstall, installedPlugins); const restartApp = useCallback(() => { reloadFlipper(); }, []); return ( {restartRequired && ( )} setQuery(e.target.value)} value={query} placeholder="Search Flipper plugins..." /> ); }; function InstallButton(props: { name: string; version: string; onInstall: () => void; updateStatus: UpdateResult; }) { type InstallAction = | {kind: 'Install'; error?: string} | {kind: 'Waiting'} | {kind: 'Remove'; error?: string} | {kind: 'Update'; error?: string}; const catchError = (actionKind: 'Install' | 'Remove' | 'Update', fn: () => Promise) => async () => { try { await fn(); } catch (err) { console.error( `Installation process of kind ${actionKind} failed with:`, err, ); setAction({kind: actionKind, error: err.toString()}); } }; const mkInstallCallback = (action: 'Install' | 'Update') => catchError(action, async () => { reportUsage( action === 'Install' ? `${TAG}:install` : `${TAG}:update`, undefined, props.name, ); setAction({kind: 'Waiting'}); await installPluginFromNpm(props.name); props.onInstall(); setAction({kind: 'Remove'}); }); const performInstall = useCallback(mkInstallCallback('Install'), [ props.name, props.version, ]); const performUpdate = useCallback(mkInstallCallback('Update'), [ props.name, props.version, ]); const performRemove = useCallback( catchError('Remove', async () => { reportUsage(`${TAG}:remove`, undefined, props.name); setAction({kind: 'Waiting'}); await removePlugin(props.name); props.onInstall(); setAction({kind: 'Install'}); }), [props.name], ); const [action, setAction] = useState( props.updateStatus.kind === 'update-available' ? {kind: 'Update'} : props.updateStatus.kind === 'not-installed' ? {kind: 'Install'} : {kind: 'Remove'}, ); if (action.kind === 'Waiting') { return ; } if ((action.kind === 'Install' || action.kind === 'Remove') && action.error) { } const button = ( ); if (action.error) { const glyph = ( ); return ( {button} ); } else { return button; } } function useNPMSearch( query: string, onInstall: () => void, installedPlugins: Map, ): TableRows { useEffect(() => { reportUsage(`${TAG}:open`); }, []); const [searchResults, setSearchResults] = useState( [], ); const createRow = useCallback( (h: UpdatablePluginDetails) => ({ key: h.name, columns: { name: { value: {h.name.replace(/^flipper-plugin-/, '')}, }, version: { value: {h.version}, align: 'flex-end' as 'flex-end', }, description: { value: ( {h.description} ), }, install: { value: ( ), align: 'center' as 'center', }, }, }), [onInstall], ); useEffect(() => { (async () => { let canceled = false; const updatablePlugins = await reportPlatformFailures( getUpdatablePlugins(query), `${TAG}:queryIndex`, ); if (canceled) { return; } setSearchResults(updatablePlugins); // Clean up: if query changes while we're searching, abandon results. return () => { canceled = true; }; })(); }, [query, installedPlugins]); const rows = searchResults.map(createRow); return rows; } PluginInstaller.defaultProps = defaultProps; export default connect( ({plugins: {installedPlugins}}) => ({ installedPlugins, }), (dispatch: Dispatch>) => ({ refreshInstalledPlugins: async () => { const plugins = await getInstalledPlugins(); dispatch(registerInstalledPlugins(plugins)); }, }), )(PluginInstaller);