/** * 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 { FlexColumn, styled, ManagedTable_immutable, Toolbar, SearchInput, SearchBox, Button, colors, Spacer, TableRows_immutable, FlexRow, Glyph, Link, Text, LoadingIndicator, Tooltip, } from 'flipper'; import React, {useCallback, useState, useEffect} from 'react'; import {List} from 'immutable'; import {reportPlatformFailures, reportUsage} from '../../utils/metrics'; import reloadFlipper from '../../utils/reloadFlipper'; import {registerInstalledPlugins} from '../../reducers/pluginManager'; import { UpdateResult, getInstalledPlugins, getUpdatablePlugins, removePlugin, UpdatablePluginDetails, InstalledPluginDetails, } 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'; const TAG = 'PluginInstaller'; const EllipsisText = styled(Text)({ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); const columnSizes = { name: '25%', version: '10%', description: 'flex', install: '15%', }; const columns = { name: { value: 'Name', }, version: { value: 'Version', }, description: { value: 'Description', }, install: { value: '', }, }; const Container = styled(FlexColumn)({ height: 300, backgroundColor: colors.white, borderRadius: 4, overflow: 'hidden', border: `1px solid ${colors.macOSTitleBarButtonBorder}`, }); const RestartBar = styled(FlexColumn)({ backgroundColor: colors.red, color: colors.white, fontWeight: 500, padding: 10, cursor: 'pointer', textAlign: 'center', }); type PropsFromState = { installedPlugins: InstalledPluginDetails[]; }; 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 && ( To apply the changes, Flipper needs to reload. Click here to reload! )} setQuery(e.target.value)} value={query} placeholder="Search Flipper plugins..." /> ); }; const TableButton = styled(Button)({ marginTop: 2, }); const Spinner = styled(LoadingIndicator)({ marginTop: 6, }); const AlignedGlyph = styled(Glyph)({ marginTop: 6, }); 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(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 = ( { switch (action.kind) { case 'Install': reportPlatformFailures(performInstall(), `${TAG}:install`); break; case 'Remove': reportPlatformFailures(performRemove(), `${TAG}:remove`); break; case 'Update': reportPlatformFailures(performUpdate(), `${TAG}:update`); break; } }}> {action.kind} ); if (action.error) { const glyph = ( ); return ( {button} ); } else { return button; } } function useNPMSearch( query: string, onInstall: () => void, installedPlugins: InstalledPluginDetails[], ): TableRows_immutable { 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 cancelled = false; const updatablePlugins = await reportPlatformFailures( getUpdatablePlugins(query), `${TAG}:queryIndex`, ); if (cancelled) { return; } setSearchResults(updatablePlugins); // Clean up: if query changes while we're searching, abandon results. return () => { cancelled = true; }; })(); }, [query, installedPlugins]); const rows: TableRows_immutable = List(searchResults.map(createRow)); return rows; } PluginInstaller.defaultProps = defaultProps; export default connect( ({pluginManager: {installedPlugins}}) => ({ installedPlugins, }), (dispatch: Dispatch>) => ({ refreshInstalledPlugins: () => { getInstalledPlugins().then((plugins) => dispatch(registerInstalledPlugins(plugins)), ); }, }), )(PluginInstaller);