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,
|
||||
password: string,
|
||||
processing: boolean
|
||||
}
|
||||
|
||||
class SignInPage extends Component<SignInPageProps, SignInPageState> {
|
||||
class SignIn extends Component<SignInProps, SignInState> {
|
||||
|
||||
constructor(props: SignInPageProps) {
|
||||
constructor(props: SignInProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
@ -115,6 +115,10 @@ class SignInPage extends Component<SignInPageProps, SignInPageState> {
|
||||
value={username}
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
autoCapitalize: "none",
|
||||
autoCorrect: "off",
|
||||
}}
|
||||
/>
|
||||
<PasswordValidator
|
||||
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_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) {
|
||||
if (location) {
|
||||
localStorage.setItem(LOGIN_PATHNAME, location.pathname);
|
||||
localStorage.setItem(LOGIN_SEARCH, location.search);
|
||||
getStorage().setItem(LOGIN_PATHNAME, location.pathname);
|
||||
getStorage().setItem(LOGIN_SEARCH, location.search);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearLoginRedirect() {
|
||||
localStorage.removeItem(LOGIN_PATHNAME);
|
||||
localStorage.removeItem(LOGIN_SEARCH);
|
||||
getStorage().removeItem(LOGIN_PATHNAME);
|
||||
getStorage().removeItem(LOGIN_SEARCH);
|
||||
}
|
||||
|
||||
export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
||||
const loginPathname = localStorage.getItem(LOGIN_PATHNAME);
|
||||
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
|
||||
const loginPathname = getStorage().getItem(LOGIN_PATHNAME);
|
||||
const loginSearch = getStorage().getItem(LOGIN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
return {
|
||||
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.
|
||||
*/
|
||||
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
params = params || {};
|
||||
params.credentials = 'include';
|
||||
@ -63,7 +70,7 @@ export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestIni
|
||||
}
|
||||
|
||||
export function addAccessTokenParameter(url: string) {
|
||||
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (!accessToken) {
|
||||
return url;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/s
|
||||
|
||||
import history from '../history'
|
||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
|
||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||
import { AuthenticationContext, Me } from './AuthenticationContext';
|
||||
|
||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
||||
@ -81,7 +81,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const accessToken = localStorage.getItem(ACCESS_TOKEN)
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
||||
if (accessToken) {
|
||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||
.then(response => {
|
||||
@ -100,7 +100,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
|
||||
signIn = (accessToken: string) => {
|
||||
try {
|
||||
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
||||
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||
const me: Me = decodeMeJWT(accessToken);
|
||||
this.setState({ context: { ...this.state.context, me } });
|
||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
||||
@ -111,7 +111,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
}
|
||||
|
||||
signOut = () => {
|
||||
localStorage.removeItem(ACCESS_TOKEN);
|
||||
getStorage().removeItem(ACCESS_TOKEN);
|
||||
this.setState({
|
||||
context: {
|
||||
...this.state.context,
|
||||
|
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
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.
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
The demo project interface code stored in the interface/project directory:
|
||||
</Typography>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
File
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
Description
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
ProjectMenu.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
You can add your project's screens to the side bar here.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
ProjectRouting.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
The routing which controls the screens of your project.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
DemoProject.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
This screen, with tabs and tab routing.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
DemoInformation.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
The demo information page.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<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>
|
||||
The demo project interface code is stored in the interface/project directory:
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="ProjectMenu.tsx"
|
||||
secondary="You can add your project's screens to the side bar here."
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="ProjectRouting.tsx"
|
||||
secondary="The routing which controls the screens of your project."
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="DemoProject.tsx"
|
||||
secondary="This screen, with tabs and tab routing."
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="DemoInformation.tsx"
|
||||
secondary="The demo information page."
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="LightStateRestController.tsx"
|
||||
secondary="A form which lets the user control the LED over a REST service."
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="LightStateWebSocketController.tsx"
|
||||
secondary="A form which lets the user control and monitor the status of the LED over WebSockets."
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="LightMqttSettingsController.tsx"
|
||||
secondary="A form which lets the user change the MQTT settings for MQTT based control of the LED."
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Box mt={2}>
|
||||
<Typography variant="body1">
|
||||
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 { 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 EditIcon from '@material-ui/icons/Edit';
|
||||
@ -28,7 +28,7 @@ function compareUsers(a: User, b: User) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
|
||||
|
||||
type ManageUsersFormState = {
|
||||
creating: boolean;
|
||||
@ -106,12 +106,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, loadData } = this.props;
|
||||
const { width, data, loadData } = this.props;
|
||||
const { user, creating } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<Table size="small">
|
||||
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Username</TableCell>
|
||||
@ -141,12 +141,12 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableFooter >
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="center">
|
||||
<TableCell align="center" padding="default">
|
||||
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
|
||||
Add User
|
||||
Add
|
||||
</Button>
|
||||
</TableCell>
|
||||
</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 />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip ? data.gateway_ip : "none"} />
|
||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
|
Loading…
Reference in New Issue
Block a user