Move desktop-related code to "desktop" subfolder (#872)
Summary: Pull Request resolved: https://github.com/facebook/flipper/pull/872 Move all the JS code related to desktop app to "desktop" subfolder. The structure of "desktop" folder: - `src` - JS code of Flipper desktop app executing in Electron Renderer (Chrome) process. This folder also contains all the Flipper plugins in subfolder "src/plugins". - `static` - JS code of Flipper desktop app bootstrapping executing in Electron Main (Node.js) process - `pkg` - Flipper packaging lib and CLI tool - `doctor` - Flipper diagnostics lib and CLI tool - `scripts` - Build scripts for Flipper desktop app - `headless` - Headless version of Flipper app - `headless-tests` - Integration tests running agains Flipper headless version Reviewed By: passy Differential Revision: D20249304 fbshipit-source-id: 9a51c63b51b92b758a02fc8ebf7d3d116770efe9
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a60e6fee87
commit
85c13bb1f3
440
desktop/src/chrome/plugin-manager/PluginInstaller.tsx
Normal file
440
desktop/src/chrome/plugin-manager/PluginInstaller.tsx
Normal file
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* 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, useMemo, useEffect} from 'react';
|
||||
import {List} from 'immutable';
|
||||
import {SearchIndex} from 'algoliasearch';
|
||||
import {SearchResponse} from '@algolia/client-search';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
|
||||
import restartFlipper from '../../utils/restartFlipper';
|
||||
import {
|
||||
PluginMap,
|
||||
PluginDefinition,
|
||||
registerInstalledPlugins,
|
||||
} from '../../reducers/pluginManager';
|
||||
import {
|
||||
PLUGIN_DIR,
|
||||
readInstalledPlugins,
|
||||
provideSearchIndex,
|
||||
findPluginUpdates as _findPluginUpdates,
|
||||
UpdateResult,
|
||||
installPluginFromNpm,
|
||||
} from '../../utils/pluginManager';
|
||||
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: PluginMap;
|
||||
};
|
||||
|
||||
type DispatchFromProps = {
|
||||
refreshInstalledPlugins: () => void;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
searchIndexFactory: () => SearchIndex;
|
||||
autoHeight: boolean;
|
||||
findPluginUpdates: (
|
||||
currentPlugins: PluginMap,
|
||||
) => Promise<[string, UpdateResult][]>;
|
||||
};
|
||||
|
||||
type Props = OwnProps & PropsFromState & DispatchFromProps;
|
||||
|
||||
const defaultProps: OwnProps = {
|
||||
searchIndexFactory: provideSearchIndex,
|
||||
autoHeight: false,
|
||||
findPluginUpdates: _findPluginUpdates,
|
||||
};
|
||||
|
||||
type UpdatablePlugin = {
|
||||
updateStatus: UpdateResult;
|
||||
};
|
||||
|
||||
type UpdatablePluginDefinition = PluginDefinition & UpdatablePlugin;
|
||||
|
||||
// exported for testing
|
||||
export function annotatePluginsWithUpdates(
|
||||
installedPlugins: Map<string, PluginDefinition>,
|
||||
updates: Map<string, UpdateResult>,
|
||||
): Map<string, UpdatablePluginDefinition> {
|
||||
const annotated: Array<[string, UpdatablePluginDefinition]> = Array.from(
|
||||
installedPlugins.entries(),
|
||||
).map(([key, value]) => {
|
||||
const updateStatus = updates.get(key) || {kind: 'up-to-date'};
|
||||
return [key, {...value, updateStatus: updateStatus}];
|
||||
});
|
||||
return new Map(annotated);
|
||||
}
|
||||
|
||||
const PluginInstaller = function props(props: Props) {
|
||||
const [restartRequired, setRestartRequired] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const onInstall = useCallback(async () => {
|
||||
props.refreshInstalledPlugins();
|
||||
setRestartRequired(true);
|
||||
}, []);
|
||||
|
||||
const rows = useNPMSearch(
|
||||
query,
|
||||
setQuery,
|
||||
props.searchIndexFactory,
|
||||
props.installedPlugins,
|
||||
onInstall,
|
||||
props.findPluginUpdates,
|
||||
);
|
||||
const restartApp = useCallback(() => {
|
||||
restartFlipper();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
{restartRequired && (
|
||||
<RestartBar onClick={restartApp}>
|
||||
To activate this plugin, Flipper needs to restart. Click here to
|
||||
restart!
|
||||
</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={props.autoHeight}
|
||||
rows={rows}
|
||||
/>
|
||||
</Container>
|
||||
<PluginPackageInstaller onInstall={onInstall} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
PluginInstaller.defaultProps = defaultProps;
|
||||
|
||||
const TableButton = styled(Button)({
|
||||
marginTop: 2,
|
||||
});
|
||||
|
||||
const Spinner = styled(LoadingIndicator)({
|
||||
marginTop: 6,
|
||||
});
|
||||
|
||||
const AlignedGlyph = styled(Glyph)({
|
||||
marginTop: 6,
|
||||
});
|
||||
|
||||
function liftUpdatable(val: PluginDefinition): UpdatablePluginDefinition {
|
||||
return {
|
||||
...val,
|
||||
updateStatus: {kind: 'up-to-date'},
|
||||
};
|
||||
}
|
||||
|
||||
function InstallButton(props: {
|
||||
name: string;
|
||||
version: string;
|
||||
onInstall: () => void;
|
||||
installed: boolean;
|
||||
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 fs.remove(path.join(PLUGIN_DIR, props.name));
|
||||
props.onInstall();
|
||||
setAction({kind: 'Install'});
|
||||
}),
|
||||
[props.name],
|
||||
);
|
||||
|
||||
const [action, setAction] = useState<InstallAction>(
|
||||
props.updateStatus.kind === 'update-available'
|
||||
? {kind: 'Update'}
|
||||
: props.installed
|
||||
? {kind: 'Remove'}
|
||||
: {kind: 'Install'},
|
||||
);
|
||||
|
||||
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,
|
||||
setQuery: (query: string) => void,
|
||||
searchClientFactory: () => SearchIndex,
|
||||
installedPlugins: Map<string, PluginDefinition>,
|
||||
onInstall: () => Promise<void>,
|
||||
findPluginUpdates: (
|
||||
currentPlugins: PluginMap,
|
||||
) => Promise<[string, UpdateResult][]>,
|
||||
): TableRows_immutable {
|
||||
const index = useMemo(searchClientFactory, []);
|
||||
|
||||
useEffect(() => {
|
||||
reportUsage(`${TAG}:open`);
|
||||
}, []);
|
||||
|
||||
const createRow = useCallback(
|
||||
(h: UpdatablePluginDefinition) => ({
|
||||
key: h.name,
|
||||
columns: {
|
||||
name: {value: <EllipsisText>{h.name}</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}
|
||||
installed={installedPlugins.has(h.name)}
|
||||
updateStatus={h.updateStatus}
|
||||
/>
|
||||
),
|
||||
align: 'center' as 'center',
|
||||
},
|
||||
},
|
||||
}),
|
||||
[installedPlugins],
|
||||
);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
UpdatablePluginDefinition[]
|
||||
>([]);
|
||||
const [
|
||||
updateAnnotatedInstalledPlugins,
|
||||
setUpdateAnnotatedInstalledPlugins,
|
||||
] = useState<Map<string, UpdatablePluginDefinition>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const {hits} = await reportPlatformFailures(
|
||||
index.search<PluginDefinition>('', {
|
||||
query,
|
||||
filters: 'keywords:flipper-plugin',
|
||||
hitsPerPage: 20,
|
||||
}) as Promise<SearchResponse<PluginDefinition>>,
|
||||
`${TAG}:queryIndex`,
|
||||
);
|
||||
|
||||
setSearchResults(
|
||||
hits.filter(hit => !installedPlugins.has(hit.name)).map(liftUpdatable),
|
||||
);
|
||||
setQuery(query);
|
||||
})();
|
||||
}, [query, installedPlugins]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const updates = new Map(await findPluginUpdates(installedPlugins));
|
||||
setUpdateAnnotatedInstalledPlugins(
|
||||
annotatePluginsWithUpdates(installedPlugins, updates),
|
||||
);
|
||||
})();
|
||||
}, [installedPlugins]);
|
||||
|
||||
const results = Array.from(updateAnnotatedInstalledPlugins.values()).concat(
|
||||
searchResults,
|
||||
);
|
||||
return List(results.map(createRow));
|
||||
}
|
||||
|
||||
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
||||
({pluginManager: {installedPlugins}}) => ({
|
||||
installedPlugins,
|
||||
}),
|
||||
(dispatch: Dispatch<Action<any>>) => ({
|
||||
refreshInstalledPlugins: () => {
|
||||
readInstalledPlugins().then(plugins =>
|
||||
dispatch(registerInstalledPlugins(plugins)),
|
||||
);
|
||||
},
|
||||
}),
|
||||
)(PluginInstaller);
|
||||
Reference in New Issue
Block a user