Re-engineer UI in TypeScript (#89)
* Re-engineer UI in TypeScript * Switch to named imports where possible * Restructure file system layout * Update depencencies * Update README.md * Change explicit colors for better support for dark theme
This commit is contained in:
30
interface/src/security/ManageUsersController.tsx
Normal file
30
interface/src/security/ManageUsersController.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import ManageUsersForm from './ManageUsersForm';
|
||||
import { SecuritySettings } from './types';
|
||||
|
||||
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
|
||||
|
||||
class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="Manage Users" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <ManageUsersForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
|
190
interface/src/security/ManageUsersForm.tsx
Normal file
190
interface/src/security/ManageUsersForm.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
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 { Box, Button, Typography, } from '@material-ui/core';
|
||||
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import { RestFormProps, FormActions, FormButton } from '../components';
|
||||
|
||||
import UserForm from './UserForm';
|
||||
import { SecuritySettings, User } from './types';
|
||||
|
||||
function compareUsers(a: User, b: User) {
|
||||
if (a.username < b.username) {
|
||||
return -1;
|
||||
}
|
||||
if (a.username > b.username) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||
|
||||
type ManageUsersFormState = {
|
||||
creating: boolean;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
||||
|
||||
state: ManageUsersFormState = {
|
||||
creating: false
|
||||
};
|
||||
|
||||
createUser = () => {
|
||||
this.setState({
|
||||
creating: true,
|
||||
user: {
|
||||
username: "",
|
||||
password: "",
|
||||
admin: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
uniqueUsername = (username: string) => {
|
||||
return !this.props.data.users.find(u => u.username === username);
|
||||
}
|
||||
|
||||
noAdminConfigured = () => {
|
||||
return !this.props.data.users.find(u => u.admin);
|
||||
}
|
||||
|
||||
removeUser = (user: User) => {
|
||||
const { data } = this.props;
|
||||
const users = data.users.filter(u => u.username !== user.username);
|
||||
this.props.setData({ ...data, users });
|
||||
}
|
||||
|
||||
startEditingUser = (user: User) => {
|
||||
this.setState({
|
||||
creating: false,
|
||||
user
|
||||
});
|
||||
};
|
||||
|
||||
cancelEditingUser = () => {
|
||||
this.setState({
|
||||
user: undefined
|
||||
});
|
||||
}
|
||||
|
||||
doneEditingUser = () => {
|
||||
const { user } = this.state;
|
||||
if (user) {
|
||||
const { data } = this.props;
|
||||
const users = data.users.filter(u => u.username !== user.username);
|
||||
users.push(user);
|
||||
this.props.setData({ ...data, users });
|
||||
this.setState({
|
||||
user: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ user: { ...this.state.user!, [name]: event.target.value } });
|
||||
};
|
||||
|
||||
handleUserCheckboxChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ user: { ...this.state.user!, [name]: event.target.checked } });
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.saveData();
|
||||
this.props.authenticatedContext.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, loadData } = this.props;
|
||||
const { user, creating } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Username</TableCell>
|
||||
<TableCell align="center">Admin?</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.users.sort(compareUsers).map(user => (
|
||||
<TableRow key={user.username}>
|
||||
<TableCell component="th" scope="row">
|
||||
{user.username}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{
|
||||
user.admin ? <CheckIcon /> : <CloseIcon />
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconButton aria-label="Delete" onClick={() => this.removeUser(user)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="center">
|
||||
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
|
||||
Add User
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
{
|
||||
this.noAdminConfigured() &&
|
||||
<Typography component="div" variant="body1">
|
||||
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
|
||||
You must have at least one admin user configured.
|
||||
</Box>
|
||||
</Typography>
|
||||
}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
|
||||
Save
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||
Reset
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
{
|
||||
user &&
|
||||
<UserForm
|
||||
user={user}
|
||||
creating={creating}
|
||||
onDoneEditing={this.doneEditingUser}
|
||||
onCancelEditing={this.cancelEditingUser}
|
||||
handleValueChange={this.handleUserValueChange}
|
||||
handleCheckboxChange={this.handleUserCheckboxChange}
|
||||
uniqueUsername={this.uniqueUsername}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(ManageUsersForm);
|
37
interface/src/security/Security.tsx
Normal file
37
interface/src/security/Security.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import ManageUsersController from './ManageUsersController';
|
||||
import SecuritySettingsController from './SecuritySettingsController';
|
||||
|
||||
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class Security extends Component<SecurityProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Security">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value="/security/users" label="Manage Users" />
|
||||
<Tab value="/security/settings" label="Security Settings" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact={true} path="/security/users" component={ManageUsersController} />
|
||||
<AuthenticatedRoute exact={true} path="/security/settings" component={SecuritySettingsController} />
|
||||
<Redirect to="/security/users" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Security;
|
30
interface/src/security/SecuritySettingsController.tsx
Normal file
30
interface/src/security/SecuritySettingsController.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||
import { SecuritySettings } from './types';
|
||||
|
||||
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
|
||||
|
||||
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="Security Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <SecuritySettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
|
55
interface/src/security/SecuritySettingsForm.tsx
Normal file
55
interface/src/security/SecuritySettingsForm.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import { RestFormProps, PasswordValidator, FormActions, FormButton } from '../components';
|
||||
|
||||
import { SecuritySettings } from './types';
|
||||
|
||||
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||
|
||||
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.saveData();
|
||||
this.props.authenticatedContext.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, handleValueChange, loadData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']}
|
||||
name="jwt_secret"
|
||||
label="JWT Secret"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.jwt_secret}
|
||||
onChange={handleValueChange('jwt_secret')}
|
||||
margin="normal"
|
||||
/>
|
||||
<Typography component="div" variant="body1">
|
||||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
|
||||
If you modify the JWT Secret, all users will be logged out.
|
||||
</Box>
|
||||
</Typography>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
Save
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||
Reset
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(SecuritySettingsForm);
|
87
interface/src/security/UserForm.tsx
Normal file
87
interface/src/security/UserForm.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Checkbox } from '@material-ui/core';
|
||||
|
||||
import { PasswordValidator, BlockFormControlLabel, FormButton } from '../components';
|
||||
|
||||
import { User } from './types';
|
||||
|
||||
interface UserFormProps {
|
||||
creating: boolean;
|
||||
user: User;
|
||||
uniqueUsername: (value: any) => boolean;
|
||||
handleValueChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleCheckboxChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => void;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
}
|
||||
|
||||
class UserForm extends React.Component<UserFormProps> {
|
||||
|
||||
formRef: RefObject<any> = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
this.formRef.current.submit();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, creating, handleValueChange, handleCheckboxChange, onDoneEditing, onCancelEditing } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={true}>
|
||||
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<TextValidator
|
||||
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}
|
||||
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []}
|
||||
name="username"
|
||||
label="Username"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={user.username}
|
||||
disabled={!creating}
|
||||
onChange={handleValueChange('username')}
|
||||
margin="normal"
|
||||
/>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||
errorMessages={['Password is required', 'Password must be 64 characters or less']}
|
||||
name="password"
|
||||
label="Password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={user.password}
|
||||
onChange={handleValueChange('password')}
|
||||
margin="normal"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
value="admin"
|
||||
checked={user.admin}
|
||||
onChange={handleCheckboxChange('admin')}
|
||||
/>
|
||||
}
|
||||
label="Admin?"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
|
||||
Done
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
|
||||
Cancel
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserForm;
|
11
interface/src/security/types.ts
Normal file
11
interface/src/security/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
export interface SecuritySettings {
|
||||
users: User[];
|
||||
jwt_secret: string;
|
||||
}
|
||||
|
Reference in New Issue
Block a user