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:
Anton Nikolaev
2020-03-14 14:26:07 -07:00
committed by Facebook GitHub Bot
parent a60e6fee87
commit 85c13bb1f3
607 changed files with 103 additions and 142 deletions

View 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);