Created
November 25, 2019 20:28
-
-
Save danielsmykowski1/49dc08bf30285401a0312ffefd4ceb7d to your computer and use it in GitHub Desktop.
A React main screen component written in Typescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react'; | |
import { reaction } from 'mobx'; | |
import { observer, inject } from 'mobx-react'; | |
import * as usb from 'usb-detection' | |
import * as moment from 'moment' | |
import { | |
Table, Pagination, Header, Icon, Form, | |
Container, Modal, Button, Input, | |
} from 'semantic-ui-react'; | |
import { BaseComponent } from '../components' | |
import { Notifications } from '../components/nofitications'; | |
import { DomainStore, CheckUgrardKeyUniqueType } from '../../stores/domain.store'; | |
import { AsyncState, AsyncError } from '../../models/async.models'; | |
import { C } from '../../c'; | |
import { User } from '../../models/domain.models'; | |
interface Props { | |
domainStore: DomainStore | |
notifications: () => Notifications | |
} | |
interface State { | |
newUser: User | |
editUser: User | |
saveUserState: AsyncState<User> | |
loadUserListState: AsyncState<User[]> | |
checkUguardKeyUniqueState: AsyncState<CheckUgrardKeyUniqueType> | |
userListPage: number | |
usbDevice: usb.Device, | |
pickUserMode?: User, | |
} | |
@inject('domainStore') | |
@observer | |
export class MainScreen extends BaseComponent<Props, State> { | |
constructor(props: Props) { | |
super(props) | |
this.state = { | |
newUser: null, | |
editUser: null, | |
saveUserState: props.domainStore.saveUserState, | |
loadUserListState: props.domainStore.loadUserListState, | |
checkUguardKeyUniqueState: props.domainStore.checkUguardKeyUniqueState, | |
userListPage: 1, | |
usbDevice: null, | |
} | |
} | |
componentDidMount() { | |
usb.startMonitoring() | |
usb.on('add', (device) => { | |
console.log('usb device pluged in', device) | |
if (this.state.usbDevice) { | |
this.props.notifications().add({ | |
level: 'warning', | |
title: 'Multiple USB', | |
message: 'Plugged-in USB device ignored', | |
}) | |
return | |
} | |
if (!device.serialNumber || device.serialNumber.length < 5) | |
return this.props.notifications().add({ | |
level: 'error', | |
title: 'Invalid serial number', | |
message: `Serial number "${device.serialNumber}" is invalid`, | |
delay: 9000, | |
}) | |
usb.on(`remove:${device.vendorId}:${device.productId}`, () => { | |
console.log('usb device unplugged') | |
if (this.state.usbDevice) { | |
this.setState({ newUser: null, usbDevice: null }) | |
this.props.notifications().add({ | |
level: 'error', | |
title: 'Unplugged', | |
message: 'The usb device has been unplugged', | |
}) | |
} | |
}); | |
const newUser = new User('', '', device, moment().add('year', 1).toDate(), '') | |
this.setState({ newUser, usbDevice: device, }, | |
() => this.props.domainStore.checkUguardKeyUnique(newUser.uguardKeys[0])) | |
}) | |
this.disposables = [ | |
reaction( | |
() => this.props.domainStore.saveUserState, | |
(saveUserState) => { | |
if (saveUserState.isFailed()) { | |
this.props.notifications() | |
.addFromAsyncError(saveUserState.error) | |
} | |
if (saveUserState.isSuccessful()) { | |
this.props.notifications().add({ | |
level: 'success', | |
message: 'Saved successfully', | |
onDismiss: () => this.setState({ newUser: null, usbDevice: null }) | |
}) | |
} | |
this.setState({ saveUserState }) | |
}, | |
), | |
reaction( | |
() => this.props.domainStore.loadUserListState, | |
(loadUserListState) => { | |
this.setState({ loadUserListState }) | |
}, | |
), | |
reaction( | |
() => this.props.domainStore.checkUguardKeyUniqueState, | |
(checkUguardKeyUniqueState) => { | |
this.setState({ checkUguardKeyUniqueState }) | |
}, | |
), | |
] | |
if (this.state.loadUserListState.isUseless()) | |
this.props.domainStore.loadUserList() | |
} | |
componentWillUnmount() { | |
usb.stopMonitoring() | |
} | |
render() { | |
return ( | |
<Container> | |
{this.state.newUser != null && !this.state.pickUserMode | |
? this.renderNewUserModal() : null} | |
{this.state.editUser != null ? this.renderEditUserModal() : null} | |
{this.asyncStateWidget(this.state.loadUserListState, { | |
onFail: (error) => this.asyncErrorMessage(error, { | |
text: 'Try again', | |
onClick: () => this.props.domainStore.loadUserList(), | |
}), | |
onProgress: () => this.backLoading(), | |
onSuccess: (data) => this.renderUserListTable(data), | |
})} | |
</Container > | |
) | |
} | |
private saveUser(user: User, saveDownloadLinks: boolean) { | |
if (this.state.saveUserState.isInProgress()) | |
return | |
if (!user.id && !user.name) | |
return this.props.notifications().add({ | |
level: 'error', | |
message: 'Empty name', | |
}) | |
if (!user.phone) | |
return this.props.notifications().add({ | |
level: 'error', | |
message: 'Empty phone number', | |
}) | |
this.props.domainStore.saveUser(user, saveDownloadLinks) | |
} | |
private renderUserListTable(users: User[]): JSX.Element { | |
const pageCount = Math.ceil(users.length / C.ITEMS_PER_PAGE) | |
const footer = pageCount > 1 ? ( | |
<Table.Footer> | |
<Table.Row> | |
<Table.HeaderCell colSpan='8' className='right aligned'> | |
<Pagination | |
size='tiny' | |
activePage={this.state.userListPage} | |
totalPages={pageCount} | |
nextItem={false} | |
prevItem={false} | |
siblingRange={1} | |
boundaryRange={0} | |
ellipsisItem={false} | |
onPageChange={(_, data) => this.setState({ | |
userListPage: data.activePage as number, | |
})} /> | |
</Table.HeaderCell> | |
</Table.Row> | |
</Table.Footer> | |
) : null | |
const startIndex = (this.state.userListPage - 1) * C.ITEMS_PER_PAGE | |
return ( | |
<Table celled selectable size='small'> | |
<Table.Header className='hideOnMobile'> | |
<Table.Row> | |
<Table.HeaderCell width='3'>ID</Table.HeaderCell> | |
<Table.HeaderCell>User name</Table.HeaderCell> | |
<Table.HeaderCell>User phone</Table.HeaderCell> | |
<Table.HeaderCell>Active Until</Table.HeaderCell> | |
<Table.HeaderCell>Description</Table.HeaderCell> | |
</Table.Row> | |
</Table.Header> | |
<Table.Body> | |
{ | |
users | |
.slice(startIndex, startIndex + C.ITEMS_PER_PAGE) | |
.map(user => ( | |
<Table.Row key={'userRow' + user.id} | |
className={this.state.pickUserMode ? 'pick-mode' : 'standard-mode'} | |
style={{ cursor: 'pointer' }} | |
onClick={() => { | |
if (this.state.pickUserMode) { | |
this.state.pickUserMode.id = user.id | |
this.state.pickUserMode.name = user.name | |
this.state.pickUserMode.phone = user.phone | |
this.state.pickUserMode.activeUntil = user.activeUntil | |
this.state.pickUserMode.description = user.description | |
this.state.pickUserMode.uguardKeys.push(...user.uguardKeys) | |
this.setState({ pickUserMode: undefined }) | |
} | |
}} | |
onDoubleClick={() => this.setState({ editUser: user })} > | |
<Table.Cell>{user.id}</Table.Cell> | |
<Table.Cell>{user.name}</Table.Cell> | |
<Table.Cell>{user.phone}</Table.Cell> | |
<Table.Cell>{moment(user.activeUntil).format('DD-MM-YYYY')}</Table.Cell> | |
<Table.Cell>{user.description}</Table.Cell> | |
</Table.Row> | |
)) | |
} | |
</Table.Body> | |
{footer} | |
</Table> | |
) | |
} | |
private renderNewUserModal() { | |
const user = this.state.newUser | |
const device = this.state.usbDevice | |
return ( | |
<Modal size='small' centered={false} | |
open={this.state.newUser != null} | |
onClose={() => this.setState({ newUser: null, usbDevice: null })} | |
closeOnDimmerClick={false} | |
closeOnDocumentClick={false} | |
closeIcon> | |
<Header icon='key' content='New UGuard Key' /> | |
<Modal.Content> | |
<Form size='tiny' > | |
<Header>USB Info</Header> | |
<Form.Group widths='equal'> | |
<Form.Field> | |
<label>Vendor ID</label> | |
<input readOnly | |
value={device.vendorId} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Product ID</label> | |
<input readOnly | |
value={device.productId} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Device Name</label> | |
<input readOnly | |
value={device.deviceName} /> | |
</Form.Field> | |
</Form.Group> | |
<Form.Group widths='equal'> | |
<Form.Field> | |
<label>Manufacturer</label> | |
<input readOnly | |
value={device.manufacturer} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Serial Number</label> | |
<input readOnly | |
value={device.serialNumber} /> | |
</Form.Field> | |
</Form.Group> | |
<Form.Field> | |
<label>Security Key</label> | |
<Input readOnly | |
value={user.uguardKeys[0]} | |
loading={this.state.checkUguardKeyUniqueState.isInProgress()} | |
icon={this.state.checkUguardKeyUniqueState.isSuccessful() | |
? this.state.checkUguardKeyUniqueState.value.unique | |
? { name: 'check circle', color: 'green' } | |
: { | |
name: 'cancel', color: 'red', link: true, | |
onClick: () => this.props.notifications(). | |
addFromAsyncError(new AsyncError('API_ERROR', 'WARNING', | |
'UGUARD_KEY_ALREADY_EXISTS')) | |
} | |
: null} /> | |
</Form.Field> | |
<Header className='user-info'> | |
User Info | |
{ | |
user.id | |
? <Icon link | |
name='user cancel' | |
color='blue' | |
size='mini' | |
onClick={() => { | |
user.id = null | |
user.name = '' | |
user.phone = '' | |
user.activeUntil = moment().add('year', 1).toDate() | |
user.description = '' | |
user.uguardKeys.splice(1) | |
this.setState({}) | |
}} /> | |
: <Icon link | |
name='user' | |
color='blue' | |
size='mini' | |
onClick={() => { | |
this.setState({ pickUserMode: user }) | |
this.props.notifications().add({ | |
message: 'Pick the user', | |
level: 'info', | |
}) | |
}} /> | |
} | |
</Header> | |
<Form.Field> | |
<label>User name</label> | |
<Form.Input autoFocus | |
value={user.name || ''} | |
disabled={user.id != undefined} | |
onChange={(e, { value }) => this.setState(s => ({ | |
newUser: { | |
...s.newUser, | |
name: value, | |
}, | |
}))} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Phone number</label> | |
<Form.Input | |
value={user.phone || ''} | |
disabled={user.id != undefined} | |
onChange={(e, { value }) => this.setState(s => ({ | |
newUser: { | |
...s.newUser, | |
phone: value, | |
}, | |
}))} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Active Until</label> | |
<Form.Input type='date' | |
value={user.activeUntil ? | |
moment(user.activeUntil).format('YYYY-MM-DD') : ''} | |
disabled={user.id != undefined} | |
onChange={(e, { value }) => { | |
const activeUntil = moment(value).toDate() | |
this.setState(s => ({ | |
newUser: { | |
...s.newUser, | |
activeUntil, | |
}, | |
})) | |
}} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Description</label> | |
<Form.Input | |
value={user.description || ''} | |
disabled={user.id != undefined} | |
onChange={(e, { value }) => this.setState(s => ({ | |
newUser: { | |
...s.newUser, | |
description: value, | |
}, | |
}))} /> | |
</Form.Field> | |
</Form> | |
</Modal.Content> | |
<Modal.Actions> | |
<Button color='green' | |
loading={this.state.saveUserState.isInProgress()} | |
onClick={() => this.saveUser(user, true)}> | |
<Icon name='checkmark' /> | |
Create | |
</Button> | |
</Modal.Actions> | |
</Modal> | |
) | |
} | |
private renderEditUserModal() { | |
const user = this.state.editUser | |
return ( | |
<Modal size='small' centered={false} | |
open={this.state.editUser != null} | |
onClose={() => this.setState({ editUser: null })} | |
closeOnDimmerClick={false} | |
closeOnDocumentClick={false} | |
closeIcon> | |
<Header icon='key' content='Edit UGuard Key' /> | |
<Modal.Content> | |
<Form size='small' > | |
<Form.Field> | |
<label>ID</label> | |
<input readOnly | |
value={user.id} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Phone</label> | |
<Form.Input required | |
value={user.phone} | |
onChange={(e, { value }) => this.setState(s => ({ | |
editUser: { | |
...s.editUser, | |
phone: value, | |
}, | |
}))} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Active Until</label> | |
<Form.Input type='date' | |
value={user.activeUntil ? moment(user.activeUntil).format('YYYY-MM-DD') : ''} | |
onChange={(e, { value }) => { | |
const activeUntil = moment(value).toDate() | |
this.setState(s => ({ | |
editUser: { | |
...s.editUser, | |
activeUntil, | |
}, | |
})) | |
}} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Description</label> | |
<Form.Input autoFocus | |
value={user.description || ''} | |
onChange={(e, { value }) => this.setState(s => ({ | |
editUser: { | |
...s.editUser, | |
description: value, | |
}, | |
}))} /> | |
</Form.Field> | |
<Form.Field> | |
<label>Security Keys</label> | |
{user.uguardKeys.map((uguardKey, idx) => ( | |
<Form.Input required | |
key={'uguardKey-' + idx} | |
value={uguardKey} | |
icon={ | |
user.uguardKeys.length > 1 | |
? <Icon name='trash' link | |
onClick={() => { | |
const editUser = { ...user } | |
editUser.uguardKeys = user.uguardKeys.slice() | |
editUser.uguardKeys.splice(idx, 1) | |
this.setState({ editUser }) | |
}} /> | |
: null | |
} | |
onChange={(e, { value }) => { | |
const editUser = { ...user } | |
editUser.uguardKeys = user.uguardKeys.slice() | |
editUser.uguardKeys[idx] = value | |
this.setState({ editUser }) | |
}} /> | |
))} | |
</Form.Field> | |
</Form> | |
</Modal.Content> | |
<Modal.Actions> | |
<Button color='green' | |
loading={this.state.saveUserState.isInProgress()} | |
onClick={() => this.saveUser(user, false)}> | |
<Icon name='checkmark' /> | |
Save | |
</Button> | |
</Modal.Actions> | |
</Modal> | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment