Files
flipper/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx
Anton Nikolaev dfbf66408a Fix search by query in Plugin Manager
Summary: I accidently broke search by query in a previous diff, so fixing it here

Reviewed By: passy

Differential Revision: D23842187

fbshipit-source-id: 9fcc7a46048ff99e1bf26c8a70ef0240b38018cb
2020-09-22 10:35:55 -07:00

380 lines
8.8 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 {
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 (
<>
<Container>
{restartRequired && (
<RestartBar onClick={restartApp}>
To apply the changes, Flipper needs to reload. Click here to reload!
</RestartBar>
)}
<Toolbar>
<SearchBox>
<SearchInput
onChange={(e) => setQuery(e.target.value)}
value={query}
placeholder="Search Flipper plugins..."
/>
</SearchBox>
</Toolbar>
<ManagedTable_immutable
rowLineHeight={28}
floating={false}
multiline={true}
columnSizes={columnSizes}
columns={columns}
highlightableRows={false}
highlightedRows={new Set()}
autoHeight={autoHeight}
rows={rows}
/>
</Container>
<PluginPackageInstaller onInstall={onInstall} />
</>
);
};
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<void>,
) => 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<InstallAction>(
props.updateStatus.kind === 'update-available'
? {kind: 'Update'}
: props.updateStatus.kind === 'not-installed'
? {kind: 'Install'}
: {kind: 'Remove'},
);
if (action.kind === 'Waiting') {
return <Spinner size={16} />;
}
if ((action.kind === 'Install' || action.kind === 'Remove') && action.error) {
}
const button = (
<TableButton
compact
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}
</TableButton>
);
if (action.error) {
const glyph = (
<AlignedGlyph color={colors.orange} size={16} name="caution-triangle" />
);
return (
<FlexRow>
<Tooltip
options={{position: 'toLeft'}}
title={`Something went wrong: ${action.error}`}
children={glyph}
/>
{button}
</FlexRow>
);
} else {
return button;
}
}
function useNPMSearch(
query: string,
onInstall: () => void,
installedPlugins: InstalledPluginDetails[],
): TableRows_immutable {
useEffect(() => {
reportUsage(`${TAG}:open`);
}, []);
const [searchResults, setSearchResults] = useState<UpdatablePluginDetails[]>(
[],
);
const createRow = useCallback(
(h: UpdatablePluginDetails) => ({
key: h.name,
columns: {
name: {
value: (
<EllipsisText>
{h.name.replace(/^flipper-plugin-/, '')}
</EllipsisText>
),
},
version: {
value: <EllipsisText>{h.version}</EllipsisText>,
align: 'flex-end' as 'flex-end',
},
description: {
value: (
<FlexRow grow>
<EllipsisText>{h.description}</EllipsisText>
<Spacer />
<Link href={`https://yarnpkg.com/en/package/${h.name}`}>
<Glyph color={colors.light20} name="info-circle" size={16} />
</Link>
</FlexRow>
),
},
install: {
value: (
<InstallButton
name={h.name}
version={h.version}
onInstall={onInstall}
updateStatus={h.updateStatus}
/>
),
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<PropsFromState, DispatchFromProps, OwnProps, AppState>(
({pluginManager: {installedPlugins}}) => ({
installedPlugins,
}),
(dispatch: Dispatch<Action<any>>) => ({
refreshInstalledPlugins: () => {
getInstalledPlugins().then((plugins) =>
dispatch(registerInstalledPlugins(plugins)),
);
},
}),
)(PluginInstaller);