Add settings UI

Summary:
Adds a simple UI for editing settings, a reducer and persistance config for the data.
These values aren't yet used for anything.

Reviewed By: passy

Differential Revision: D17684490

fbshipit-source-id: e76ac43ffa17d3606e59f4a1ccb940e8d9fbd9e8
This commit is contained in:
John Knox
2019-10-07 08:49:05 -07:00
committed by Facebook Github Bot
parent 8c15547597
commit eb64ff0832
8 changed files with 249 additions and 0 deletions

View File

@@ -26,6 +26,7 @@ import {
ACTIVE_SHEET_PLUGINS, ACTIVE_SHEET_PLUGINS,
ACTIVE_SHEET_SHARE_DATA, ACTIVE_SHEET_SHARE_DATA,
ACTIVE_SHEET_SIGN_IN, ACTIVE_SHEET_SIGN_IN,
ACTIVE_SHEET_SETTINGS,
ACTIVE_SHEET_SHARE_DATA_IN_FILE, ACTIVE_SHEET_SHARE_DATA_IN_FILE,
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT, ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
ACTIVE_SHEET_PLUGIN_SHEET, ACTIVE_SHEET_PLUGIN_SHEET,
@@ -37,6 +38,7 @@ import {StaticView} from './reducers/connections';
import PluginManager from './chrome/PluginManager'; import PluginManager from './chrome/PluginManager';
import BaseDevice from './devices/BaseDevice'; import BaseDevice from './devices/BaseDevice';
import StatusBar from './chrome/StatusBar'; import StatusBar from './chrome/StatusBar';
import SettingsSheet from './chrome/SettingsSheet';
const version = remote.app.getVersion(); const version = remote.app.getVersion();
type OwnProps = { type OwnProps = {
@@ -88,6 +90,8 @@ export class App extends React.Component<Props> {
return <ShareSheet onHide={onHide} logger={this.props.logger} />; return <ShareSheet onHide={onHide} logger={this.props.logger} />;
case ACTIVE_SHEET_SIGN_IN: case ACTIVE_SHEET_SIGN_IN:
return <SignInSheet onHide={onHide} />; return <SignInSheet onHide={onHide} />;
case ACTIVE_SHEET_SETTINGS:
return <SettingsSheet onHide={onHide} />;
case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT: case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT:
return <ExportDataPluginSheet onHide={onHide} />; return <ExportDataPluginSheet onHide={onHide} />;
case ACTIVE_SHEET_SHARE_DATA_IN_FILE: case ACTIVE_SHEET_SHARE_DATA_IN_FILE:

View File

@@ -11,6 +11,7 @@ import {
setSelectPluginsToExportActiveSheet, setSelectPluginsToExportActiveSheet,
setActiveSheet, setActiveSheet,
ACTIVE_SHEET_PLUGINS, ACTIVE_SHEET_PLUGINS,
ACTIVE_SHEET_SETTINGS,
} from './reducers/application'; } from './reducers/application';
import {Store} from './reducers/'; import {Store} from './reducers/';
import electron, {MenuItemConstructorOptions} from 'electron'; import electron, {MenuItemConstructorOptions} from 'electron';
@@ -214,6 +215,11 @@ function getTemplate(
{ {
label: 'File', label: 'File',
submenu: [ submenu: [
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => store.dispatch(setActiveSheet(ACTIVE_SHEET_SETTINGS)),
},
{ {
label: 'Import Flipper File...', label: 'Import Flipper File...',
accelerator: 'CommandOrControl+O', accelerator: 'CommandOrControl+O',

View File

@@ -0,0 +1,168 @@
/**
* 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,
Button,
styled,
Text,
FlexRow,
Spacer,
Input,
colors,
Glyph,
} from 'flipper';
import React, {Component, useState} from 'react';
import {updateSettings, Action} from '../reducers/settings';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
import {Settings} from '../reducers/settings';
import {promises as fs} from 'fs';
import {remote} from 'electron';
import path from 'path';
const Container = styled(FlexColumn)({
padding: 20,
width: 800,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
const InfoText = styled(Text)({
lineHeight: 1.35,
paddingTop: 5,
});
const FileInputBox = styled(Input)(({isValid}: {isValid: boolean}) => ({
marginRight: 0,
flexGrow: 1,
fontFamily: 'monospace',
color: isValid ? undefined : colors.red,
marginLeft: 10,
marginTop: 'auto',
marginBottom: 'auto',
}));
const CenteredGlyph = styled(Glyph)({
margin: 'auto',
marginLeft: 10,
});
type OwnProps = {
onHide: () => void;
};
type StateFromProps = {
settings: Settings;
};
type DispatchFromProps = {
updateSettings: (settings: Settings) => Action;
};
type State = {
updatedSettings: Settings;
};
function FilePathConfigField(props: {
label: string;
defaultValue: string;
onChange: (path: string) => void;
}) {
const [value, setValue] = useState(props.defaultValue);
const [isValid, setIsValid] = useState(true);
fs.stat(value)
.then(stat => setIsValid(stat.isDirectory()))
.catch(_ => setIsValid(false));
return (
<FlexRow>
<InfoText>{props.label}</InfoText>
<FileInputBox
placeholder={props.label}
value={value}
isValid={isValid}
onChange={e => {
setValue(e.target.value);
props.onChange(e.target.value);
fs.stat(e.target.value)
.then(stat => setIsValid(stat.isDirectory()))
.catch(_ => setIsValid(false));
}}
/>
<FlexColumn
onClick={() =>
remote.dialog.showOpenDialog(
{
properties: ['openDirectory', 'showHiddenFiles'],
defaultPath: path.resolve('/'),
},
(paths: Array<string> | undefined) => {
paths && setValue(paths[0]);
paths && props.onChange(paths[0]);
},
)
}>
<CenteredGlyph name="dots-3-circle" variant="outline" />
</FlexColumn>
{isValid ? null : (
<CenteredGlyph name="caution-triangle" color={colors.yellow} />
)}
</FlexRow>
);
}
type Props = OwnProps & StateFromProps & DispatchFromProps;
class SignInSheet extends Component<Props, State> {
state = {
updatedSettings: {...this.props.settings},
};
applyChanges = async () => {
this.props.updateSettings(this.state.updatedSettings);
this.props.onHide();
};
render() {
return (
<Container>
<Title>Settings</Title>
<FilePathConfigField
label="Android SDK Location"
defaultValue={this.state.updatedSettings.androidHome}
onChange={v => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
androidHome: v,
},
});
}}
/>
<br />
<FlexRow>
<Spacer />
<Button compact padded onClick={this.props.onHide}>
Cancel
</Button>
<Button type="primary" compact padded onClick={this.applyChanges}>
Apply
</Button>
</FlexRow>
</Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({settingsState}) => ({settings: settingsState.settings}),
{updateSettings},
)(SignInSheet);

View File

@@ -14,6 +14,7 @@ import {
toggleRightSidebarVisible, toggleRightSidebarVisible,
ACTIVE_SHEET_BUG_REPORTER, ACTIVE_SHEET_BUG_REPORTER,
setFlipperRating, setFlipperRating,
ACTIVE_SHEET_SETTINGS,
} from '../reducers/application'; } from '../reducers/application';
import { import {
colors, colors,
@@ -181,6 +182,12 @@ class TitleBar extends React.Component<Props, StateFromProps> {
icon="bug" icon="bug"
/> />
)} )}
<Button
icon="settings"
title="Settings"
compact={true}
onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_SETTINGS)}
/>
<ButtonGroup> <ButtonGroup>
<Button <Button
compact={true} compact={true}

View File

@@ -17,6 +17,7 @@ export const ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT: 'SELECT_PLUGINS_TO_EXPORT' =
'SELECT_PLUGINS_TO_EXPORT'; 'SELECT_PLUGINS_TO_EXPORT';
export const ACTIVE_SHEET_SHARE_DATA: 'SHARE_DATA' = 'SHARE_DATA'; export const ACTIVE_SHEET_SHARE_DATA: 'SHARE_DATA' = 'SHARE_DATA';
export const ACTIVE_SHEET_SIGN_IN: 'SIGN_IN' = 'SIGN_IN'; export const ACTIVE_SHEET_SIGN_IN: 'SIGN_IN' = 'SIGN_IN';
export const ACTIVE_SHEET_SETTINGS: 'SETTINGS' = 'SETTINGS';
export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' = export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' =
'SHARE_DATA_IN_FILE'; 'SHARE_DATA_IN_FILE';
export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' = export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' =
@@ -29,6 +30,7 @@ export type ActiveSheet =
| typeof ACTIVE_SHEET_PLUGINS | typeof ACTIVE_SHEET_PLUGINS
| typeof ACTIVE_SHEET_SHARE_DATA | typeof ACTIVE_SHEET_SHARE_DATA
| typeof ACTIVE_SHEET_SIGN_IN | typeof ACTIVE_SHEET_SIGN_IN
| typeof ACTIVE_SHEET_SETTINGS
| typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE | typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT | typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
| null; | null;

View File

@@ -26,6 +26,10 @@ import plugins, {
State as PluginsState, State as PluginsState,
Action as PluginsAction, Action as PluginsAction,
} from './plugins'; } from './plugins';
import settings, {
State as SettingsState,
Action as SettingsAction,
} from './settings';
import user, {State as UserState, Action as UserAction} from './user'; import user, {State as UserState, Action as UserAction} from './user';
import {persistReducer, PersistPartial} from 'redux-persist'; import {persistReducer, PersistPartial} from 'redux-persist';
@@ -41,6 +45,7 @@ export type Actions =
| NotificationsAction | NotificationsAction
| PluginsAction | PluginsAction
| UserAction | UserAction
| SettingsAction
| {type: 'INIT'}; | {type: 'INIT'};
export type State = { export type State = {
@@ -50,6 +55,7 @@ export type State = {
notifications: NotificationsState & PersistPartial; notifications: NotificationsState & PersistPartial;
plugins: PluginsState; plugins: PluginsState;
user: UserState & PersistPartial; user: UserState & PersistPartial;
settingsState: SettingsState & PersistPartial;
}; };
export type Store = ReduxStore<State, Actions>; export type Store = ReduxStore<State, Actions>;
@@ -94,4 +100,8 @@ export default combineReducers<State, Actions>({
}, },
user, user,
), ),
settingsState: persistReducer(
{key: 'settings', storage, whitelist: ['settings']},
settings,
),
}); });

49
src/reducers/settings.tsx Normal file
View File

@@ -0,0 +1,49 @@
/**
* 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 {Actions} from './index';
export type Settings = {
androidHome: string;
};
export type State = {
settings: Settings;
};
export type Action =
| {type: 'INIT'}
| {
type: 'UPDATE_SETTINGS';
payload: Settings;
};
const initialState: State = {
settings: {
androidHome: '/opt/android_sdk',
},
};
export default function reducer(
state: State = initialState,
action: Actions,
): State {
if (action.type === 'UPDATE_SETTINGS') {
return {
...state,
settings: action.payload,
};
}
return state;
}
export function updateSettings(settings: Settings): Action {
return {
type: 'UPDATE_SETTINGS',
payload: settings,
};
}

View File

@@ -46,6 +46,9 @@ const ICONS = {
profile: [12], profile: [12],
target: [12], target: [12],
bird: [12], bird: [12],
settings: [12],
directions: [12],
'dots-3-circle-outline': [16],
}; };
// Takes a string like 'star', or 'star-outline', and converts it to // Takes a string like 'star', or 'star-outline', and converts it to