Summary: Noticed a bunch of failures [here](https://our.intern.facebook.com/intern/scuba/query/?dataset=flipper_session_event_stats_scuba&drillstate=%7B%22sampleCols%22%3A[%22errors%22%2C%22event%22]%2C%22cols%22%3A[]%2C%22derivedCols%22%3A[%7B%22name%22%3A%22perfect_session%22%2C%22sql%22%3A%22CAST_AS_DOUBLE(no_failures)%22%2C%22type%22%3A%22Numeric%22%7D]%2C%22mappedCols%22%3A[]%2C%22enumCols%22%3A[]%2C%22return_remainder%22%3Afalse%2C%22should_pivot%22%3Afalse%2C%22is_timeseries%22%3Atrue%2C%22hideEmptyColumns%22%3Afalse%2C%22start%22%3A%22-90%20days%22%2C%22samplingRatio%22%3A1%2C%22compare%22%3A%22comparison%22%2C%22axes%22%3A%22linked%22%2C%22bucket%22%3A%221%22%2C%22overlay_types%22%3A[]%2C%22minBucketSamples%22%3A%22%22%2C%22dimensions%22%3A[%22time%22%2C%22event%22]%2C%22scale_type%22%3A%22absolute%22%2C%22num_samples%22%3A%22100%22%2C%22metric%22%3A%22avg%22%2C%22fill_missing_buckets%22%3A%22connect%22%2C%22smoothing_bucket%22%3A%221%22%2C%22top%22%3A20%2C%22markers%22%3A%22%22%2C%22timezone%22%3A%22America%2FLos_Angeles%22%2C%22end%22%3A%22now%22%2C%22time_bucket%22%3A%22604800%22%2C%22compare_mode%22%3A%22normal%22%2C%22aggregateList%22%3A[]%2C%22param_dimensions%22%3A[]%2C%22modifiers%22%3A[]%2C%22order%22%3A%22none%22%2C%22order_desc%22%3Atrue%2C%22filterMode%22%3A%22DEFAULT%22%2C%22constraints%22%3A[[%7B%22column%22%3A%22plugin%22%2C%22op%22%3A%22eq%22%2C%22value%22%3A[%22[%5C%22null%5C%22]%22]%7D%2C%7B%22column%22%3A%22is_headless%22%2C%22op%22%3A%22!substr%22%2C%22value%22%3A[%22[%5C%22true%5C%22]%22]%7D%2C%7B%22column%22%3A%22event%22%2C%22op%22%3A%22substr%22%2C%22value%22%3A[%22[%5C%22getInstalledPlugins%5C%22]%22]%7D]]%2C%22c_constraints%22%3A[[]]%2C%22b_constraints%22%3A[[]]%2C%22metrik_view_params%22%3A%7B%22xaxis_type%22%3A%22auto%22%2C%22should_use_legacy_colors%22%3Afalse%2C%22view%22%3A%22Samples%22%2C%22width%22%3A%222126%22%2C%22height%22%3A%221132%22%2C%22y_max_hint%22%3A%221%22%2C%22yaxis_settings%22%3A[%7B%22yaxis_title%22%3A%22%22%2C%22yaxis_series_name%22%3A%22%22%2C%22yaxis_width%22%3A%220%22%2C%22yaxis_format%22%3A%22%25P%22%7D]%2C%22tooltip_outside%22%3Atrue%2C%22state%22%3A%22published%22%2C%22use_y_axis_hints_as_limits%22%3Atrue%2C%22legend_mode%22%3A%22nongrid%22%2C%22connect_nulls%22%3Atrue%2C%22yaxismin%22%3A0%2C%22title%22%3A%22Operation%20success%20rate%20over%20time%20(human)%22%2C%22tooltip_disabled%22%3Atrue%2C%22timezone_offset%22%3A420%2C%22y_min_hint%22%3A0%2C%22legend_position%22%3A%22none%22%2C%22title_use_v2%22%3Atrue%7D%7D&pool=uber&view=Samples&dashboard_id&tab_id&widget_id&widget_piece_id). This fixes it. Reviewed By: passy Differential Revision: D17877108 fbshipit-source-id: 85586a1ce65033b98d793746c611b44f68e13eff
387 lines
9.6 KiB
TypeScript
387 lines
9.6 KiB
TypeScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* 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 {remote} from 'electron';
|
|
import {List} from 'immutable';
|
|
import algoliasearch from 'algoliasearch';
|
|
import path from 'path';
|
|
import fs from 'fs-extra';
|
|
import {promisify} from 'util';
|
|
import {homedir} from 'os';
|
|
import {PluginManager as PM} from 'live-plugin-manager';
|
|
import {reportPlatformFailures, reportUsage} from '../utils/metrics';
|
|
|
|
const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty');
|
|
const ALGOLIA_APPLICATION_ID = 'OFCNCOG2CU';
|
|
const ALGOLIA_API_KEY = 'f54e21fa3a2a0160595bb058179bfb1e';
|
|
const TAG = 'PluginInstaller';
|
|
const PluginManager = new PM({
|
|
ignoredDependencies: ['flipper', 'react', 'react-dom', '@types/*'],
|
|
});
|
|
|
|
export type PluginDefinition = {
|
|
name: string;
|
|
version: string;
|
|
description: string;
|
|
};
|
|
|
|
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 Props = {
|
|
searchIndexFactory: () => algoliasearch.Index;
|
|
getInstalledPlugins: () => Promise<Map<string, PluginDefinition>>;
|
|
autoHeight: boolean;
|
|
};
|
|
|
|
const defaultProps: Props = {
|
|
searchIndexFactory: () => {
|
|
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
|
|
return client.initIndex('npm-search');
|
|
},
|
|
getInstalledPlugins: _getInstalledPlugins,
|
|
autoHeight: false,
|
|
};
|
|
|
|
const PluginInstaller = function props(props: Props) {
|
|
const [restartRequired, setRestartRequired] = useState(false);
|
|
const [query, setQuery] = useState('');
|
|
const rows = useNPMSearch(
|
|
setRestartRequired,
|
|
query,
|
|
setQuery,
|
|
props.searchIndexFactory,
|
|
props.getInstalledPlugins,
|
|
);
|
|
const restartApp = useCallback(() => {
|
|
remote.app.relaunch();
|
|
remote.app.exit();
|
|
}, []);
|
|
|
|
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>
|
|
);
|
|
};
|
|
PluginInstaller.defaultProps = defaultProps;
|
|
export default PluginInstaller;
|
|
|
|
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;
|
|
installed: boolean;
|
|
}) {
|
|
type InstallAction =
|
|
| {kind: 'Install'}
|
|
| {kind: 'Waiting'}
|
|
| {kind: 'Remove'}
|
|
| {kind: 'Error'; error: string};
|
|
|
|
const catchError = (fn: () => Promise<void>) => async () => {
|
|
try {
|
|
await fn();
|
|
} catch (err) {
|
|
setAction({kind: 'Error', error: err.toString()});
|
|
}
|
|
};
|
|
|
|
const onInstall = useCallback(
|
|
catchError(async () => {
|
|
reportUsage(`${TAG}:install`, undefined, props.name);
|
|
setAction({kind: 'Waiting'});
|
|
await fs.ensureDir(PLUGIN_DIR);
|
|
// create empty watchman config (required by metro's file watcher)
|
|
await fs.writeFile(path.join(PLUGIN_DIR, '.watchmanconfig'), '{}');
|
|
|
|
// install the plugin and all it's dependencies into node_modules
|
|
PluginManager.options.pluginsPath = path.join(
|
|
PLUGIN_DIR,
|
|
props.name,
|
|
'node_modules',
|
|
);
|
|
await PluginManager.install(props.name);
|
|
|
|
// move the plugin itself out of the node_modules folder
|
|
const pluginDir = path.join(
|
|
PLUGIN_DIR,
|
|
props.name,
|
|
'node_modules',
|
|
props.name,
|
|
);
|
|
const pluginFiles = await fs.readdir(pluginDir);
|
|
await Promise.all(
|
|
pluginFiles.map(f =>
|
|
fs.move(path.join(pluginDir, f), path.join(pluginDir, '..', '..', f)),
|
|
),
|
|
);
|
|
|
|
props.onInstall();
|
|
setAction({kind: 'Remove'});
|
|
}),
|
|
[props.name, props.version],
|
|
);
|
|
|
|
const onRemove = useCallback(
|
|
catchError(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.installed ? {kind: 'Remove'} : {kind: 'Install'},
|
|
);
|
|
|
|
if (action.kind === 'Waiting') {
|
|
return <Spinner size={16} />;
|
|
}
|
|
if (action.kind === 'Error') {
|
|
const glyph = (
|
|
<AlignedGlyph color={colors.orange} size={16} name="caution-triangle" />
|
|
);
|
|
return (
|
|
<Tooltip
|
|
options={{position: 'toRight'}}
|
|
title={`Something went wrong: ${action.error}`}
|
|
children={glyph}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<TableButton
|
|
compact
|
|
type={action.kind === 'Install' ? 'primary' : undefined}
|
|
onClick={
|
|
action.kind === 'Install'
|
|
? () => reportPlatformFailures(onInstall(), `${TAG}:install`)
|
|
: () => reportPlatformFailures(onRemove(), `${TAG}:remove`)
|
|
}>
|
|
{action.kind}
|
|
</TableButton>
|
|
);
|
|
}
|
|
|
|
function useNPMSearch(
|
|
setRestartRequired: (restart: boolean) => void,
|
|
query: string,
|
|
setQuery: (query: string) => void,
|
|
searchClientFactory: () => algoliasearch.Index,
|
|
getInstalledPlugins: () => Promise<Map<string, PluginDefinition>>,
|
|
): TableRows_immutable {
|
|
const index = useMemo(searchClientFactory, []);
|
|
const [installedPlugins, setInstalledPlugins] = useState(
|
|
new Map<string, PluginDefinition>(),
|
|
);
|
|
|
|
const getAndSetInstalledPlugins = () =>
|
|
reportPlatformFailures(
|
|
getInstalledPlugins(),
|
|
`${TAG}:getInstalledPlugins`,
|
|
).then(setInstalledPlugins);
|
|
|
|
useEffect(() => {
|
|
reportUsage(`${TAG}:open`);
|
|
getAndSetInstalledPlugins();
|
|
}, []);
|
|
|
|
const onInstall = useCallback(async () => {
|
|
getAndSetInstalledPlugins();
|
|
setRestartRequired(true);
|
|
}, []);
|
|
|
|
const createRow = useCallback(
|
|
(h: PluginDefinition) => ({
|
|
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)}
|
|
/>
|
|
),
|
|
align: 'center' as 'center',
|
|
},
|
|
},
|
|
}),
|
|
[installedPlugins],
|
|
);
|
|
|
|
const [searchResults, setSearchResults] = useState<PluginDefinition[]>([]);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
const {hits} = await reportPlatformFailures(
|
|
index.search({
|
|
query,
|
|
filters: 'keywords:flipper-plugin',
|
|
hitsPerPage: 20,
|
|
}),
|
|
`${TAG}:queryIndex`,
|
|
);
|
|
|
|
setSearchResults(hits.filter(hit => !installedPlugins.has(hit.name)));
|
|
setQuery(query);
|
|
})();
|
|
}, [query, installedPlugins]);
|
|
|
|
const results = Array.from(installedPlugins.values()).concat(searchResults);
|
|
return List(results.map(createRow));
|
|
}
|
|
|
|
async function _getInstalledPlugins(): Promise<Map<string, PluginDefinition>> {
|
|
const pluginDirExists = await promisify(fs.exists)(PLUGIN_DIR);
|
|
|
|
if (!pluginDirExists) {
|
|
return new Map();
|
|
}
|
|
const dirs = await fs.readdir(PLUGIN_DIR);
|
|
const plugins = await Promise.all<[string, PluginDefinition]>(
|
|
dirs.map(
|
|
name =>
|
|
new Promise(async (resolve, reject) => {
|
|
if (!(await fs.lstat(path.join(PLUGIN_DIR, name))).isDirectory()) {
|
|
return resolve(undefined);
|
|
}
|
|
|
|
const packageJSON = await fs.readFile(
|
|
path.join(PLUGIN_DIR, name, 'package.json'),
|
|
);
|
|
|
|
try {
|
|
resolve([name, JSON.parse(packageJSON.toString())]);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}),
|
|
),
|
|
);
|
|
return new Map(plugins.filter(Boolean));
|
|
}
|