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:
committed by
Facebook Github Bot
parent
15ed856388
commit
984cdbfb67
@@ -25,6 +25,7 @@ import {
|
|||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
|
import {default as FileSelector} from '../../ui/components/FileSelector';
|
||||||
import React, {useCallback, useState, useMemo, useEffect} from 'react';
|
import React, {useCallback, useState, useMemo, useEffect} from 'react';
|
||||||
import {List} from 'immutable';
|
import {List} from 'immutable';
|
||||||
import {SearchIndex} from 'algoliasearch';
|
import {SearchIndex} from 'algoliasearch';
|
||||||
@@ -144,6 +145,8 @@ export function annotatePluginsWithUpdates(
|
|||||||
const PluginInstaller = function props(props: Props) {
|
const PluginInstaller = function props(props: Props) {
|
||||||
const [restartRequired, setRestartRequired] = useState(false);
|
const [restartRequired, setRestartRequired] = useState(false);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
const [_localPackagePath, setLocalPackagePath] = useState('');
|
||||||
|
const [isLocalPackagePathValid, setIsLocalPackagePathValud] = useState(false);
|
||||||
const rows = useNPMSearch(
|
const rows = useNPMSearch(
|
||||||
setRestartRequired,
|
setRestartRequired,
|
||||||
query,
|
query,
|
||||||
@@ -158,6 +161,7 @@ const PluginInstaller = function props(props: Props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Container>
|
<Container>
|
||||||
{restartRequired && (
|
{restartRequired && (
|
||||||
<RestartBar onClick={restartApp}>
|
<RestartBar onClick={restartApp}>
|
||||||
@@ -186,6 +190,27 @@ const PluginInstaller = function props(props: Props) {
|
|||||||
rows={rows}
|
rows={rows}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
<Toolbar>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
PluginInstaller.defaultProps = defaultProps;
|
PluginInstaller.defaultProps = defaultProps;
|
||||||
|
|||||||
127
src/ui/components/FileSelector.tsx
Normal file
127
src/ui/components/FileSelector.tsx
Normal 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;
|
||||||
@@ -48,12 +48,20 @@ function ColoredIcon(
|
|||||||
className?: string;
|
className?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
title?: string;
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
glyphColor?: string;
|
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 =
|
const isBlack =
|
||||||
color == null ||
|
color == null ||
|
||||||
@@ -69,6 +77,7 @@ function ColoredIcon(
|
|||||||
size={size}
|
size={size}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
style={style}
|
style={style}
|
||||||
|
title={title}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -79,6 +88,7 @@ function ColoredIcon(
|
|||||||
src={src}
|
src={src}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
style={style}
|
style={style}
|
||||||
|
title={title}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,7 +108,15 @@ export default class Glyph extends React.PureComponent<{
|
|||||||
title?: string;
|
title?: string;
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {name, size = 16, variant, color, className, style} = this.props;
|
const {
|
||||||
|
name,
|
||||||
|
size = 16,
|
||||||
|
variant,
|
||||||
|
color,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
title,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColoredIcon
|
<ColoredIcon
|
||||||
@@ -106,6 +124,7 @@ export default class Glyph extends React.PureComponent<{
|
|||||||
className={className}
|
className={className}
|
||||||
color={color}
|
color={color}
|
||||||
size={size}
|
size={size}
|
||||||
|
title={title}
|
||||||
src={getIconURL(
|
src={getIconURL(
|
||||||
variant === 'outline' ? `${name}-outline` : name,
|
variant === 'outline' ? `${name}-outline` : name,
|
||||||
size,
|
size,
|
||||||
|
|||||||
Reference in New Issue
Block a user