UI Usability Fixes
* Fallback to sessionStorage if localStorage is absent * Disable auto-correct and auto-capitalize on username field (SignIn) * Fix SignIn component name * Improve support for low screen widths Co-authored-by: kasedy <kasedy@gmail.com>
This commit is contained in:
parent
a1f4e57a21
commit
7d3bbf4240
@ -39,17 +39,17 @@ const styles = (theme: Theme) => createStyles({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type SignInPageProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
||||||
|
|
||||||
interface SignInPageState {
|
interface SignInState {
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
processing: boolean
|
processing: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class SignInPage extends Component<SignInPageProps, SignInPageState> {
|
class SignIn extends Component<SignInProps, SignInState> {
|
||||||
|
|
||||||
constructor(props: SignInPageProps) {
|
constructor(props: SignInProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
username: '',
|
username: '',
|
||||||
@ -115,6 +115,10 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
|
|||||||
value={username}
|
value={username}
|
||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
|
inputProps={{
|
||||||
|
autoCapitalize: "none",
|
||||||
|
autoCorrect: "off",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@ -140,4 +144,4 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignInPage)));
|
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
||||||
|
@ -7,21 +7,28 @@ export const ACCESS_TOKEN = 'access_token';
|
|||||||
export const LOGIN_PATHNAME = 'loginPathname';
|
export const LOGIN_PATHNAME = 'loginPathname';
|
||||||
export const LOGIN_SEARCH = 'loginSearch';
|
export const LOGIN_SEARCH = 'loginSearch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
|
||||||
|
*/
|
||||||
|
export function getStorage() {
|
||||||
|
return localStorage || sessionStorage;
|
||||||
|
}
|
||||||
|
|
||||||
export function storeLoginRedirect(location?: H.Location) {
|
export function storeLoginRedirect(location?: H.Location) {
|
||||||
if (location) {
|
if (location) {
|
||||||
localStorage.setItem(LOGIN_PATHNAME, location.pathname);
|
getStorage().setItem(LOGIN_PATHNAME, location.pathname);
|
||||||
localStorage.setItem(LOGIN_SEARCH, location.search);
|
getStorage().setItem(LOGIN_SEARCH, location.search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearLoginRedirect() {
|
export function clearLoginRedirect() {
|
||||||
localStorage.removeItem(LOGIN_PATHNAME);
|
getStorage().removeItem(LOGIN_PATHNAME);
|
||||||
localStorage.removeItem(LOGIN_SEARCH);
|
getStorage().removeItem(LOGIN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
||||||
const loginPathname = localStorage.getItem(LOGIN_PATHNAME);
|
const loginPathname = getStorage().getItem(LOGIN_PATHNAME);
|
||||||
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
|
const loginSearch = getStorage().getItem(LOGIN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
return {
|
return {
|
||||||
pathname: loginPathname || `/${PROJECT_PATH}/`,
|
pathname: loginPathname || `/${PROJECT_PATH}/`,
|
||||||
@ -33,7 +40,7 @@ export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
|||||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
* Wraps the normal fetch routene with one with provides the access token if present.
|
||||||
*/
|
*/
|
||||||
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||||
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
params.credentials = 'include';
|
params.credentials = 'include';
|
||||||
@ -63,7 +70,7 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addAccessTokenParameter(url: string) {
|
export function addAccessTokenParameter(url: string) {
|
||||||
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/s
|
|||||||
|
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||||
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
|
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||||
import { AuthenticationContext, Me } from './AuthenticationContext';
|
import { AuthenticationContext, Me } from './AuthenticationContext';
|
||||||
|
|
||||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
||||||
@ -81,7 +81,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
refresh = () => {
|
refresh = () => {
|
||||||
const accessToken = localStorage.getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@ -100,7 +100,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
|
|
||||||
signIn = (accessToken: string) => {
|
signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
const me: Me = decodeMeJWT(accessToken);
|
const me: Me = decodeMeJWT(accessToken);
|
||||||
this.setState({ context: { ...this.state.context, me } });
|
this.setState({ context: { ...this.state.context, me } });
|
||||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
||||||
@ -111,7 +111,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
signOut = () => {
|
signOut = () => {
|
||||||
localStorage.removeItem(ACCESS_TOKEN);
|
getStorage().removeItem(ACCESS_TOKEN);
|
||||||
this.setState({
|
this.setState({
|
||||||
context: {
|
context: {
|
||||||
...this.state.context,
|
...this.state.context,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Typography, TableRow, TableBody, TableCell, TableHead, Table, Box } from '@material-ui/core';
|
import { Typography, Box, List, ListItem, ListItemText } from '@material-ui/core';
|
||||||
import { SectionContent } from '../components';
|
import { SectionContent } from '../components';
|
||||||
|
|
||||||
class DemoInformation extends Component {
|
class DemoInformation extends Component {
|
||||||
@ -17,78 +17,52 @@ class DemoInformation extends Component {
|
|||||||
simplify merges should you wish to update your project with future framework changes.
|
simplify merges should you wish to update your project with future framework changes.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" paragraph>
|
<Typography variant="body1" paragraph>
|
||||||
The demo project interface code stored in the interface/project directory:
|
The demo project interface code is stored in the interface/project directory:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Table>
|
<List>
|
||||||
<TableHead>
|
<ListItem>
|
||||||
<TableRow>
|
<ListItemText
|
||||||
<TableCell>
|
primary="ProjectMenu.tsx"
|
||||||
File
|
secondary="You can add your project's screens to the side bar here."
|
||||||
</TableCell>
|
/>
|
||||||
<TableCell>
|
</ListItem>
|
||||||
Description
|
<ListItem>
|
||||||
</TableCell>
|
<ListItemText
|
||||||
</TableRow>
|
primary="ProjectRouting.tsx"
|
||||||
</TableHead>
|
secondary="The routing which controls the screens of your project."
|
||||||
<TableBody>
|
/>
|
||||||
<TableRow>
|
</ListItem>
|
||||||
<TableCell>
|
<ListItem>
|
||||||
ProjectMenu.tsx
|
<ListItemText
|
||||||
</TableCell>
|
primary="DemoProject.tsx"
|
||||||
<TableCell>
|
secondary="This screen, with tabs and tab routing."
|
||||||
You can add your project's screens to the side bar here.
|
/>
|
||||||
</TableCell>
|
</ListItem>
|
||||||
</TableRow>
|
<ListItem>
|
||||||
<TableRow>
|
<ListItemText
|
||||||
<TableCell>
|
primary="DemoInformation.tsx"
|
||||||
ProjectRouting.tsx
|
secondary="The demo information page."
|
||||||
</TableCell>
|
/>
|
||||||
<TableCell>
|
</ListItem>
|
||||||
The routing which controls the screens of your project.
|
<ListItem>
|
||||||
</TableCell>
|
<ListItemText
|
||||||
</TableRow>
|
primary="LightStateRestController.tsx"
|
||||||
<TableRow>
|
secondary="A form which lets the user control the LED over a REST service."
|
||||||
<TableCell>
|
/>
|
||||||
DemoProject.tsx
|
</ListItem>
|
||||||
</TableCell>
|
<ListItem>
|
||||||
<TableCell>
|
<ListItemText
|
||||||
This screen, with tabs and tab routing.
|
primary="LightStateWebSocketController.tsx"
|
||||||
</TableCell>
|
secondary="A form which lets the user control and monitor the status of the LED over WebSockets."
|
||||||
</TableRow>
|
/>
|
||||||
<TableRow>
|
</ListItem>
|
||||||
<TableCell>
|
<ListItem>
|
||||||
DemoInformation.tsx
|
<ListItemText
|
||||||
</TableCell>
|
primary="LightMqttSettingsController.tsx"
|
||||||
<TableCell>
|
secondary="A form which lets the user change the MQTT settings for MQTT based control of the LED."
|
||||||
The demo information page.
|
/>
|
||||||
</TableCell>
|
</ListItem>
|
||||||
</TableRow>
|
</List>
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
LightStateRestController.tsx
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
A form which lets the user control the LED over a REST service.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
LightStateWebSocketController.tsx
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
A form which lets the user control and monitor the status of the LED over WebSockets.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
LightMqttSettingsController.tsx
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
A form which lets the user change the MQTT settings for MQTT based control of the LED.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
|
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow } from '@material-ui/core';
|
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
|
||||||
import { Box, Button, Typography, } from '@material-ui/core';
|
import { Box, Button, Typography, } from '@material-ui/core';
|
||||||
|
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
@ -28,7 +28,7 @@ function compareUsers(a: User, b: User) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
|
||||||
|
|
||||||
type ManageUsersFormState = {
|
type ManageUsersFormState = {
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@ -106,12 +106,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, loadData } = this.props;
|
const { width, data, loadData } = this.props;
|
||||||
const { user, creating } = this.state;
|
const { user, creating } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ValidatorForm onSubmit={this.onSubmit}>
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
<Table size="small">
|
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Username</TableCell>
|
<TableCell>Username</TableCell>
|
||||||
@ -141,12 +141,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter>
|
<TableFooter >
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={2} />
|
||||||
<TableCell align="center">
|
<TableCell align="center" padding="default">
|
||||||
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
|
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
|
||||||
Add User
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -188,4 +188,4 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticatedContext(ManageUsersForm);
|
export default withAuthenticatedContext(withWidth()(ManageUsersForm));
|
||||||
|
@ -79,7 +79,7 @@ class WiFiStatusForm extends Component<WiFiStatusFormProps> {
|
|||||||
<SettingsInputComponentIcon />
|
<SettingsInputComponentIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip ? data.gateway_ip : "none"} />
|
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
|
Loading…
Reference in New Issue
Block a user