Files
flipper/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx
Michel Weststrate 3e7a6b1b4b Move metrics / tracking / error analysis to flipper-common package
Summary:
what else can I say

move_complexity

Reviewed By: passy

Differential Revision: D31483414

fbshipit-source-id: 1692c792121a3aae0843eb238040cae0445cdf54
2021-10-12 16:00:52 -07:00

332 lines
8.2 KiB
TypeScript

/**
* 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} from 'flipper-common';
import reloadFlipper from '../../utils/reloadFlipper';
import {registerInstalledPlugins} from '../../reducers/plugins';
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';
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<string, 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 (
<Layout.Container gap height={500}>
{restartRequired && (
<Alert
onClick={restartApp}
type="error"
message="To apply the changes, Flipper needs to reload. Click here to reload!"
style={{cursor: 'pointer'}}
/>
)}
<Toolbar>
<Input.Search
onChange={(e) => setQuery(e.target.value)}
value={query}
placeholder="Search Flipper plugins..."
/>
</Toolbar>
<ManagedTable
rowLineHeight={28}
floating={false}
multiline
columnSizes={columnSizes}
columns={columns}
highlightableRows={false}
highlightedRows={new Set()}
autoHeight={autoHeight}
rows={rows}
horizontallyScrollable
/>
<PluginPackageInstaller onInstall={onInstall} />
</Layout.Container>
);
};
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<void>) =>
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<InstallAction>(
props.updateStatus.kind === 'update-available'
? {kind: 'Update'}
: props.updateStatus.kind === 'not-installed'
? {kind: 'Install'}
: {kind: 'Remove'},
);
if (action.kind === 'Waiting') {
return <LoadingIndicator size={16} />;
}
if ((action.kind === 'Install' || action.kind === 'Remove') && action.error) {
}
const button = (
<Button
size="small"
type={action.kind !== 'Remove' ? 'primary' : undefined}
onClick={() => {
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}
</Button>
);
if (action.error) {
const glyph = (
<Glyph color={theme.warningColor} size={16} name="caution-triangle" />
);
return (
<Layout.Horizontal gap>
<Tooltip
placement="leftBottom"
title={`Something went wrong: ${action.error}`}
children={glyph}
/>
{button}
</Layout.Horizontal>
);
} else {
return button;
}
}
function useNPMSearch(
query: string,
onInstall: () => void,
installedPlugins: Map<string, InstalledPluginDetails>,
): TableRows {
useEffect(() => {
reportUsage(`${TAG}:open`);
}, []);
const [searchResults, setSearchResults] = useState<UpdatablePluginDetails[]>(
[],
);
const createRow = useCallback(
(h: UpdatablePluginDetails) => ({
key: h.name,
columns: {
name: {
value: <Text ellipsis>{h.name.replace(/^flipper-plugin-/, '')}</Text>,
},
version: {
value: <Text ellipsis>{h.version}</Text>,
align: 'flex-end' as 'flex-end',
},
description: {
value: (
<Layout.Horizontal center gap>
<Text ellipsis>{h.description}</Text>
<Link href={`https://yarnpkg.com/en/package/${h.name}`}>
<Glyph
color={theme.textColorActive}
name="info-circle"
size={16}
/>
</Link>
</Layout.Horizontal>
),
},
install: {
value: (
<InstallButton
name={h.name}
version={h.version}
onInstall={onInstall}
updateStatus={h.updateStatus}
/>
),
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<PropsFromState, DispatchFromProps, OwnProps, AppState>(
({plugins: {installedPlugins}}) => ({
installedPlugins,
}),
(dispatch: Dispatch<Action<any>>) => ({
refreshInstalledPlugins: async () => {
const plugins = await getInstalledPlugins();
dispatch(registerInstalledPlugins(plugins));
},
}),
)(PluginInstaller);