Browse Source

Add LoginStatusPane.

pull/1/head
Titouan Rigoudy 9 years ago
parent
commit
98a4f832b9
10 changed files with 223 additions and 21 deletions
  1. +7
    -7
      src/actions/socketHandlerActions.js
  2. +6
    -6
      src/components/ConnectForm.js
  3. +7
    -4
      src/components/SolsticeApp.js
  4. +4
    -0
      src/constants/login.js
  5. +104
    -0
      src/containers/LoginStatusPane.js
  6. +2
    -0
      src/reducers/index.js
  7. +56
    -0
      src/reducers/login.js
  8. +2
    -4
      src/reducers/socket.js
  9. +15
    -0
      src/utils/propTypeRequiredWrapper.js
  10. +20
    -0
      src/utils/propTypeSymbol.js

+ 7
- 7
src/actions/socketHandlerActions.js View File

@ -13,18 +13,18 @@ export default {
} }
}), }),
onerror: event => ({
type: SOCKET_SET_ERROR
}),
onerror: event => ({ type: SOCKET_SET_ERROR }),
onopen: event => (dispatch, getState) => ({
type: SOCKET_SET_OPEN
}),
onopen: event => ({ type: SOCKET_SET_OPEN }),
onmessage: event => { onmessage: event => {
const action = { type: SOCKET_RECEIVE_MESSAGE }; const action = { type: SOCKET_RECEIVE_MESSAGE };
try { try {
action.payload = JSON.parse(event.data);
const { variant, fields: [data] } = JSON.parse(event.data);
if (typeof variant === "undefined") {
throw new Error('Missing "variant" field in control response');
}
action.payload = { variant, data };
} catch (err) { } catch (err) {
action.error = true; action.error = true;
action.payload = err; action.payload = err;


+ 6
- 6
src/components/ConnectForm.js View File

@ -5,12 +5,12 @@ import { STATE_CLOSED } from "../constants/socket";
import ControlRequest from "../utils/ControlRequest"; import ControlRequest from "../utils/ControlRequest";
const ConnectForm = (props) => { const ConnectForm = (props) => {
const { fields: { url }, handleSubmit, socket, socketActions } = props;
const submit = (values, dispatch) => {
dispatch(socketActions.open(values.url));
};
const { fields: { url }, handleSubmit, socket, socketOpen } = props;
const onSubmit = handleSubmit((values) => socketOpen(values.url));
return ( return (
<form onSubmit={handleSubmit(submit)}>
<form onSubmit={onSubmit}>
<input type="url" defaultValue="ws://localhost:2244" {...url} <input type="url" defaultValue="ws://localhost:2244" {...url}
required pattern="wss?://.+"/> required pattern="wss?://.+"/>
<button type="submit" disabled={socket.state !== STATE_CLOSED}> <button type="submit" disabled={socket.state !== STATE_CLOSED}>
@ -24,7 +24,7 @@ ConnectForm.propTypes = {
fields: PropTypes.object.isRequired, fields: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
socket: PropTypes.object.isRequired, socket: PropTypes.object.isRequired,
socketActions: PropTypes.object.isRequired
socketOpen: PropTypes.func.isRequired
}; };
export default reduxForm({ export default reduxForm({


+ 7
- 4
src/components/SolsticeApp.js View File

@ -3,15 +3,17 @@ import React, {PropTypes} from "react";
import ConnectForm from "../components/ConnectForm"; import ConnectForm from "../components/ConnectForm";
import SocketStatusPane from "../components/SocketStatusPane"; import SocketStatusPane from "../components/SocketStatusPane";
import LoginStatusPane from "../containers/LoginStatusPane";
import { STATE_OPEN } from "../constants/socket"; import { STATE_OPEN } from "../constants/socket";
const SolsticeApp = (props) => { const SolsticeApp = (props) => {
const { socket, actions } = props;
const { actions, socket } = props;
if (socket.state !== STATE_OPEN ) { if (socket.state !== STATE_OPEN ) {
return ( return (
<main> <main>
<ConnectForm socket={socket} <ConnectForm socket={socket}
socketActions={actions.socketActions}/>
socketOpen={actions.socketActions.open}/>
<SocketStatusPane {...socket} /> <SocketStatusPane {...socket} />
</main> </main>
); );
@ -20,13 +22,14 @@ const SolsticeApp = (props) => {
<main> <main>
<h1>Solstice web UI</h1> <h1>Solstice web UI</h1>
<SocketStatusPane {...socket} /> <SocketStatusPane {...socket} />
<LoginStatusPane socketSend={actions.socketActions.send}/>
</main> </main>
); );
}; };
SolsticeApp.propTypes = { SolsticeApp.propTypes = {
socket: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
actions: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
}; };
export default SolsticeApp; export default SolsticeApp;

+ 4
- 0
src/constants/login.js View File

@ -0,0 +1,4 @@
export const LOGIN_STATUS_UNKNOWN = Symbol("LOGIN_STATUS_UNKNOWN");
export const LOGIN_STATUS_PENDING = Symbol("LOGIN_STATUS_PENDING");
export const LOGIN_STATUS_SUCCESS = Symbol("LOGIN_STATUS_SUCCESS");
export const LOGIN_STATUS_FAILURE = Symbol("LOGIN_STATUS_FAILURE");

+ 104
- 0
src/containers/LoginStatusPane.js View File

@ -0,0 +1,104 @@
import React, { PropTypes } from "react";
import { connect } from "react-redux";
import ControlRequest from "../utils/ControlRequest";
import propTypeSymbol from "../utils/propTypeSymbol";
import {
LOGIN_STATUS_UNKNOWN,
LOGIN_STATUS_PENDING,
LOGIN_STATUS_SUCCESS,
LOGIN_STATUS_FAILURE
} from "../constants/login";
const ID = "login-status-pane";
const INTERVAL_MSEC = 60 * 1000;
class LoginStatusPane extends React.Component
{
constructor(props) {
super(props);
}
componentDidMount() {
const fetchStatus = () => {
this.props.socketSend(ControlRequest.loginStatus());
};
fetchStatus();
// Refresh login status periodically
this.interval_id = setInterval(fetchStatus, INTERVAL_MSEC);
}
componentWillUnmount() {
clearInterval(this.interval_id);
}
render_unknown() {
return (
<div id={ID}>
Fetching login status...
</div>
);
}
render_pending() {
return (
<div id={ID}>
Logging in as {this.props.username}...
</div>
);
}
render_success() {
let motd_element;
if (this.props.motd) {
motd_element = (
<span id="login-motd">
MOTD: {this.props.motd}
</span>
);
}
return (
<div id={ID}>
Logged in as {this.props.username}
{motd_element}
</div>
);
}
render_failure() {
return (
<div id={ID}>
Failed to log in as {this.props.username}
<span id="login-reason">
Reason: {this.props.reason}
</span>
</div>
);
}
render() {
switch (this.props.status) {
case LOGIN_STATUS_UNKNOWN:
return this.render_unknown();
case LOGIN_STATUS_PENDING:
return this.render_pending();
case LOGIN_STATUS_SUCCESS:
return this.render_success();
case LOGIN_STATUS_FAILURE:
return this.render_failure();
}
}
}
LoginStatusPane.propTypes = {
status: propTypeSymbol.isRequired,
username: PropTypes.string,
motd: PropTypes.string,
reason: PropTypes.string,
socketSend: PropTypes.func.isRequired
};
export default connect(
state => state.login
)(LoginStatusPane);

+ 2
- 0
src/reducers/index.js View File

@ -1,9 +1,11 @@
import { combineReducers } from "redux"; import { combineReducers } from "redux";
import { reducer as form } from "redux-form"; import { reducer as form } from "redux-form";
import login from "./login";
import socket from "./socket"; import socket from "./socket";
const rootReducer = combineReducers({ const rootReducer = combineReducers({
login,
socket, socket,
form form
}); });


+ 56
- 0
src/reducers/login.js View File

@ -0,0 +1,56 @@
import { SOCKET_RECEIVE_MESSAGE } from "../constants/ActionTypes";
import {
LOGIN_STATUS_UNKNOWN,
LOGIN_STATUS_PENDING,
LOGIN_STATUS_SUCCESS,
LOGIN_STATUS_FAILURE
} from "../constants/login";
const initialState = {
status: LOGIN_STATUS_UNKNOWN
};
export default (state = initialState, action) => {
const { type, payload } = action;
if (type !== SOCKET_RECEIVE_MESSAGE) {
return state;
}
const { variant, data } = payload;
if (variant !== "LoginStatusResponse") {
return state;
}
switch (data.variant) {
case "Pending":
{ // sub-block required otherwise const username declarations clash
const [ username ] = data.fields;
return {
status: LOGIN_STATUS_PENDING,
username
};
}
case "Success":
{ // sub-block required otherwise const username declarations clash
const [ username, motd ] = data.fields;
return {
status: LOGIN_STATUS_SUCCESS,
username,
motd
};
}
case "Failure":
{ // sub-block required otherwise const username declarations clash
const [ username, reason ] = data.fields;
return {
status: LOGIN_STATUS_FAILURE,
username,
reason
};
}
default:
return state;
}
};

+ 2
- 4
src/reducers/socket.js View File

@ -1,5 +1,3 @@
import objectAssign from "object-assign";
import * as types from "../constants/ActionTypes"; import * as types from "../constants/ActionTypes";
import { import {
STATE_OPENING, STATE_OPEN, STATE_CLOSING, STATE_CLOSED STATE_OPENING, STATE_OPEN, STATE_CLOSING, STATE_CLOSED
@ -10,7 +8,7 @@ const initialState = {
url: null url: null
}; };
export default function socket(state = initialState, action) {
export default (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case types.SOCKET_SET_OPENING: case types.SOCKET_SET_OPENING:
if (action.error) { if (action.error) {
@ -47,4 +45,4 @@ export default function socket(state = initialState, action) {
default: default:
return state; return state;
} }
}
};

+ 15
- 0
src/utils/propTypeRequiredWrapper.js View File

@ -0,0 +1,15 @@
const checkRequiredThenValidate = (validator) =>
(props, propName, componentName, ...rest) =>
{
if (props[propName] !== null) {
return validator(props, propName, componentName, ...rest);
}
return new Error(
`Missing prop \`${propName}\` not supplied to \`${componentName}\``
);
};
export default (validator) => {
validator.isRequired = checkRequiredThenValidate(validator);
return validator;
};

+ 20
- 0
src/utils/propTypeSymbol.js View File

@ -0,0 +1,20 @@
import propTypeRequiredWrapper from "./propTypeRequiredWrapper";
// Simple validator for Symbols (instanceof does not work on symbols).
function propTypeSymbol(props, propName, componentName) {
const prop = props[propName];
if (prop === null) {
return;
}
const type = typeof prop;
if (type === "symbol") {
return;
}
return new Error(
`Invalid prop \`${propName}\` of type \`${type}\` ` +
`supplied to \`${componentName}\`, expected \`symbol\``
);
}
export default propTypeRequiredWrapper(propTypeSymbol);

Loading…
Cancel
Save