diff --git a/api/src/opentrons/server/endpoints/networking.py b/api/src/opentrons/server/endpoints/networking.py index 6e61590abc9..ba6e8c7fdd4 100644 --- a/api/src/opentrons/server/endpoints/networking.py +++ b/api/src/opentrons/server/endpoints/networking.py @@ -44,7 +44,7 @@ async def list_networks(request: web.Request) -> web.Response: signal: int // e.g. 100; arbitrary signal strength, more is better active: boolean // e.g. true; whether there is a connection active security: str // e.g. "WPA2 802.1X" raw nmcli security type output - securityType: str // e.g. "wpa-eap"; see blow + securityType: str // e.g. "wpa-eap"; see below } ] } diff --git a/app/src/components/RobotSettings/SelectNetwork/ConnectForm.js b/app/src/components/RobotSettings/SelectNetwork/ConnectForm.js index 6335403f2f8..b789e39884d 100644 --- a/app/src/components/RobotSettings/SelectNetwork/ConnectForm.js +++ b/app/src/components/RobotSettings/SelectNetwork/ConnectForm.js @@ -3,15 +3,21 @@ import * as React from 'react' import {Formik} from 'formik' import get from 'lodash/get' import find from 'lodash/find' -import map from 'lodash/map' import set from 'lodash/set' -import {WPA_PSK_SECURITY, WPA_EAP_SECURITY} from '../../../http-api-client' +import { + NO_SECURITY, + WPA_PSK_SECURITY, + WPA_EAP_SECURITY, + SECURITY_TYPE_FIELD, + EAP_CONFIG_FIELD, + EAP_TYPE_FIELD, +} from '../../../http-api-client' -import {DropdownField} from '@opentrons/components' import {BottomButtonBar} from '../../modals' import ConnectFormField, {CONNECT_FIELD_ID_PREFIX} from './ConnectFormField' -import FormTable, {FormTableRow} from './FormTable' +import SelectSecurity from './SelectSecurity' +import FormTable from './FormTable' import type { WifiSecurityType, @@ -24,7 +30,7 @@ import type { type Props = { // TODO(mc, 2018-10-22): optional SSID ssid: string, - securityType: WifiSecurityType, + securityType: ?WifiSecurityType, eapOptions: ?WifiEapOptionsList, keys: ?WifiKeysList, configure: WifiConfigureRequest => mixed, @@ -36,7 +42,7 @@ type State = {| showPassword: {[name: string]: boolean}, |} -type FormValues = {[string]: ?(string | {[string]: string})} +export type FormValues = {[string]: ?(string | {[string]: string})} const PSK_FIELD_NAME = 'psk' const PSK_MIN_LENGTH = 8 @@ -52,11 +58,9 @@ const WPA_PSK_FIELDS = [ // all eap options go in a sub-object `eapConfig` // eap method is stored under eapConfig.eapType -const EAP_FIELD_PREFIX = 'eapConfig.' -const EAP_METHOD_DISPLAY_NAME = 'Authentication' -const EAP_METHOD_FIELD = `${EAP_FIELD_PREFIX}eapType` -const EAP_METHOD_FIELD_ID = `${CONNECT_FIELD_ID_PREFIX}${EAP_METHOD_FIELD}` -const getEapMethod = (v: FormValues): ?string => get(v, EAP_METHOD_FIELD) +const SECURITY_TYPE_LABEL = 'Authentication' +const SECURITY_TYPE_ID = `${CONNECT_FIELD_ID_PREFIX}${SECURITY_TYPE_FIELD}` +const getEapType = (v: FormValues): ?string => get(v, EAP_TYPE_FIELD) export default class ConnectForm extends React.Component { constructor (props: Props) { @@ -64,7 +68,7 @@ export default class ConnectForm extends React.Component { this.state = {showPassword: {}} } - onSubmit = (values: {[name: string]: string}) => { + onSubmit = (values: FormValues) => { this.props.configure({ ssid: this.props.ssid, securityType: this.props.securityType, @@ -85,6 +89,10 @@ export default class ConnectForm extends React.Component { } getValidationSchema = (values: FormValues) => { + const securityType = this.getSecurityType(values) + + if (securityType === NO_SECURITY) return {} + const errors = this.getFields(values).reduce((errors, field) => { const {name, displayName, required} = field const value = get(values, name, '') @@ -98,19 +106,19 @@ export default class ConnectForm extends React.Component { return errors }, {}) - if (this.getSecurityType(values) === WPA_EAP_SECURITY) { - if (!getEapMethod(values)) { - set(errors, EAP_METHOD_FIELD, `${EAP_METHOD_DISPLAY_NAME} is required`) - } + if ( + !securityType || + (securityType === WPA_EAP_SECURITY && !getEapType(values)) + ) { + set(errors, SECURITY_TYPE_FIELD, `${SECURITY_TYPE_LABEL} is required`) } return errors } - // TODO(mc, 2018-10-26): allow security type to be pulled from values - // if not in props - getSecurityType (values: FormValues): WifiSecurityType { - return this.props.securityType + getSecurityType (values: FormValues): ?WifiSecurityType { + const formSecurityType: ?WifiSecurityType = (values.securityType: any) + return this.props.securityType || formSecurityType } getFields (values: FormValues): Array { @@ -118,10 +126,10 @@ export default class ConnectForm extends React.Component { if (securityType === WPA_PSK_SECURITY) return WPA_PSK_FIELDS if (securityType === WPA_EAP_SECURITY) { - const method = find(this.props.eapOptions, {name: getEapMethod(values)}) + const method = find(this.props.eapOptions, {name: getEapType(values)}) return get(method, 'options', []).map(field => ({ ...field, - name: `${EAP_FIELD_PREFIX}${field.name}`, + name: `${EAP_CONFIG_FIELD}.${field.name}`, })) } @@ -130,11 +138,7 @@ export default class ConnectForm extends React.Component { render () { const {showPassword} = this.state - const {securityType, keys, addKey, close} = this.props - const eapOptions = map(this.props.eapOptions, o => ({ - name: o.displayName || o.name, - value: o.name, - })) + const {keys, addKey, close, securityType: knownSecurityType} = this.props return ( { handleBlur, setFieldTouched, resetForm, + setValues, handleSubmit, } = formProps return (
- {securityType === WPA_EAP_SECURITY && ( - - { - // reset all other fields on EAP type change - resetForm(set({}, EAP_METHOD_FIELD, e.target.value)) - }} - onBlur={handleBlur} - error={ - get(touched, EAP_METHOD_FIELD) && - get(errors, EAP_METHOD_FIELD) - } - /> - - )} + { + if (nextValues.securityType === NO_SECURITY) { + setValues(nextValues) + } else { + resetForm(nextValues) + } + }} + onLoseFocus={setFieldTouched} + /> {this.getFields(values).map(field => ( mixed, children: React.Node, } @@ -24,6 +24,8 @@ export default function ConnectModal (props: Props) { body = `Wi-Fi network ${ssid} requires a WPA2 password.` } else if (securityType === WPA_EAP_SECURITY) { body = `Wi-Fi network ${ssid} requires 802.1X authentication.` + } else { + body = `Please select the security type for Wi-Fi network ${ssid}.` } } diff --git a/app/src/components/RobotSettings/SelectNetwork/SelectKey.js b/app/src/components/RobotSettings/SelectNetwork/SelectKey.js index a98c0c46b23..146a883671b 100644 --- a/app/src/components/RobotSettings/SelectNetwork/SelectKey.js +++ b/app/src/components/RobotSettings/SelectNetwork/SelectKey.js @@ -75,6 +75,7 @@ export default class SelectKey extends React.Component { const {id, name, value, error, keys, onLoseFocus} = this.props const keyOptions = map(keys, k => ({value: k.id, label: k.name})) const addNewGroup = { + label: null, options: [ { value: UPLOAD_KEY_VALUE, diff --git a/app/src/components/RobotSettings/SelectNetwork/SelectSecurity.js b/app/src/components/RobotSettings/SelectNetwork/SelectSecurity.js new file mode 100644 index 00000000000..a42140314b2 --- /dev/null +++ b/app/src/components/RobotSettings/SelectNetwork/SelectSecurity.js @@ -0,0 +1,122 @@ +// @flow +import * as React from 'react' +import find from 'lodash/find' +import set from 'lodash/set' + +import { + NO_SECURITY, + WPA_PSK_SECURITY, + WPA_EAP_SECURITY, + EAP_TYPE_FIELD, +} from '../../../http-api-client' +import SelectField from '../../SelectField' +import {FormTableRow} from './FormTable' +import styles from './styles.css' + +import type {SelectOption} from '../../SelectField' +import type { + WifiSecurityType, + WifiEapOptionsList, +} from '../../../http-api-client' +import type {FormValues} from './ConnectForm' + +type Props = { + id: string, + name: string, + label: string, + value: ?string, + knownSecurityType: ?WifiSecurityType, + touched: ?boolean, + error: ?string, + eapOptions: ?WifiEapOptionsList, + setValues: (values: FormValues) => mixed, + onLoseFocus: (name: string) => mixed, +} + +const NO_SECURITY_LABEL = 'None' +const WPA_PSK_SECURITY_LABEL = 'WPA2 Personal' +const PLACEHOLDER = 'Select authentication method' + +const BASE_SECURITY_OPTIONS: Array = [ + { + label: null, + options: [{value: NO_SECURITY, label: NO_SECURITY_LABEL}], + }, + { + label: null, + options: [{value: WPA_PSK_SECURITY, label: WPA_PSK_SECURITY_LABEL}], + }, +] + +const eapOptToSelectOpt = o => ({ + label: o.displayName || o.name, + value: o.name, +}) + +export default class SelectSecurity extends React.Component { + handleValueChange = (name: string, value: string) => { + const {knownSecurityType, eapOptions, setValues} = this.props + const eapType = find(eapOptions, {name: value}) + const nextValues = {} + + if (eapType) { + if (!knownSecurityType) set(nextValues, name, WPA_EAP_SECURITY) + set(nextValues, EAP_TYPE_FIELD, value) + } else { + set(nextValues, name, value) + } + + setValues(nextValues) + } + + getOptions (): Array { + const {knownSecurityType, eapOptions} = this.props + const options = knownSecurityType ? [] : [...BASE_SECURITY_OPTIONS] + + if (eapOptions && eapOptions.length) { + options.push({ + label: null, + options: eapOptions.map(eapOptToSelectOpt), + }) + } + + return options + } + + render () { + const { + id, + name, + label, + value, + touched, + error, + knownSecurityType, + onLoseFocus, + } = this.props + + if ( + knownSecurityType === NO_SECURITY || + knownSecurityType === WPA_PSK_SECURITY + ) { + return null + } + + return ( + + + + ) + } +} diff --git a/app/src/components/RobotSettings/SelectNetwork/SelectSsid.js b/app/src/components/RobotSettings/SelectNetwork/SelectSsid.js index bb889c68ad0..544e508c00f 100644 --- a/app/src/components/RobotSettings/SelectNetwork/SelectSsid.js +++ b/app/src/components/RobotSettings/SelectNetwork/SelectSsid.js @@ -44,7 +44,7 @@ function makeNetworkOption (nw: WifiNetwork): OptionType { ) const securedIcon = - nw.securityType !== 'none' ? ( + nw.securityType && nw.securityType !== 'none' ? ( ) : ( diff --git a/app/src/components/RobotSettings/SelectNetwork/index.js b/app/src/components/RobotSettings/SelectNetwork/index.js index ca02326071c..a6477ff386b 100644 --- a/app/src/components/RobotSettings/SelectNetwork/index.js +++ b/app/src/components/RobotSettings/SelectNetwork/index.js @@ -55,6 +55,7 @@ type Props = {...$Exact, ...SP, ...DP} type SelectNetworkState = { ssid: ?string, securityType: ?WifiSecurityType, + modalOpen: boolean, } const LIST_REFRESH_MS = 15000 @@ -63,7 +64,11 @@ class SelectNetwork extends React.Component { constructor (props) { super(props) // prepopulate selected SSID with currently connected network, if any - this.state = {ssid: this.getActiveSsid(), securityType: null} + this.state = { + ssid: this.getActiveSsid(), + securityType: null, + modalOpen: false, + } } setCurrentSsid = (_: string, ssid: string) => { @@ -71,21 +76,24 @@ class SelectNetwork extends React.Component { if (network) { const securityType = network.securityType - const nextState: $Shape = {ssid, securityType} + const nextState: $Shape = { + ssid, + securityType, + modalOpen: securityType !== NO_SECURITY, + } - if (securityType === NO_SECURITY) { + if (!nextState.modalOpen) { this.props.configure({ssid}) - } else if (securityType === WPA_EAP_SECURITY) { + } else if (securityType === WPA_EAP_SECURITY || !securityType) { this.props.fetchEapOptions() this.props.fetchKeys() } - // TODO(mc, 2018-10-18): handle hidden network this.setState(nextState) } } - closeConnectForm = () => this.setState({securityType: null}) + closeConnectForm = () => this.setState({securityType: null, modalOpen: false}) getActiveSsid (): ?string { const activeNetwork = find(this.props.list, 'active') @@ -110,7 +118,7 @@ class SelectNetwork extends React.Component { configure, addKey, } = this.props - const {ssid, securityType} = this.state + const {ssid, securityType, modalOpen} = this.state return ( @@ -127,8 +135,7 @@ class SelectNetwork extends React.Component { /> )} {ssid && - securityType && - securityType !== NO_SECURITY && ( + modalOpen && ( , label?: React.Node|} +export type GroupType = {|options: Array, label: React.Node|} +export type SelectOption = OptionType | GroupType type OptionList = Array @@ -21,7 +22,7 @@ type SelectProps = { /** field name */ name: string, /** React-Select option, usually label, value */ - options: Array, + options: Array, /** currently selected value */ value: ?string, /** change handler called with (name, value) */ @@ -38,6 +39,8 @@ type SelectProps = { caption?: string, /** if included, use error style and display error instead of caption */ error?: ?string, + /** menuPosition prop to send to react-select */ + menuPosition?: 'absolute' | 'fixed', } const SELECT_STYLES = { @@ -72,6 +75,7 @@ export default class SelectField extends React.Component { placeholder, className, error, + menuPosition, } = this.props const allOptions = flatMap(options, getOpts) const value = find(allOptions, {value: this.props.value}) || null @@ -99,6 +103,7 @@ export default class SelectField extends React.Component { IndicatorSeparator: null, }} className={className} + menuPosition={menuPosition} /> {caption &&

{caption}

} diff --git a/app/src/components/SelectField/styles.css b/app/src/components/SelectField/styles.css index 0c428e937f8..c8e9fa9d26c 100644 --- a/app/src/components/SelectField/styles.css +++ b/app/src/components/SelectField/styles.css @@ -45,8 +45,7 @@ @apply --font-body-1-dark; background-color: var(--c-white); - border-radius: 2px; - padding: 2px 0 0 0; + border-radius: 3px; } .select_group { diff --git a/app/src/http-api-client/networking.js b/app/src/http-api-client/networking.js index a71aaab4129..6627d667688 100644 --- a/app/src/http-api-client/networking.js +++ b/app/src/http-api-client/networking.js @@ -135,6 +135,10 @@ export const NO_SECURITY: 'none' = 'none' export const WPA_PSK_SECURITY: 'wpa-psk' = 'wpa-psk' export const WPA_EAP_SECURITY: 'wpa-eap' = 'wpa-eap' +export const SECURITY_TYPE_FIELD = 'securityType' +export const EAP_CONFIG_FIELD = 'eapConfig' +export const EAP_TYPE_FIELD = `${EAP_CONFIG_FIELD}.eapType` + export const fetchNetworkingStatus = buildRequestMaker('GET', STATUS) export const fetchWifiList = buildRequestMaker('GET', LIST) export const fetchWifiEapOptions = buildRequestMaker('GET', EAP_OPTIONS)