Install plugin from package file

Summary:
Adding a way to install plugins directly from package files. This is required for testing after packaging format changes.

Stage 2: Added new component for file selection and implemented UI required for plugin installation from package file

Reviewed By: priteshrnandgaonkar

Differential Revision: D19743998

fbshipit-source-id: 1112d5afca9a649df11e33eb6ac15c0e06747d47
This commit is contained in:
Anton Nikolaev
2020-02-06 08:00:01 -08:00
committed by Facebook Github Bot
parent 15ed856388
commit 984cdbfb67
3 changed files with 199 additions and 28 deletions

View File

@@ -25,6 +25,7 @@ import {
LoadingIndicator,
Tooltip,
} from 'flipper';
import {default as FileSelector} from '../../ui/components/FileSelector';
import React, {useCallback, useState, useMemo, useEffect} from 'react';
import {List} from 'immutable';
import {SearchIndex} from 'algoliasearch';
@@ -144,6 +145,8 @@ export function annotatePluginsWithUpdates(
const PluginInstaller = function props(props: Props) {
const [restartRequired, setRestartRequired] = useState(false);
const [query, setQuery] = useState('');
const [_localPackagePath, setLocalPackagePath] = useState('');
const [isLocalPackagePathValid, setIsLocalPackagePathValud] = useState(false);
const rows = useNPMSearch(
setRestartRequired,
query,
@@ -158,34 +161,56 @@ const PluginInstaller = function props(props: Props) {
}, []);
return (
<Container>
{restartRequired && (
<RestartBar onClick={restartApp}>
To activate this plugin, Flipper needs to restart. Click here to
restart!
</RestartBar>
)}
<>
<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>
<Toolbar>
<SearchBox>
<SearchInput
onChange={e => setQuery(e.target.value)}
value={query}
placeholder="Search Flipper plugins..."
/>
</SearchBox>
<FileSelector
placeholderText="Specify path to a Flipper package or just drag and drop it here..."
onPathChanged={e => {
setLocalPackagePath(e.path);
setIsLocalPackagePathValud(e.isValid);
}}
/>
<Button
compact
type="primary"
disabled={!isLocalPackagePathValid}
title={
isLocalPackagePathValid
? 'Click to install the specified plugin package'
: 'Cannot install plugin package by the specified path'
}>
Install
</Button>
</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;

View File

@@ -0,0 +1,127 @@
/**
* 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 React, {useState} from 'react';
import FlexRow from './FlexRow';
import FlexColumn from './FlexColumn';
import Glyph from './Glyph';
import Input from './Input';
import electron from 'electron';
import styled from '@emotion/styled';
import {colors} from './colors';
import Electron from 'electron';
import fs from 'fs';
const CenteredGlyph = styled(Glyph)({
flexGrow: 0,
margin: 'auto',
marginLeft: 10,
});
const Container = styled(FlexRow)({
width: '100%',
});
const FileInputBox = styled(Input)<{isValid: boolean}>(({isValid}) => ({
flexGrow: 1,
color: isValid ? undefined : colors.red,
'&::-webkit-input-placeholder': {
color: colors.placeholder,
fontWeight: 300,
},
}));
function strToArr<T extends string>(item: T): T[] {
return [item];
}
export interface Props {
onPathChanged: (evtArgs: {path: string; isValid: boolean}) => void;
placeholderText: string;
defaultPath: string;
showHiddenFiles: boolean;
}
const defaultProps: Props = {
onPathChanged: () => {},
placeholderText: '',
defaultPath: '/',
showHiddenFiles: false,
};
export default function FileSelector({
onPathChanged,
placeholderText,
defaultPath,
showHiddenFiles,
}: Props) {
const [value, setValue] = useState('');
const [isValid, setIsValid] = useState(false);
const options: Electron.OpenDialogOptions = {
properties: [
'openFile',
...(showHiddenFiles ? strToArr('showHiddenFiles') : []),
],
defaultPath,
};
const onChange = (path: string) => {
setValue(path);
let isNewPathValid = false;
try {
isNewPathValid = fs.statSync(path).isFile();
} catch {
isNewPathValid = false;
}
setIsValid(isNewPathValid);
onPathChanged({path, isValid: isNewPathValid});
};
return (
<Container>
<FileInputBox
placeholder={placeholderText}
value={value}
isValid={true}
onDrop={e => {
if (e.dataTransfer.files.length) {
onChange(e.dataTransfer.files[0].path);
}
}}
onChange={e => {
onChange(e.target.value);
}}
/>
<FlexColumn
onClick={() =>
electron.remote.dialog
.showOpenDialog(options)
.then((result: electron.OpenDialogReturnValue) => {
if (result && !result.canceled && result.filePaths.length) {
onChange(result.filePaths[0]);
}
})
}>
<CenteredGlyph
name="dots-3-circle"
variant="outline"
title="Open file selection dialog"
/>
</FlexColumn>
{isValid ? null : (
<CenteredGlyph
name="caution-triangle"
color={colors.yellow}
title="Path is invalid"
/>
)}
</Container>
);
}
FileSelector.defaultProps = defaultProps;

View File

@@ -48,12 +48,20 @@ function ColoredIcon(
className?: string;
color?: string;
style?: React.CSSProperties;
title?: string;
},
context: {
glyphColor?: string;
},
) {
const {color = context.glyphColor, name, size = 16, src, style} = props;
const {
color = context.glyphColor,
name,
size = 16,
src,
style,
title,
} = props;
const isBlack =
color == null ||
@@ -69,6 +77,7 @@ function ColoredIcon(
size={size}
className={props.className}
style={style}
title={title}
/>
);
} else {
@@ -79,6 +88,7 @@ function ColoredIcon(
src={src}
className={props.className}
style={style}
title={title}
/>
);
}
@@ -98,7 +108,15 @@ export default class Glyph extends React.PureComponent<{
title?: string;
}> {
render() {
const {name, size = 16, variant, color, className, style} = this.props;
const {
name,
size = 16,
variant,
color,
className,
style,
title,
} = this.props;
return (
<ColoredIcon
@@ -106,6 +124,7 @@ export default class Glyph extends React.PureComponent<{
className={className}
color={color}
size={size}
title={title}
src={getIconURL(
variant === 'outline' ? `${name}-outline` : name,
size,