React App

This commit is contained in:
Björn Dahlgren 2021-07-18 18:49:08 +02:00
parent 7b6d0c6425
commit 2a3814c138
16 changed files with 471 additions and 11 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["react", "es2015"]
}

5
app.js
View File

@ -75,4 +75,9 @@ if (require.main === module) {
server.listen(config.port, config.host)
}
// Serve main HTML file for all other requests
app.get('*', function (req, res) {
res.sendFile(path.join(__dirname, 'public', 'index.html'))
})
module.exports = app

View File

@ -13,17 +13,23 @@
},
"standard": {
"env": [
"browser",
"mocha"
]
],
"parser": "babel-eslint"
},
"dependencies": {
"arma-server": "0.0.10",
"async": "^0.9.0",
"babel-core": "^6.26.3",
"babel-loader": "^6.4.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"backbone": "1.3.3",
"backbone.bootstrap-modal": "https://github.com/powmedia/backbone.bootstrap-modal/archive/632210077c2424be2ee6ea2aafe0d3fe016ae524.tar.gz",
"backbone.marionette": "2.4.7",
"body-parser": "^1.17.1",
"bootstrap": "^3.4.0",
"bootstrap": "^4.1.3",
"css-loader": "0.17.0",
"express": "^4.15.2",
"express-basic-auth": "^1.0.1",
@ -39,6 +45,11 @@
"morgan": "^1.8.1",
"multer": "^1.3.0",
"raw-loader": "^0.5.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"reactstrap": "^8.9.0",
"serve-static": "^1.12.1",
"slugify": "^1.1.0",
"socket.io": "2.1.1",
@ -53,6 +64,7 @@
"winser": "^1.0.2"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
"mocha": "^5.2.0",
"should": "^13.2.3",
"standard": "^14.3.3",

View File

@ -5,14 +5,11 @@
<title>Arma Server Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/favicon.ico" />
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div id="content"></div>
<script src="/socket.io/socket.io.js"></script>
<script src="bundle.js"></script>
</head>
<body></body>
<script src="/bundle.js"></script>
</body>
</html>

41
react/App.jsx Normal file
View File

@ -0,0 +1,41 @@
import React, { Component } from 'react'
import Navigation from './Navigation.jsx'
import MissionsContext from './contexts/Missions'
import ModsContext from './contexts/Mods'
import ServersContext from './contexts/Servers'
export default class App extends Component {
constructor (props) {
super(props)
this.state = {
missions: [],
mods: [],
servers: []
}
}
componentDidMount () {
/* global io */
const socket = io.connect()
socket.on('missions', missions => this.setState({ missions }))
socket.on('mods', mods => this.setState({ mods }))
socket.on('servers', servers => this.setState({ servers }))
}
render () {
return (
<div id='app'>
<Navigation />
<MissionsContext.Provider value={this.state.missions}>
<ModsContext.Provider value={this.state.mods}>
<ServersContext.Provider value={this.state.servers}>
{this.props.children}
</ServersContext.Provider>
</ModsContext.Provider>
</MissionsContext.Provider>
</div>
)
}
};

53
react/Navigation.jsx Normal file
View File

@ -0,0 +1,53 @@
import React, { Component } from 'react'
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
import { NavLink as RRNavLink } from 'react-router-dom'
export default class Navigation extends Component {
constructor (props) {
super(props)
this.handleNavbarToggle = this.handleNavbarToggle.bind(this)
this.state = {
isOpen: false
}
}
handleNavbarToggle () {
this.setState({
isOpen: !this.state.isOpen
})
}
render () {
return (
<Navbar color='light' light expand='md'>
<NavbarBrand>Arma Admin</NavbarBrand>
<NavbarToggler onClick={this.handleNavbarToggle} />
<Collapse isOpen={this.state.isOpen} navbar>
<Nav className='ml-auto' navbar>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} to='/logs'>
Logs
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} to='/missions'>
Missions
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} to='/mods'>
Mods
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} to='/servers'>
Servers
</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>
)
}
}

View File

@ -0,0 +1,3 @@
import React from 'react'
export default React.createContext([])

3
react/contexts/Mods.js Normal file
View File

@ -0,0 +1,3 @@
import React from 'react'
export default React.createContext([])

View File

@ -0,0 +1,3 @@
import React from 'react'
export default React.createContext([])

25
react/index.jsx Normal file
View File

@ -0,0 +1,25 @@
import 'bootstrap/dist/css/bootstrap.css'
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'
import App from './App.jsx'
import Logs from './pages/Logs.jsx'
import Missions from './pages/Missions.jsx'
import Mods from './pages/Mods.jsx'
import Server from './pages/Server.jsx'
import Servers from './pages/Servers.jsx'
render((
<BrowserRouter>
<App>
<Switch>
<Route exact path='/logs' component={Logs} />
<Route exact path='/missions' component={Missions} />
<Route exact path='/mods' component={Mods} />
<Route exact path='/servers' component={Servers} />
<Route path='/servers/:id' component={Server} />
<Redirect path='*' to='/servers' />
</Switch>
</App>
</BrowserRouter>
), document.getElementById('content'))

49
react/pages/Logs.jsx Normal file
View File

@ -0,0 +1,49 @@
import React, { Component } from 'react'
import { Table } from 'reactstrap'
export default class Logs extends Component {
constructor (props) {
super(props)
this.state = { logs: [] }
}
componentDidMount () {
this.requestLogs()
}
requestLogs () {
fetch('/api/logs')
.then(response => response.json())
.then(data => this.setState({ logs: data }))
}
renderRow (log) {
return (
<tr key={log.name}>
<td>{log.name}</td>
<td>{log.formattedSize}</td>
<td>{new Date(log.created).toLocaleString()}</td>
<td>{new Date(log.modified).toLocaleString()}</td>
</tr>
)
}
render () {
return (
<Table>
<thead>
<tr>
<th>Mission</th>
<th>Size</th>
<th>Created</th>
<th>Updated</th>
<th />
</tr>
</thead>
<tbody>
{this.state.logs.map(this.renderRow)}
</tbody>
</Table>
)
}
}

38
react/pages/Missions.jsx Normal file
View File

@ -0,0 +1,38 @@
import React, { Component } from 'react'
import { Table } from 'reactstrap'
import MissionsContext from '../contexts/Missions'
export default class Missions extends Component {
renderRow (mission) {
return (
<tr key={mission.name}>
<td>{mission.name}</td>
<td>{mission.sizeFormatted}</td>
<td>{mission.dateModified}</td>
</tr>
)
}
render () {
return (
<Table>
<thead>
<tr>
<th>Mission</th>
<th>Size</th>
<th>Updated</th>
<th />
</tr>
</thead>
<tbody>
<MissionsContext.Consumer>
{missions => (
missions.map(this.renderRow)
)}
</MissionsContext.Consumer>
</tbody>
</Table>
)
}
}

34
react/pages/Mods.jsx Normal file
View File

@ -0,0 +1,34 @@
import React, { Component } from 'react'
import { Table } from 'reactstrap'
import ModsContext from '../contexts/Mods'
export default class Mods extends Component {
renderRow (mod) {
return (
<tr key={mod.name}>
<td>{mod.name}</td>
</tr>
)
}
render () {
return (
<Table>
<thead>
<tr>
<th>Mod</th>
<th />
</tr>
</thead>
<tbody>
<ModsContext.Consumer>
{mods => (
mods.map(this.renderRow)
)}
</ModsContext.Consumer>
</tbody>
</Table>
)
}
}

112
react/pages/Server.jsx Normal file
View File

@ -0,0 +1,112 @@
import React, { Component } from 'react'
import { Nav, NavItem, NavLink } from 'reactstrap'
import { Route, Switch } from 'react-router'
import { NavLink as RRNavLink } from 'react-router-dom'
import ServersContext from '../contexts/Servers'
class ServerInfo extends Component {
render () {
return 'ServerInfo'
}
}
class ServerMissions extends Component {
render () {
return 'ServerMissions'
}
}
class ServerMods extends Component {
render () {
return 'ServerMods'
}
}
class ServerPlayers extends Component {
render () {
return 'ServerPlayers'
}
}
class ServerSettings extends Component {
render () {
return 'ServerSettings'
}
}
export default class Server extends Component {
renderServer (server) {
const { path, url } = this.props.match
return (
<>
<Nav tabs>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} exact to={`${url}`}>
Info
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} exact to={`${url}/mods`}>
Mods
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} exact to={`${url}/missions`}>
Missions
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} exact to={`${url}/players`}>
Players
</NavLink>
</NavItem>
<NavItem>
<NavLink activeClassName='active' tag={RRNavLink} exact to={`${url}/settings`}>
Settings
</NavLink>
</NavItem>
</Nav>
<Switch>
<Route exact path={`${path}`}>
<ServerInfo />
</Route>
<Route exact path={`${path}/missions`}>
<ServerMissions />
</Route>
<Route exact path={`${path}/mods`}>
<ServerMods />
</Route>
<Route exact path={`${path}/players`}>
<ServerPlayers />
</Route>
<Route exact path={`${path}/settings`}>
<ServerSettings />
</Route>
</Switch>
</>
)
}
renderNotFound () {
return 'Not Found'
}
render () {
const serverId = this.props.match.params.id
return (
<ServersContext.Consumer>
{servers => {
const server = servers.find(server => server.id === serverId)
if (server) {
return this.renderServer(server)
}
return this.renderNotFound()
}}
</ServersContext.Consumer>
)
}
}

81
react/pages/Servers.jsx Normal file
View File

@ -0,0 +1,81 @@
import React, { Component } from 'react'
import { Badge, Button, Table } from 'reactstrap'
import { Link } from 'react-router-dom'
import ServersContext from '../contexts/Servers'
export default class Servers extends Component {
deleteServer (server) {
fetch('/api/servers/' + server.id + '', { method: 'DELETE' })
}
startServer (server) {
fetch('/api/servers/' + server.id + '/start', { method: 'POST' })
}
stopServer (server) {
fetch('/api/servers/' + server.id + '/stop', { method: 'POST' })
}
renderRow (server) {
return (
<tr key={server.id}>
<td>{this.renderStatus(server)}</td>
<td>{server.port}</td>
<td><Link to={`/servers/${server.id}`}>{server.title}</Link></td>
<td><Button color='danger' size='sm' onClick={this.deleteServer.bind(this, server)}>Delete</Button></td>
</tr>
)
}
renderStatus (server) {
if (server.pid) {
const button = <Button color='primary' size='xs' onClick={this.stopServer.bind(this, server)}>Stop</Button>
if (server.state) {
return (
<>
<Badge color='success'>Online {(server.state.players || []).length} / {server.state.maxplayers}</Badge>
{button}
</>
)
}
return (
<>
<Badge color='info'>Launching</Badge>
{button}
</>
)
}
return (
<>
<Badge color='secondary'>Offline</Badge>
<Button color='primary' size='sm' onClick={this.startServer.bind(this, server)}>Start</Button>
</>
)
}
render () {
return (
<Table>
<thead>
<tr>
<th>Status</th>
<th>Port</th>
<th>Title</th>
<th />
</tr>
</thead>
<tbody>
<ServersContext.Consumer>
{servers => (
servers.map(server => this.renderRow(server))
)}
</ServersContext.Consumer>
</tbody>
</Table>
)
}
}

View File

@ -3,7 +3,7 @@ var webpack = require('webpack')
module.exports = {
// Entry point for static analyzer
entry: path.join(__dirname, 'public', 'js', 'app.js'),
entry: path.join(__dirname, 'react', 'index.jsx'),
output: {
// Where to build results
@ -36,6 +36,7 @@ module.exports = {
module: {
loaders: [
{ test: /\.js/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.css$/, loaders: ['style-loader', 'css-loader'] },
{ test: /\.html$/, loaders: ['raw-loader'] },
{ test: /\.json$/, loaders: ['json-loader'] },