This commit is contained in:
Jan Naahs 2020-08-02 20:22:53 +02:00
parent 12509e2bd1
commit 738bdf4f99
55 changed files with 336 additions and 3952 deletions

View File

@ -6,11 +6,15 @@ import {Redirect, Route, Switch, useHistory} from "react-router";
import Controls from "./views/Controls";
import {BrowserRouter} from "react-router-dom";
import Logs from "./views/Logs";
import Saves from "./views/Saves";
import Saves from "./views/Saves/Saves";
import Layout from "./components/Layout";
import server from "../api/resources/server";
import Mods from "./views/Mods";
import UserManagement from "./views/UserManagment";
import UserManagement from "./views/UserManagement/UserManagment";
import ServerSettings from "./views/ServerSettings";
import GameSettings from "./views/GameSettings";
import Console from "./views/Console";
import Help from "./views/Help";
const App = () => {
@ -51,7 +55,7 @@ const App = () => {
), [isAuthenticated, serverStatus]);
return (
<BrowserRouter>
<BrowserRouter basename="/rework">
<Switch>
<Route path="/login" render={() => (<Login handleLogin={handleAuthenticationStatus}/>)}/>
@ -59,12 +63,12 @@ const App = () => {
<ProtectedRoute exact path="/" component={Controls}/>
<ProtectedRoute path="/saves" component={Saves}/>
<ProtectedRoute path="/mods" component={Mods}/>
<ProtectedRoute path="/server-settings" component={Controls}/>
<ProtectedRoute path="/game-settings" component={Controls}/>
<ProtectedRoute path="/console" component={Controls}/>
<ProtectedRoute path="/server-settings" component={ServerSettings}/>
<ProtectedRoute path="/game-settings" component={GameSettings}/>
<ProtectedRoute path="/console" component={Console}/>
<ProtectedRoute path="/logs" component={Logs}/>
<ProtectedRoute path="/user-management" component={UserManagement}/>
<ProtectedRoute path="/help" component={Controls}/>
<ProtectedRoute path="/help" component={Help}/>
</Layout>
</Switch>
</BrowserRouter>

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class Settings extends React.Component {
constructor(props) {
super(props)
}
render() {
return(
<tbody>
{Object.keys(this.props.config).map(function(key) {
return(
<tr key={key}>
<td>{key}</td>
<td>{this.props.config[key]}</td>
</tr>
)
}, this)}
</tbody>
)
}
}
Settings.propTypes = {
section: PropTypes.string.isRequired,
config: PropTypes.object.isRequired,
}
export default Settings

View File

@ -1,362 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import Settings from './Config/Settings.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
//https://stackoverflow.com/a/1414175
function stringToBoolean(string) {
switch(string.toLowerCase().trim()) {
case "true":
case "yes":
case "1":
return true;
case "false":
case "no":
case "0":
case null:
return false;
default:
return Boolean(string);
}
}
class ConfigContent extends React.Component {
constructor(props) {
super(props);
this.getConfig = this.getConfig.bind(this);
this.getServerSettings = this.getServerSettings.bind(this);
this.updateServerSettings = this.updateServerSettings.bind(this);
this.handleServerSettingsChange = this.handleServerSettingsChange.bind(this);
this.formTypeField = this.formTypeField.bind(this);
this.capitalizeFirstLetter = this.capitalizeFirstLetter.bind(this)
this.state = {
config: {},
serverSettings: {}
}
}
componentDidMount() {
this.getConfig();
this.getServerSettings();
}
capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
handleServerSettingsChange(name, e) {
let fieldValue
var change = this.state.serverSettings;
// if true, false and something else can be used, dont switch to boolean type!
if(e.target.id == "allow_commands") {
fieldValue = e.target.value;
} else if(e.target.value === "true" || e.target.value === "false") {
// Ensure Boolean type is used if required
if(e.target.id === "lan" || e.target.id === "public") {
if(e.target.value == "true") {
fieldValue = true
} else {
fieldValue = false
}
change["visibility"][e.target.id] = fieldValue
this.setState({serverSettings: change});
return;
}
fieldValue = stringToBoolean(e.target.value)
} else if(e.target.id === "admins" || e.target.id === "tags") {
// Split settings values that are stored as arrays
fieldValue = e.target.value.split(",")
} else {
fieldValue = e.target.value
}
change[name] = fieldValue;
this.setState({serverSettings: change});
}
getConfig() {
$.ajax({
url: "/api/config",
dataType: "json",
success: (resp) => {
if(resp.success === true) {
this.setState({config: resp.data})
}
},
error: (xhr, status, err) => {
console.log('/api/config/get', status, err.toString());
}
});
}
getServerSettings() {
$.ajax({
url: "/api/settings",
dataType: "json",
success: (resp) => {
if(resp.success === true) {
this.setState({serverSettings: resp.data})
console.log(this.state)
}
},
error: (xhr, status, err) => {
console.log('/api/settings/get', status, err.toString());
}
});
}
updateServerSettings(e) {
e.preventDefault();
var serverSettingsJSON = JSON.stringify(this.state.serverSettings)
$.ajax({
url: "/api/settings/update",
datatype: "json",
type: "POST",
data: serverSettingsJSON,
success: (data) => {
console.log(data);
if(data.success === true) {
console.log("settings updated")
}
}
})
}
formTypeField(key, setting) {
if(key.startsWith("_comment_")) {
return (
<input
key={key}
ref={key}
id={key}
defaultValue={setting}
type="hidden"
/>
)
}
switch(typeof setting) {
case "number":
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="number"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
case "string":
if(key.includes("password")) {
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="password"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
} else {
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="text"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
}
case "boolean":
return (
<select
ref={key}
id={key}
className="form-control"
onChange={this.handleServerSettingsChange.bind(this, key)}
>
<option value={true}>True</option>
<option value={false}>False</option>
</select>
)
case "object":
if(Array.isArray(setting)) {
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="text"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
} else {
if(key.includes("visibility")) {
let vis_fields = []
for(const key in setting) {
const field =
<div key={key}>
<p>{key}</p>
<select
label={key}
ref={key}
id={key}
className="form-control"
onChange={this.handleServerSettingsChange.bind(this, key)}
value={setting[key]}
>
<option value={true}>True</option>
<option value={false}>False</option>
</select>
</div>
vis_fields.push(field)
}
return vis_fields
}
}
default:
return (
<input
ref={key}
id={key}
className="form-control"
defaultValue={setting}
type="text"
onChange={this.handleServerSettingsChange.bind(this, key)}
/>
)
}
}
render() {
return (
<div className="content-wrapper">
<section className="content-header">
<h1>
Config
<small>Manage game configuration</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="cogs"/>Game configurations
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<div className="box">
<div className="box-header">
<h3 className="box-title">Server Settings</h3>
</div>
<div className="box-body">
<div className="row">
<div className="col-md-10">
<div className="server-settings-section">
<div className="table-responsive">
<form ref="settingsForm"
className="form-horizontal"
onSubmit={this.updateServerSettings}
>
{
Object.keys(this.state.serverSettings).map(function(key) {
if(key.startsWith("_comment_")) {
return (
<div key={key}>
{this.formTypeField(key, setting)}
</div>
);
}
var setting = this.state.serverSettings[key]
var setting_key = this.capitalizeFirstLetter(key.replace(/_/g, " "))
var comment = this.state.serverSettings["_comment_" + key]
return (
<div className="form-group" key={key}>
<label htmlFor={key}
className="control-label col-md-3"
>
{setting_key}
</label>
<div className="col-md-6">
{
this.formTypeField(key, setting)
}
<p className="help-block">{comment}</p>
</div>
</div>
)
}, this)
}
<div className="col-xs-6">
<div className="form-group">
<input className="form-control btn btn-success" type="submit"
ref="button" value="Update Settings"/>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="content">
<div className="box">
<div className="box-header">
<h3 className="box-title">Game Configuration</h3>
</div>
<div className="box-body">
<div className="row">
<div className="col-md-10">
{Object.keys(this.state.config).map(function(key) {
var conf = this.state.config[key]
return (
<div className="settings-section" key={key}>
<h3>{key}</h3>
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Setting name</th>
<th>Setting value</th>
</tr>
</thead>
<Settings
section={key}
config={conf}
/>
</table>
</div>
</div>
)
}, this)}
</div>
</div>
</div>
</div>
</section>
</div>
)
}
}
export default ConfigContent

View File

@ -1,133 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import PropTypes from 'prop-types';
import FontAwesomeIcon from "./FontAwesomeIcon";
class ConsoleContent extends React.Component {
constructor(props) {
super(props);
this.componentDidMount = this.componentDidMount.bind(this);
this.handleInput = this.handleInput.bind(this);
this.clearInput = this.clearInput.bind(this);
this.clearHistory = this.clearHistory.bind(this);
this.addHistory = this.addHistory.bind(this);
this.handleClick = this.handleClick.bind(this);
this.newLogLine = this.newLogLine.bind(this);
this.subscribeLogToSocket = this.subscribeLogToSocket.bind(this);
this.state = {
commands: {},
history: [],
prompt: '$ ',
}
}
componentDidMount() {
this.subscribeLogToSocket();
}
subscribeLogToSocket() {
let wsReadyState = this.props.socket.emit("log subscribe");
if(wsReadyState != WebSocket.OPEN) {
setTimeout(() => {
this.subscribeLogToSocket();
}, 50);
return;
}
this.setState({connected: true});
this.props.socket.on('log update', this.newLogLine.bind(this));
}
componentDidUpdate() {
var el = this.refs.output;
var container = document.getElementById("console-output");
container.scrollTop = this.refs.output.scrollHeight;
}
handleInput(e) {
if (e.key === "Enter") {
var input_text = this.refs.term.value;
this.props.socket.emit("command send", input_text);
this.addHistory(this.state.prompt + " " + input_text);
this.clearInput();
}
}
clearInput() {
this.refs.term.value = "";
}
clearHistory() {
ths.setState({ history: [] });
}
addHistory(output) {
var history = this.state.history;
history.push(output);
this.setState({
'history': history
});
}
handleClick() {
var term = this.refs.term;
term.focus();
}
newLogLine(logline) {
var history = this.state.history;
history.push(logline);
this.setState({
'history': history
});
}
render() {
var output = this.state.history.map((op, i) => {
return <p key={i}>{op}</p>
});
return(
<div className="content-wrapper">
<section className="content-header">
<h1>
Server Console
<small>Send commands and messages to the Factorio server</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="terminal"/>Console
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<div className="console-box" >
<div id='console-output' className='console-container' onClick={this.handleClick} ref="output">
{output}
</div>
<p>
<span className="console-prompt-box">{this.state.prompt}
<input type="text" onKeyPress={this.handleInput} ref="term" /></span>
</p>
</div>
</section>
</div>
);
}
}
ConsoleContent.propTypes = {
socket: PropTypes.object.isRequired,
}
export default ConsoleContent;

View File

@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
class FontAwesomeIcon extends React.Component {
constructor(props) {
super(props);
}
render() {
let classes = classNames(this.props.prefix, {
"fas": !this.props.prefix,
}, 'fa-' + this.props.icon, this.props.className);
return (
<i className={classes}></i>
);
}
}
FontAwesomeIcon.propTypes = {
icon: PropTypes.string.isRequired,
prefix: PropTypes.string,
className: PropTypes.string
};
export default FontAwesomeIcon;

View File

@ -1,15 +0,0 @@
import React from 'react';
class Footer extends React.Component {
render() {
return(
<footer className="main-footer">
<div className="pull-right hidden-xs">
</div>
<strong>Copyright &copy; 2019 <a href="https://github.com/MajorMJR/factorio-server-manager">Mitch Roote</a>.</strong> MIT License.
</footer>
)
}
}
export default Footer

View File

@ -1,66 +0,0 @@
import React from 'react';
import {Link, withRouter} from 'react-router-dom';
import PropTypes from 'prop-types';
import FontAwesomeIcon from "./FontAwesomeIcon";
class Header extends React.Component {
constructor(props) {
super(props);
this.onLogout = this.onLogout.bind(this);
}
onLogout(e) {
e.preventDefault();
$.ajax({
url: "/api/logout",
dataType: "json",
success: (resp) => {
console.log(resp)
}
});
// Wait for 1 second for logout callback to complete
setTimeout(() => {
this.props.history.push("/login")
}, 1000);
}
render() {
var loginMenu;
if (this.props.loggedIn) {
loginMenu =
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<Link className="nav-link" to="/settings">
<FontAwesomeIcon icon="cogs" className="fa-fw"/>Settings
</Link>
</li>
<li className="nav-item">
<a href="javascript:void(0)" onClick={this.onLogout} className="nav-link">
<FontAwesomeIcon icon="lock" className="fa-fw"/>Logout
</a>
</li>
</ul>
}
return(
<nav className="main-header navbar navbar-expand navbar-light border-bottom">
<ul className="navbar-nav">
<li className="nav-item">
<a className="nav-link" data-widget="pushmenu" href="#">
<FontAwesomeIcon icon="bars"/>
</a>
</li>
</ul>
{loginMenu}
</nav>
)
}
}
Header.propTypes = {
username: PropTypes.string.isRequired,
loggedIn: PropTypes.bool.isRequired,
}
export default withRouter(Header);

View File

@ -1,59 +0,0 @@
import React from 'react';
import ServerCtl from './ServerCtl/ServerCtl.jsx';
import ServerStatus from './ServerCtl/ServerStatus.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
class Index extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.facServStatus();
this.props.getSaves();
this.props.getStatus();
}
componentWillUnmount() {
this.props.facServStatus();
}
render() {
return(
<div className="content-wrapper" style={{height: "100%"}}>
<section className="content-header" style={{height: "100%"}}>
<h1>
Factorio Server
<small>Control your Factorio server</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="tachometer-alt"/>Server Control
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<ServerStatus
serverStatus={this.props.serverStatus}
facServStatus={this.props.facServStatus}
getStatus={this.props.getStatus}
/>
<ServerCtl
getStatus={this.props.getStatus}
saves={this.props.saves}
getSaves={this.props.getSaves}
serverStatus={this.props.serverStatus}
facServStatus={this.props.facServStatus}
/>
</section>
</div>
)
}
}
export default Index

View File

@ -1,7 +1,7 @@
import React, {useEffect} from "react";
import server from "../../api/resources/server";
import {NavLink} from "react-router-dom";
import Button from "../elements/Button";
import Button from "./Button";
const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {

View File

@ -1,79 +0,0 @@
import React from 'react';
import {withRouter} from 'react-router-dom';
import FontAwesomeIcon from "./FontAwesomeIcon";
class LoginContent extends React.Component {
constructor(props) {
super(props);
this.loginUser = this.loginUser.bind(this);
}
componentDidMount() {}
loginUser(e) {
e.preventDefault();
let user = {
username: this.refs.username.value,
password: this.refs.password.value,
};
$.ajax({
type: "POST",
url: "/api/login",
dataType: "json",
data: JSON.stringify(user),
success: (resp) => {
console.log(resp);
this.props.history.push("/");
}
});
}
render() {
return(
<div className="container" id="login">
<div className="d-flex justify-content-center h-100">
<div className="card">
<div className="card-header">
<h1>
<img src="./images/factorio.jpg" className="img-circle" alt="User Image"/>
Factorio Server Manager
</h1>
</div>
<div className="car-body">
<form onSubmit={this.loginUser}>
<label className="input-group form-group">
<div className="input-group-prepend">
<span className="input-group-text">
<FontAwesomeIcon icon="user"/>
</span>
</div>
<input className="form-control" type="text" ref="username" placeholder="Username"/>
</label>
<label className="input-group form-group">
<div className="input-group-prepend">
<span className="input-group-text">
<FontAwesomeIcon icon="lock"/>
</span>
</div>
<input className="form-control" type="password" ref="password" placeholder="Password"/>
</label>
<label className="remember-me">
<input type="checkbox"/>&nbsp;
Remember me
</label>
<input type="submit" value="Sign In" className="btn btn-primary btn-block btn-flat"/>
</form>
</div>
</div>
</div>
</div>
)
}
}
export default withRouter(LoginContent);

View File

@ -1,37 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class LogLines extends React.Component {
updateLog() {
this.props.getLastLog();
}
render() {
this.props.log.reverse();
return(
<div id="logLines" className="box">
<div className="box-header">
<h3 className="box-title">Factorio Log</h3>
</div>
<div className="box-body">
<input className="btn btn-default" type='button' onClick={this.updateLog.bind(this)} value="Refresh" />
<h5>Latest log line at the top</h5>
<samp>
{this.props.log.map ( (line, i) => {
return(
<p key={i}>{line}</p>
)
})}
</samp>
</div>
</div>
)
}
}
LogLines.propTypes = {
log: PropTypes.array.isRequired,
getLastLog: PropTypes.func.isRequired
}
export default LogLines

View File

@ -1,66 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import LogLines from './Logs/LogLines.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
class LogsContent extends React.Component {
constructor(props) {
super(props);
this.componentDidMount = this.componentDidMount.bind(this);
this.getLastLog = this.getLastLog.bind(this);
this.state = {
log: []
}
}
componentDidMount() {
this.getLastLog();
}
getLastLog() {
$.ajax({
url: "/api/log/tail",
dataType: "json",
success: (data) => {
this.setState({log: data.data})
},
error: (xhr, status, err) => {
console.log('api/mods/list', status, err.toString());
}
})
}
render() {
return (
<div className="content-wrapper">
<section className="content-header">
<h1>
Logs
<small>Analyze Factorio Logs</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="file-alt" prefix="far"/>Logs
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<LogLines
getLastLog={this.getLastLog}
{...this.state}
/>
</section>
</div>
)
}
}
export default LogsContent

View File

@ -1,195 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import SemVer from 'semver';
import FontAwesomeIcon from "../FontAwesomeIcon";
class Mod extends React.Component {
constructor(props) {
super(props);
this.toggleUpdateStatus = this.toggleUpdateStatus.bind(this);
this.removeVersionAvailableStatus = this.removeVersionAvailableStatus.bind(this);
this.state = {
newVersionAvailable: false,
updateInProgress: false
}
this.versionAjax = {};
}
componentDidMount() {
this.checkForNewVersion();
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.mod.version != this.props.mod.version) {
this.checkForNewVersion();
}
}
componentWillUnmount() {
this.versionAjax.abort();
}
checkForNewVersion() {
//send AJAX that will check this
this.versionAjax = $.ajax({
url: "/api/mods/details",
method: "POST",
data: {
modId: this.props.mod.name
},
dataType: "JSON",
success: (data) => {
let newData = JSON.parse(data.data);
//get newest COMPATIBLE release
let newestRelease;
newData.releases.forEach((release) => {
//TODO change to info_json dependency (when mod-portal-api is working again)
if(SemVer.satisfies(this.props.factorioVersion, release.info_json.factorio_version + ".x")) {
if(!newestRelease) {
newestRelease = release;
} else if(SemVer.gt(release.version, newestRelease.version)) {
newestRelease = release;
}
}
});
if(newestRelease && newestRelease.version != this.props.mod.version) {
if(this.props.updateCountAdd)
this.props.updateCountAdd();
this.setState({
newVersionAvailable: true,
newVersion: {
downloadUrl: newestRelease.download_url,
file_name: newestRelease.file_name,
version: newestRelease.version
}
});
} else {
this.setState({
newVersionAvailable: false,
newVersion: null
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/details', status, err.toString());
}
});
}
toggleUpdateStatus() {
console.log("update Status changed");
this.setState({
updateInProgress: !this.state.updateInProgress
})
}
removeVersionAvailableStatus() {
this.setState({
newVersionAvailable: false,
newVersion: null
})
}
render() {
let modStatus;
if(this.props.mod.enabled === false) {
modStatus = <span className="badge badge-danger">Disabled</span>
} else {
modStatus = <span className="badge badge-success">Enabled</span>
}
let version;
if(this.state.newVersionAvailable) {
let faArrow;
if(SemVer.gt(this.state.newVersion.version, this.props.mod.version)) {
faArrow = "arrow-circle-up";
} else {
faArrow = "arrow-circle-down";
}
version = <span>{this.props.mod.version}
<a className="btn btn-xs btn-default update-button"
style={{
marginLeft: 10,
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
width: 30,
height: 30
}}
href="#"
onClick={(event) => {
this.state.updateInProgress && this.props.updateMod != null ? null : this.props.updateMod(event, this.toggleUpdateStatus, this.removeVersionAvailableStatus);
}}
data-download-url={this.state.newVersion.downloadUrl}
data-file-name={this.state.newVersion.file_name}
>
{
this.state.updateInProgress ?
<div className='loader' style={{width: 15, height: 15, marginRight: 0, borderWidth: 3,}}></div>
:
<FontAwesomeIcon icon={faArrow} title="Update Mod" style={{fontSize: "15pt"}}/>
}
</a>
</span>;
} else {
version = this.props.mod.version;
}
let factorioVersion;
if(!this.props.mod.compatibility) {
factorioVersion = <span style={{color: "red"}}>
{this.props.mod.factorio_version}&nbsp;&nbsp;
<sup>not compatible</sup>
</span>
} else {
factorioVersion = this.props.mod.factorio_version;
}
return(
<tr data-mod-name={this.props.mod.name}
data-file-name={this.props.mod.file_name}
data-mod-version={this.props.mod.version}
>
<td>{this.props.mod.title}</td>
<td>{modStatus}</td>
<td>{version}</td>
<td>{factorioVersion}</td>
<td>
<input className='btn btn-default btn-sm'
ref='modName'
type='submit'
value='Toggle'
onClick={(event) => this.props.toggleMod(event, this.state.updateInProgress)}
disabled={this.state.updateInProgress}
/>
<input className="btn btn-danger btn-sm"
style={{marginLeft: 25}}
ref="modName"
type="submit"
value="Delete"
onClick={(event) => this.props.deleteMod(event, this.state.updateInProgress)}
disabled={this.state.updateInProgress}
/>
</td>
</tr>
)
}
}
Mod.propTypes = {
mod: PropTypes.object.isRequired,
toggleMod: PropTypes.func.isRequired,
deleteMod: PropTypes.func.isRequired,
updateMod: PropTypes.func.isRequired,
updateCountAdd: PropTypes.func,
factorioVersion: PropTypes.string,
};
export default Mod

View File

@ -1,163 +0,0 @@
import React from 'react';
import {instanceOfModsContent} from "./ModsPropTypes";
import PropTypes from "prop-types";
import {ReactSwalNormal} from 'Utilities/customSwal';
class ModLoadSave extends React.Component {
constructor(props) {
super(props);
this.loadMods = this.loadMods.bind(this);
this.loadModsSwalHandler = this.loadModsSwalHandler.bind(this);
}
componentDidMount() {
//Load Saves
this.props.getSaves();
}
loadMods(e) {
e.preventDefault();
$.ajax({
url: "/api/mods/save/load",
method: "POST",
data: $(e.target).serialize(),
dataType: "JSON",
success: (data) => {
let checkboxes = [];
data.data.mods.forEach((mod) => {
if(mod.name == "base") return;
let singleCheckbox = <tr key={mod.name}>
<td>
{mod.name}
<input type="hidden" name="mod_name" value={mod.name}/>
</td>
<td>
{mod.version}
<input type="hidden" name="mod_version" value={mod.version}/>
</td>
</tr>
checkboxes.push(singleCheckbox);
});
if(checkboxes.length == 0) {
ReactSwalNormal.fire({
title: "No mods in this save!",
type: "error"
});
return;
}
let table = <div>
All Mods will be installed
<div style={{display: "flex", width: "100%", justifyContent: "center"}}>
<form id="swalForm">
<table>
<thead>
<tr>
<th>
Name
</th>
<th>
Version
</th>
</tr>
</thead>
<tbody>
{checkboxes}
</tbody>
</table>
</form>
</div>
</div>
ReactSwalNormal.fire({
title: "Mods to install",
html: table,
type: 'question',
showCancelButton: true,
confirmButtonText: "Download Mods!",
showLoaderOnConfirm: true,
preConfirm: this.loadModsSwalHandler
});
},
error: (jqXHR) => {
ReactSwalNormal.fire({
title: jqXHR.responseJSON.data,
html: true,
type: "error",
});
}
});
}
loadModsSwalHandler() {
$.ajax({
url: "/api/mods/install/multiple",
method: "POST",
dataType: "JSON",
data: $("#swalForm").serialize(),
success: (data) => {
ReactSwalNormal.fire({
title: "All Mods installed successfully!",
type: "success"
});
this.props.modContentClass.setState({
installedMods: data.data.mods
});
},
error: (jqXHR) => {
let json_data = JSON.parse(jqXHR.responseJSON.data);
ReactSwalNormal.fire({
title: json_data.detail,
type: "error",
});
}
})
}
render() {
let saves = [];
this.props.saves.forEach((value, index) => {
if(index != this.props.saves.length - 1) {
saves.push(
<option key={index} value={value.name}>
{value.name}
</option>
)
}
});
let classes = "box-body" + " " + this.props.className;
let ids = this.props.id;
return (
<div id={ids} className={classes}>
<form action="" onSubmit={this.loadMods}>
<div className="input-group">
<select className="custom-select form-control" name="saveFile">
{saves}
</select>
<div className="input-group-append">
<button className="btn btn-outline-secondary" type="submit">Load Mods</button>
</div>
</div>
</form>
</div>
)
}
}
ModLoadSave.propTypes = {
modContentClass: instanceOfModsContent.isRequired,
className: PropTypes.string,
id: PropTypes.string
}
export default ModLoadSave;

View File

@ -1,60 +0,0 @@
import React from "react";
import PropTypes from 'prop-types';
import Mod from "./Mod.jsx";
class ModManager extends React.Component {
constructor(props) {
super(props);
}
render() {
let classes = "box-body" + " " + this.props.className;
let ids = this.props.id;
return (
<div id={ids} className={classes}>
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Version</th>
<th>Factorio Version</th>
<th>Toggle/Remove</th>
</tr>
</thead>
<tbody>
{
(this.props.installedMods != null) ?
this.props.installedMods.map((mod, i) => {
if(mod.name !== "base")
return(
<Mod
key={mod.name}
mod={mod}
{...this.props}
/>
)
}):null
}
</tbody>
</table>
</div>
</div>
)
}
}
ModManager.propTypes = {
installedMods: PropTypes.array,
toggleMod: PropTypes.func.isRequired,
deleteMod: PropTypes.func.isRequired,
updateMod: PropTypes.func.isRequired,
updateCountAdd: PropTypes.func,
className: PropTypes.string,
id: PropTypes.string
}
export default ModManager;

View File

@ -1,219 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import NativeListener from 'react-native-listener';
import ModSearch from './search/ModSearch.jsx';
import ModUpload from "./ModUpload.jsx";
import ModManager from "./ModManager.jsx";
import ModPacks from "./packs/ModPackOverview.jsx";
import {instanceOfModsContent} from "./ModsPropTypes.js";
import ModLoadSave from "./ModLoadSave.jsx";
import {ReactSwalNormal} from 'Utilities/customSwal';
import FontAwesomeIcon from "../FontAwesomeIcon";
class ModOverview extends React.Component {
constructor(props) {
super(props);
this.handlerSearchMod = this.handlerSearchMod.bind(this);
this.state = {
shownModList: []
}
}
handlerSearchMod(e) {
e.preventDefault();
$.ajax({
url: "/api/mods/search",
method: "GET",
data: $(e.target).serialize(),
dataType: "JSON",
success: (data) => {
let parsed_data = JSON.parse(data.data);
this.setState({
"shownModList": parsed_data.results
});
},
error: (jqXHR) => {
let json_data = JSON.parse(jqXHR.responseJSON.data);
ReactSwalNormal.fire({
title: json_data.detail,
type: "error"
});
}
})
}
downloadAllHandler(e) {
e.stopPropagation();
}
render() {
return(
<div>
<div className="box collapsed-box" id="add-mod-box">
<div className="box-header"
data-toggle="collapse"
data-target="#modSearch"
aria-expanded="false"
aria-controls="modSearch"
role="button"
style={{cursor: "pointer"}}
>
<FontAwesomeIcon icon="plus"/>
<h3 className="box-title">Add Mod</h3>
{this.props.loggedIn ?
<div className="box-tools pull-right">
<NativeListener onClick={this.props.factorioLogoutHandler}>
<button className="btn btn-box-tool btn-danger" style={{color: "#fff"}}>
Logout
</button>
</NativeListener>
</div>
: null}
</div>
<ModSearch
{...this.state}
{...this.props}
className="collapse"
id="modSearch"
submitSearchMod={this.handlerSearchMod}
submitFactorioLogin={this.props.submitFactorioLogin}
/>
</div>
<div className="box collapsed-box">
<div className="box-header"
data-toggle="collapse"
data-target="#modUpload"
aria-expanded="false"
aria-controls="modUpload"
role="button"
style={{cursor: "pointer"}}
>
<FontAwesomeIcon icon="plus"/>
<h3 className="box-title">Upload Mod</h3>
</div>
<ModUpload
{...this.props}
className="collapse"
id="modUpload"
/>
</div>
<div className="box collapsed-box">
<div className="box-header"
data-toggle="collapse"
data-target="#modLoadSave"
aria-expanded="false"
aria-controls="modLoadSave"
role="button"
style={{cursor: "pointer"}}
>
<FontAwesomeIcon icon="plus"/>
<h3 className="box-title">Load Mods From Save</h3>
</div>
<ModLoadSave
{...this.props}
className="collapse"
id="modLoadSave"
/>
</div>
<div className="box" id="manage-mods">
<div className="box-header"
data-toggle="collapse"
data-target="#modManager"
aria-expanded="true"
aria-controls="modManager"
role="button"
style={{cursor: "pointer"}}
>
<FontAwesomeIcon icon="minus"/>
<h3 className="box-title">Manage Mods</h3>
<div className="box-tools float-sm-right">
{
this.props.installedMods != null ?
<NativeListener onClick={this.downloadAllHandler}>
<a className="btn btn-box-tool btn-default" style={{marginRight: 20}} href={"/api/mods/download"} download>
Download all Mods
</a>
</NativeListener>
: null
}
{
this.props.updatesAvailable > 0 ?
<NativeListener onClick={this.props.updateAllMods}>
<button className="btn btn-box-tool btn-default" style={{marginRight: 20}}>
Update all Mods
</button>
</NativeListener>
: null
}
{
this.props.installedMods != null ?
<NativeListener onClick={this.props.deleteAll}>
<button className="btn btn-box-tool btn-danger" style={{color: "#fff"}}>
Delete ALL Mods
</button>
</NativeListener>
: null
}
</div>
</div>
<ModManager
{...this.props}
id="modManager"
className="show"
/>
</div>
<div className="box collapsed-box">
<div className="box-header"
data-toggle="collapse"
data-target="#modPacks"
aria-expanded="false"
aria-controls="modPacks"
role="button"
style={{cursor: "pointer"}}
>
<FontAwesomeIcon icon="plus"/>
<h3 className="box-title">Manage Modpacks</h3>
</div>
<ModPacks
{...this.props}
className="collapse"
id="modPacks"
/>
</div>
</div>
);
}
}
ModOverview.propTypes = {
installedMods: PropTypes.array,
submitFactorioLogin: PropTypes.func.isRequired,
toggleMod: PropTypes.func.isRequired,
deleteMod: PropTypes.func.isRequired,
deleteAll: PropTypes.func.isRequired,
updateMod: PropTypes.func.isRequired,
uploadModSuccessHandler: PropTypes.func.isRequired,
loggedIn: PropTypes.bool.isRequired,
factorioLogoutHandler: PropTypes.func.isRequired,
updatesAvailable: PropTypes.number.isRequired,
updateAllMods: PropTypes.func.isRequired,
updateCountAdd: PropTypes.func.isRequired,
modContentClass: instanceOfModsContent.isRequired,
};
export default ModOverview;

View File

@ -1,48 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class ModUpload extends React.Component {
componentDidMount() {
$("#mod_upload_input").fileinput({
uploadUrl: '/api/mods/upload',
showCancel: false,
showUploadedThumbs: false,
browseOnZoneClick: true,
uploadAsync: false,
allowedFileExtensions: ['zip'],
browseLabel: "Select Mods...",
browseIcon: '<i class="fa fa-upload text-muted" style="color: white;"></i>&nbsp;',
theme: "fas",
slugCallback: function(filename) {
return filename;
},
}).on('filebatchuploadsuccess fileuploaded', this.props.uploadModSuccessHandler);
}
render() {
let classes = "box-body" + " " + this.props.className;
let ids = this.props.id;
return(
<div id={ids} className={classes}>
<div className="alert alert-warning alert-dismissible" role="alert">
<button type="button" className="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
The mods you upload will override every mod, that is already uploaded!<br/>
Uploaded mods will be treated same, as if you install mods with the mod-portal-api.<br/><br/>
Only zip-files are allowed to upload !!
</div>
<label className="control-label">Select File</label>
<input id="mod_upload_input" name="mod_file" multiple type="file" />
</div>
);
}
}
ModUpload.propTypes = {
uploadModSuccessHandler: PropTypes.func.isRequired,
className: PropTypes.string,
id: PropTypes.string
};
export default ModUpload;

View File

@ -1,22 +0,0 @@
import ModsContent from "../ModsContent.jsx";
function instanceOfModsContentFunction(isRequired) {
return function(props, propName, componentName) {
if(props[propName]) {
if(!props[propName] instanceof ModsContent) {
return new Error(propName + ' in ' + componentName + ' is not an instance of ModContent');
}
} else {
if(isRequired) {
return new Error(propName + ' in ' + componentName + ' is missing');
}
}
return null;
}
}
const instanceOfModsContent = instanceOfModsContentFunction(false);
instanceOfModsContent.isRequired = instanceOfModsContentFunction(true);
export {instanceOfModsContent};

View File

@ -1,502 +0,0 @@
import React from 'react';
import ModManager from "../ModManager.jsx";
import NativeListener from 'react-native-listener';
import {instanceOfModsContent} from "../ModsPropTypes.js";
import locks from "locks";
import PropTypes from "prop-types";
import {ReactSwalNormal, ReactSwalDanger} from 'Utilities/customSwal';
import FontAwesomeIcon from "../../FontAwesomeIcon";
class ModPackOverview extends React.Component {
constructor(props) {
super(props);
this.createModPack = this.createModPack.bind(this);
this.deleteModPack = this.deleteModPack.bind(this);
this.loadModPack = this.loadModPack.bind(this);
this.modPackToggleModHandler = this.modPackToggleModHandler.bind(this);
this.modPackDeleteModHandler = this.modPackDeleteModHandler.bind(this);
this.modPackUpdateModHandler = this.modPackUpdateModHandler.bind(this);
this.state = {
listPacks: []
}
this.mutex = locks.createMutex();
}
componentDidMount() {
this.getModPacks();
}
getModPacks() {
//send ajax to get all modPacks and setState
$.ajax({
url: "/api/mods/packs/list",
method: "GET",
dataType: "JSON",
success: (data) => {
this.setState({
listPacks: data.data.mod_packs
});
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/list', status, err.toString());
}
})
}
createModPack() {
ReactSwalNormal.fire({
title: "Create modpack",
html: "Please enter an unique modpack name:",
input: "text",
showCancelButton: true,
inputPlaceholder: "Modpack name",
inputAttributes: {
required: "required"
},
inputValidator: (value) => {
return new Promise(resolve => {
if(value) {
resolve();
} else {
resolve("You need to enter a name");
}
});
},
showLoaderOnConfirm: true,
preConfirm: (inputValue) => {
// console.log(this);
$.ajax({
url: "/api/mods/packs/create",
method: "POST",
data: {name: inputValue},
dataType: "JSON",
success: (data) => {
this.mutex.lock(() => {
let packList = this.state.listPacks;
data.data.mod_packs.forEach((v, k) => {
if(v.name == inputValue) {
packList.push(data.data.mod_packs[k]);
return false;
}
});
this.setState({
listPacks: packList
});
ReactSwalNormal.fire({
title: "modpack created successfully",
type: "success"
});
this.mutex.unlock();
});
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/create', status, err.toString());
let jsonResponse = jqXHR.responseJSON;
ReactSwalNormal.fire({
title: "Error on creating modpack",
text: jsonResponse.data,
type: "error"
});
}
});
}
});
}
deleteModPack(e) {
e.stopPropagation();
let name = $(e.target).parent().prev().html();
ReactSwalDanger.fire({
title: "Are you sure?",
html: <p>You really want to delete this modpack?<br/>There is no turning back, the modpack will be deleted forever (a very long time)!</p>,
type: "question",
showCancelButton: true,
showLoaderOnConfirm: true,
preConfirm: () => {
$.ajax({
url: "/api/mods/packs/delete",
method: "POST",
data: {name: name},
dataType: "JSON",
success: (data) => {
if(data.success) {
this.mutex.lock(() => {
let modPacks = this.state.listPacks;
modPacks.forEach((v, k) => {
if(v.name == name) {
delete modPacks[k];
}
});
this.setState({
listPacks: modPacks
});
ReactSwalNormal.fire({
title: "Modpack deleted successfully",
type: "success"
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/delete', status, err.toString());
let jsonResponse = jqXHR.responseJSON || err.toString();
jsonResponse = jsonResponse.data || err.toString();
ReactSwalNormal.fire({
title: "Error on creating modpack",
text: jsonResponse,
type: "error"
});
}
})
}
});
}
loadModPack(e) {
e.stopPropagation();
let name = $(e.target).parent().prev().html();
ReactSwalDanger.fire({
title: "Are you sure?",
text: "This operation will replace the current installed mods with the mods out of the selected ModPack!",
type: "question",
showCancelButton: true,
showLoaderOnConfirm: true,
preConfirm: () => {
$.ajax({
url: "/api/mods/packs/load",
method: "POST",
data: {name: name},
dataType: "JSON",
success: (data) => {
ReactSwalNormal.fire({
title: "ModPack loaded!",
type: "success"
});
this.props.modContentClass.setState({
installedMods: data.data.mods
});
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/load', status, err.toString());
let jsonResponse = jqXHR.responseJSON || err.toString();
jsonResponse = jsonResponse.data || err.toString();
ReactSwalNormal.fire({
title: "Error on loading ModPack",
text: jsonResponse,
type: "error"
});
}
})
}
});
}
downloadModPack(e) {
e.stopPropagation();
}
modPackToggleModHandler(e, updatesInProgress) {
e.preventDefault();
if(updatesInProgress) {
ReactSwalNormal.fire({
title: "Toggle mod failed",
text: "Can't toggle the mod, when an update is still in progress",
type: "error"
});
return false;
}
let $button = $(e.target);
let $row = $button.parents("tr");
let modName = $row.data("mod-name");
let modPackName = $row.parents(".single-modpack").find("h3").html();
$.ajax({
url: "/api/mods/packs/mod/toggle",
method: "POST",
data: {
modName: modName,
modPack: modPackName
},
dataType: "JSON",
success: (data) => {
if(data.success) {
this.mutex.lock(() => {
let packList = this.state.listPacks;
packList.forEach((modPack, modPackKey) => {
if(modPack.name == modPackName) {
packList[modPackKey].mods.mods.forEach((mod, modKey) => {
if(mod.name == modName) {
packList[modPackKey].mods.mods[modKey].enabled = data.data;
return false;
}
});
}
});
this.setState({
listPacks: packList
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/mod/toggle', status, err.toString());
ReactSwalNormal.fire({
title: "Toggle Mod went wrong",
text: err.toString(),
type: "error"
});
}
});
}
modPackDeleteModHandler(e, updatesInProgress) {
e.preventDefault();
if(updatesInProgress) {
ReactSwalNormal.fire({
title: "Delete failed",
text: "Can't delete the mod, when an update is still in progress",
type: "error"
});
return false;
}
let $button = $(e.target);
let $row = $button.parents("tr");
let modName = $row.data("mod-name");
let modPackName = $row.parents(".single-modpack").find("h3").html();
ReactSwalDanger.fire({
title: "Delete Mod?",
text: "This will delete the mod forever",
type: "question",
showCancelButton: true,
confirmButtonText: "Delete it!",
cancelButtonText: "Close",
showLoaderOnConfirm: true,
preConfirm: () => {
$.ajax({
url: "/api/mods/packs/mod/delete",
method: "POST",
data: {
modName: modName,
modPackName: modPackName
},
dataType: "JSON",
success: (data) => {
if(data.success) {
this.mutex.lock(() => {
ReactSwalNormal.fire({
title: <p>Delete of mod {modName} inside modPack {modPackName} successful</p>,
type: "success"
})
let packList = this.state.listPacks;
packList.forEach((modPack, modPackKey) => {
if(modPack.name == modPackName) {
packList[modPackKey].mods.mods.forEach((mod, modKey) => {
if(mod.name == modName) {
delete packList[modPackKey].mods.mods[modKey];
return false;
}
});
}
});
this.setState({
listPacks: packList
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/mod/delete', status, err.toString());
ReactSwalNormal.fire({
title: "Delete Mod went wrong",
text: jqXHR.responseJSON.data,
type: "error"
});
}
});
}
});
}
modPackUpdateModHandler(e, toggleUpdateStatus, removeVersionAvailableStatus) {
e.preventDefault();
if(!this.props.modContentClass.state.loggedIn) {
ReactSwalNormal.fire({
title: "Update failed",
text: "please login into Factorio to update mod",
type: "error",
});
let $addModBox = $('#add-mod-box');
if($addModBox.hasClass("collapsed-box")) {
$addModBox.find(".box-header").click();
}
} else {
let $button = $(e.currentTarget);
let download_url = $button.data("downloadUrl");
let filename = $button.data("fileName");
let $row = $button.parents("tr");
let modName = $row.data("modName");
let modPackName = $row.parents(".single-modpack").find("h3").html();
//make button spinning
toggleUpdateStatus();
$.ajax({
url: "/api/mods/packs/mod/update",
method: "POST",
data: {
downloadUrl: download_url,
filename: filename,
modName: modName,
modPackName: modPackName
},
success: (data) => {
toggleUpdateStatus();
removeVersionAvailableStatus();
if(data.success) {
this.mutex.lock(() => {
let packList = this.state.listPacks;
packList.forEach((modPack, modPackKey) => {
if(modPack.name == modPackName) {
packList[modPackKey].mods.mods.forEach((mod, modKey) => {
if(mod.name == modName) {
packList[modPackKey].mods.mods[modKey] = data.data;
return false;
}
});
}
});
this.setState({
listPacks: packList
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/packs/mod/update', status, err.toString());
toggleUpdateStatus();
ReactSwalNormal.fire({
title: "Update Mod went wrong",
text: jqXHR.responseJSON.data,
type: "error"
});
}
});
}
}
render() {
let classes = "box-body" + " " + this.props.className;
let ids = this.props.id;
return(
<div id={ids} className={classes}>
{
this.state.listPacks != null ?
this.state.listPacks.map(
(modpack, index) => {
return(
<div key={modpack.name} className="box single-modpack collapsed-box">
<div className="box-header"
data-toggle="collapse"
data-target={"#" + modpack.name}
aria-expanded="false"
aria-controls={modpack.name}
style={{cursor: "pointer"}}
>
<FontAwesomeIcon icon="plus"/>
<h3 className="box-title">{modpack.name}</h3>
<div className="box-tools pull-right">
<NativeListener onClick={this.downloadModPack}>
<a className="btn btn-box-tool btn-default" style={{marginRight: 10}} href={"/api/mods/packs/download/" + modpack.name} download>Download</a>
</NativeListener>
<NativeListener onClick={this.loadModPack}>
<button className="btn btn-box-tool btn-default" style={{marginRight: 10}}>Load ModPack</button>
</NativeListener>
<NativeListener onClick={this.deleteModPack}>
<button className="btn btn-box-tool btn-danger" style={{color: "#fff"}}>Delete</button>
</NativeListener>
</div>
</div>
<ModManager
{...this.props}
className="collapse"
id={modpack.name}
installedMods={modpack.mods.mods}
deleteMod={this.modPackDeleteModHandler}
toggleMod={this.modPackToggleModHandler}
updateMod={this.modPackUpdateModHandler}
/>
</div>
)
}
)
: null
}
<div className="box">
<div className="box-header" style={{cursor: "pointer"}} onClick={this.createModPack}>
<FontAwesomeIcon icon="plus"/>
<h3 className="box-title">Add ModPack with current installed mods</h3>
</div>
</div>
</div>
);
}
}
ModPackOverview.propTypes = {
modContentClass: instanceOfModsContent.isRequired,
className: PropTypes.string,
id: PropTypes.string
};
export default ModPackOverview

View File

@ -1,71 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class ModFoundOverview extends React.Component {
render() {
let imgStyle= {
width: 144,
height: 144,
border: "1px outset #333",
borderRadius: 2,
}
let noImgStyle = {
container: {
width: 144,
height: 144,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#333",
border: "1px inset #333",
fontSize: 20,
color: "#949391",
border: "1px outset #333",
borderRadius: 2,
},
}
let informationStyle = {
container: {
marginLeft: 20,
}
}
let mods = [];
this.props.shownModList.some((mod, index) => {
if(index == 10) return true;
let img =
(mod.first_media_file != null) ?
<img src={mod.first_media_file.urls.thumb} style={imgStyle} />
:
<div style={noImgStyle.container}>
<div>No picture</div>
</div>;
mods.push(
<div className="list-group-item" key={mod.title}>
<div style={{display: "flex"}}>
{img}
<div style={informationStyle.container}>
<h4 className="list-group-item-heading">{mod.title} <small>by {mod.owner}</small></h4>
<div className="list-group-item-text">{mod.summary}</div>
<button style={{marginTop: 10, display: "flex"}} onClick={this.props.loadDownloadList} data-mod-id={mod.name}>INSTALL</button>
</div>
</div>
</div>
);
});
return (
<div className="list-group">
{mods}
</div>
);
}
}
ModFoundOverview.propTypes = {
shownModList: PropTypes.array.isRequired,
loadDownloadList: PropTypes.func.isRequired
}
export default ModFoundOverview;

View File

@ -1,69 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class ModSearch extends React.Component {
render() {
let classes = "box-body" + " " + this.props.className;
let ids = this.props.id;
if(this.props.loggedIn) {
//TODO switch back to currently commented out code, when the mod-portal-api is back with all features!!
/*return (
<div className="box-body">
<form onSubmit={this.props.submitSearchMod}>
<div className="input-group col-lg-5">
<input type="text" className="form-control" placeholder="Search for Mod" name="search" />
<span className="input-group-btn">
<input className="btn btn-default" type="submit" value="Go!"/>
</span>
</div>
</form>
<ModFoundOverview
{...this.props}
/>
</div>
)*/
return (
<div id={ids} className={classes}>
<form onSubmit={this.props.loadDownloadList}>
<div className="input-group col-lg-5">
<input type="text" className="form-control" placeholder="Download mod by ID" name="modId" />
<span className="input-group-btn">
<input className="btn btn-default" type="submit" value="Go!"/>
</span>
</div>
</form>
</div>
)
} else {
return (
<div id={ids} className={classes}>
<form onSubmit={this.props.submitFactorioLogin}>
<h4>Login into Factorio</h4>
<div className="form-group">
<label htmlFor="factorio-account-name">Factorio Account Name:</label>
<input type="text" className="form-control" id="factorio-account-name" name="username" required />
</div>
<div className="form-group">
<label htmlFor="pwd">Password:</label>
<input type="password" className="form-control" id="pwd" name="password" required />
</div>
<input type="submit" className="btn btn-default" value="Login" />
</form>
</div>
)
}
}
}
ModSearch.propTypes = {
submitSearchMod: PropTypes.func.isRequired,
loggedIn: PropTypes.bool.isRequired,
submitFactorioLogin: PropTypes.func.isRequired,
loadDownloadList: PropTypes.func.isRequired,
className: PropTypes.string,
id: PropTypes.string
}
export default ModSearch;

View File

@ -1,582 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router-dom';
import ModOverview from './Mods/ModOverview.jsx';
import locks from "locks";
import SemVer from 'semver';
import {ReactSwalNormal, ReactSwalDanger} from 'Utilities/customSwal';
import FontAwesomeIcon from "./FontAwesomeIcon";
class ModsContent extends React.Component {
constructor(props) {
super(props);
this.componentDidMount = this.componentDidMount.bind(this);
this.loadModList = this.loadModList.bind(this);
this.handlerFactorioLogin = this.handlerFactorioLogin.bind(this);
this.loadDownloadList = this.loadDownloadList.bind(this);
this.loadDownloadListSwalHandler = this.loadDownloadListSwalHandler.bind(this);
this.toggleModHandler = this.toggleModHandler.bind(this);
this.deleteModHandler = this.deleteModHandler.bind(this);
this.updateModHandler = this.updateModHandler.bind(this);
this.uploadModSuccessHandler = this.uploadModSuccessHandler.bind(this);
this.factorioLogoutHandler = this.factorioLogoutHandler.bind(this);
this.deleteAllHandler = this.deleteAllHandler.bind(this);
this.updateAllModsHandler = this.updateAllModsHandler.bind(this);
this.updatesAvailable = this.updatesAvailable.bind(this);
this.updateCountSubtract = this.updateCountSubtract.bind(this);
this.updateCountAdd = this.updateCountAdd.bind(this);
this.state = {
loggedIn: false,
installedMods: null,
updatesAvailable: 0,
};
this.mutex = locks.createMutex();
}
componentDidMount() {
this.loadModList();
this.checkLoginState();
}
loadModList() {
$.ajax({
url: "/api/mods/list/installed",
dataType: "json",
success: (data) => {
this.setState({installedMods: data.data})
},
error: (xhr, status, err) => {
console.log('api/mods/list/installed', status, err.toString());
}
});
}
handlerFactorioLogin(e) {
e.preventDefault();
let $form = $(e.target);
$.ajax({
url: "/api/mods/factorio/login",
method: "POST",
data: $form.serialize(),
dataType: "JSON",
success: (data) => {
ReactSwalNormal.fire({
title: "Logged in Successfully",
type: "success"
});
this.setState({
"loggedIn": data.data
});
},
error: (jqXHR) => {
ReactSwalNormal.fire({
title: jqXHR.responseJSON.data,
type: "error"
});
}
});
}
checkLoginState() {
$.ajax({
url: "/api/mods/factorio/status",
method: "POST",
dataType: "json",
success: (data) => {
this.setState({
"loggedIn": data.data
})
},
error: (jqXHR) => {
let json_data = JSON.parse(jqXHR.responseJSON.data);
console.log("error checking login status", json_data)
}
})
}
factorioLogoutHandler(e) {
e.preventDefault();
e.stopPropagation();
$.ajax({
url: "/api/mods/factorio/logout",
method: "POST",
dataType: "JSON",
success: (data) => {
this.setState({
loggedIn: data.data
})
},
error: (jqXHR) => {
ReactSwalNormal.fire({
title: "error logging out of factorio",
text: jqXHR.responseJSON.data,
type: "error"
});
}
})
}
loadDownloadListSwalHandler() {
let $checkedInput = $('input[name=version]:checked');
let link = $checkedInput.data("link");
let filename = $checkedInput.data("filename");
let modName = $checkedInput.data("modid");
$.ajax({
method: "POST",
url: "/api/mods/install",
dataType: "JSON",
data: {
link: link,
filename: filename,
modName: modName
},
success: (data) => {
this.setState({
installedMods: data.data.mods
});
ReactSwalNormal.fire({
title: "Mod installed",
type: "success"
});
},
error: (jqXHR, status, err) => {
ReactSwalNormal.fire({
title: "An error occurred",
text: err.toString(),
type: "error"
});
}
});
}
//TODO remove modIdInput, when the factorio-mod-portal-api is fixed
// all outcommented needs to be reimplemented, when it will work again
loadDownloadList(e) {
e.preventDefault();
// let $button = $(e.target);
// let $loader = $("<div class='loader'></div>");
// $button.prepend($loader);
let modId = $(e.target).find("input[name=modId]").val();
// let modId = $button.data("modId");
$.ajax({
method: "POST",
url: "/api/mods/details",
data: {
modId: modId
},
dataType: "json",
success: (data) => {
// $loader.remove();
let correctData = JSON.parse(data.data);
let checkboxes = [];
let compatibleReleaseFound = false;
correctData.releases.reverse();
correctData.releases.forEach((release) => {
let incompatibleClass = "";
let isChecked = false;
if(!SemVer.satisfies(this.props.factorioVersion, release.info_json.factorio_version + ".x")) {
incompatibleClass = "incompatible";
} else if(compatibleReleaseFound == false) {
compatibleReleaseFound = true;
isChecked = true;
}
let date = new Date(release.released_at);
let singleBox = <tr className={incompatibleClass} key={release.version}>
<td>
<input type="radio"
name="version"
data-link={release.download_url}
data-filename={release.file_name}
data-modid={modId}
defaultChecked={isChecked}
/>
</td>
<td>
{release.version}
</td>
<td>
{release.info_json.factorio_version}
</td>
<td>
{date.toLocaleDateString()}
</td>
{/*<td>*/}
{/*{release.downloads_count}*/}
{/*</td>*/}
</tr>;
checkboxes.push(singleBox);
});
let table = <table>
<thead>
<tr>
<th></th>
<th>
Version
</th>
<th>
Game Version
</th>
<th>
Release Date
</th>
{/*<th>*/}
{/*Downloads*/}
{/*</th>*/}
</tr>
</thead>
<tbody>
{checkboxes}
</tbody>
</table>;
ReactSwalDanger.fire({
title: "Choose version",
html: table,
type: "question",
showCancelButton: true,
confirmButtonText: "Download it!",
showLoaderOnConfirm: true,
preConfirm: this.loadDownloadListSwalHandler
});
},
error: (xhr, status, err) => {
console.log('api/mods/details', status, err.toString());
// $loader.remove();
}
})
}
toggleModHandler(e, updatesInProgress) {
e.preventDefault();
if(updatesInProgress) {
ReactSwalNormal.fire({
title: "Toggle mods failed",
text: "Can't toggle the mod, when an update is still in progress",
type: "error"
});
return false;
}
let $button = $(e.target);
let $row = $button.parents("tr");
let modName = $row.data("mod-name");
$.ajax({
url: "/api/mods/toggle",
method: "POST",
data: {
modName: modName
},
dataType: "JSON",
success: (data) => {
if(data.success) {
this.mutex.lock(() => {
let installedMods = this.state.installedMods;
$.each(installedMods, (k, v) => {
if(v.name == modName) {
installedMods[k].enabled = data.data;
}
});
this.setState({
installedMods: installedMods
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/toggle', status, err.toString());
ReactSwalNormal.fire({
title: "Toggle Mod went wrong",
text: err.toString(),
type: "error"
});
}
});
}
deleteModHandler(e, updatesInProgress) {
e.preventDefault();
if(updatesInProgress) {
ReactSwalNormal.fire({
title: "Delete failed!",
text: "Can't delete the mod, when an update is still in progress",
type: "error"
});
return false;
}
let $button = $(e.target);
let $row = $button.parents("tr");
let modName = $row.data("mod-name");
ReactSwalDanger.fire({
title: "Delete Mod?",
text: "This will delete the mod and can break the save file",
type: "question",
showCancelButton: true,
confirmButtonText: "Delete it!",
showLoaderOnConfirm: true,
preConfirm: () => {
$.ajax({
url: "/api/mods/delete",
method: "POST",
data: {
modName: modName
},
dataType: "JSON",
success: (data) => {
if (data.success) {
this.mutex.lock(() => {
ReactSwalNormal.fire({
title: <p>Delete of mod {modName} successful</p>,
type: "success"
});
let installedMods = this.state.installedMods;
installedMods.forEach((v, k) => {
if (v.name == modName) {
delete installedMods[k];
}
});
this.setState({
installedMods: installedMods
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/delete', status, err.toString());
ReactSwalNormal.fire({
title: "Delete Mod went wrong",
text: err.toString(),
type: "error"
});
}
});
}
});
}
deleteAllHandler(e) {
e.preventDefault();
e.stopPropagation();
ReactSwalDanger.fire({
title: "Delete Mod?",
html: <p>This will delete ALL mods and can't be redone!<br/> Are you sure?</p>,
type: "question",
showCancelButton: true,
confirmButtonText: "Yes, Delete ALL!",
showLoaderOnConfirm: true,
preConfirm: () => {
$.ajax({
url: "/api/mods/delete/all",
method: "POST",
dataType: "JSON",
success: (data) => {
ReactSwalNormal.fire({
title: "ALL mods deleted successful",
type: "success"
})
this.setState({
installedMods: data.data
});
},
error: (jqXHR, status, err) => {
console.log('api/mods/delete/all', status, err.toString());
ReactSwalNormal.fire({
title: "Delete all mods went wrong",
html: <p>{err.toString()}<br/>{jqXHR.responseJSON.data}</p>,
type: "error",
});
}
})
}
});
}
updateModHandler(e, toggleUpdateStatus, removeVersionAvailableStatus) {
e.preventDefault();
if(!this.state.loggedIn) {
ReactSwalNormal.fire({
title: "Update failed",
text: "Please login into Factorio to update mod",
type: "error",
});
let $addModBox = $('#add-mod-box');
if($addModBox.hasClass("collapsed-box")) {
$addModBox.find(".box-header").click();
}
} else {
let $button = $(e.currentTarget);
let downloadUrl = $button.data("downloadUrl");
let filename = $button.data("fileName");
let modName = $button.parents("tr").data("modName");
//make button spinning
toggleUpdateStatus();
$.ajax({
url: "/api/mods/update",
method: "POST",
data: {
downloadUrl: downloadUrl,
filename: filename,
modName: modName,
},
success: (data) => {
toggleUpdateStatus();
removeVersionAvailableStatus();
this.updateCountSubtract();
if(data.success) {
this.mutex.lock(() => {
ReactSwalNormal.fire({
title: <p>Update of mod {modName} successful</p>,
type: "success"
})
let installedMods = this.state.installedMods;
installedMods.forEach((v, k) => {
if(v.name == modName) {
installedMods[k] = data.data;
}
});
this.setState({
installedMods: installedMods
});
this.mutex.unlock();
});
}
},
error: (jqXHR, status, err) => {
console.log('api/mods/delete', status, err.toString());
toggleUpdateStatus();
ReactSwalNormal.fire({
title: "Update Mod went wrong",
text: err.toString(),
type: "error"
});
}
});
}
}
updatesAvailable() {
this.setState({
updatesAvailable: true
});
}
updateAllModsHandler(e) {
e.preventDefault();
e.stopPropagation();
let updateButtons = $('#manage-mods').find(".update-button");
$.each(updateButtons, (k, v) => {
v.click();
});
}
updateCountSubtract() {
this.setState({
updatesAvailable: this.state.updatesAvailable > 0 ? this.state.updatesAvailable - 1 : 0
});
}
updateCountAdd() {
this.setState({
updatesAvailable: this.state.updatesAvailable + 1
});
}
uploadModSuccessHandler(event, data) {
this.setState({
installedMods: data.response.data.mods
});
}
render() {
return(
<div className="content-wrapper">
<section className="content-header">
<h1>
Mods
<small>Manage your mods</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="pencil-alt"/>Mods
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<ModOverview
{...this.state}
{...this.props}
loadDownloadList={this.loadDownloadList}
submitFactorioLogin={this.handlerFactorioLogin}
toggleMod={this.toggleModHandler}
deleteMod={this.deleteModHandler}
deleteAll={this.deleteAllHandler}
updateMod={this.updateModHandler}
updateCountAdd={this.updateCountAdd}
uploadModSuccessHandler={this.uploadModSuccessHandler}
updateAllMods={this.updateAllModsHandler}
modContentClass={this}
factorioLogoutHandler={this.factorioLogoutHandler}
/>
</section>
</div>
)
}
}
ModsContent.propTypes = {
factorioVersion: PropTypes.string,
};
export default ModsContent;

View File

@ -1,76 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import FontAwesomeIcon from "../FontAwesomeIcon";
class CreateSave extends React.Component {
constructor(props) {
super(props);
this.createSaveFile = this.createSaveFile.bind(this);
this.updateSavesList = this.updateSavesList.bind(this)
this.state = {
loading: false,
}
}
updateSavesList() {
this.props.getSaves();
}
createSaveFile(e) {
this.setState({loading: true});
$.ajax({
url: "/api/saves/create/" + this.refs.savename.value,
dataType: "json",
success: (data) => {
console.log(data);
if (data.success === true) {
alert(data.data)
this.updateSavesList();
this.setState({loading: false});
} else {
alert(data.data)
this.setState({loading: false});
}
}
})
}
render() {
var loadingOverlay
if (this.state.loading) {
loadingOverlay =
<div className="overlay">
<FontAwesomeIcon icon="refresh" className="fa-spin"/>
</div>
} else {
loadingOverlay = ""
}
return(
<div className="box" id="uploadsave">
<div className="box-header">
<h4 className="box-title">Create Save File</h4>
</div>
<div className="box-body">
<form>
<div className="form-group">
<label htmlFor="savefile">Enter Savefile Name... </label>
<input className="form-control" ref="savename" type="text" name="savefile" id="savefilename" />
</div>
<div className="form-group">
<input className="form-control btn btn-default" type="button" ref="button" value="Create" onClick={this.createSaveFile} />
</div>
</form>
</div>
{loadingOverlay}
</div>
)
}
}
CreateSave.propTypes = {
getSaves: PropTypes.func.isRequired,
}
export default CreateSave

View File

@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import FontAwesomeIcon from "../FontAwesomeIcon";
class Save extends React.Component {
render() {
let saveLocation = "/api/saves/dl/" + this.props.save.name
let saveSize = parseFloat(this.props.save.size / 1024 / 1024).toFixed(3)
let saveLastMod = Date.parse(this.props.save.last_mod);
let date = new Date(saveLastMod)
let dateFmt = date.toISOString().replace('T', ' ').split('.')[0]; //Outputs date as "YYYY-MM-DD HH:MM:SS" with zero-padding and 24h
return(
<tr>
<td>{this.props.save.name}</td>
<td>{dateFmt}</td>
<td>{saveSize} MB</td>
<td>
<a className="btn btn-default" href={saveLocation}>Download</a>
</td>
<td>
<button
className="btn btn-danger btn-small"
ref="saveInput"
type="button"
onClick={this.props.removeSave.bind(this, this.props.saves[this.props.index].name)}
>
<FontAwesomeIcon icon="trash"/>
&nbsp;
Delete
</button>
</td>
</tr>
)
}
}
Save.propTypes = {
save: PropTypes.object.isRequired,
saves: PropTypes.array.isRequired,
index: PropTypes.number.isRequired,
removeSave: PropTypes.func.isRequired
}
export default Save

View File

@ -1,116 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Save from './Save.jsx';
import {ReactSwalDanger, ReactSwalNormal} from 'Utilities/customSwal';
class SavesList extends React.Component {
constructor(props) {
super(props);
this.updateSavesList = this.updateSavesList.bind(this);
this.removeSave = this.removeSave.bind(this);
}
updateSavesList () {
this.props.getSaves();
}
removeSave(saveName, e) {
let self = this;
ReactSwalDanger.fire({
title: "Are you sure?",
html: <p>Save: {saveName} will be deleted</p>,
type: "question",
showCancelButton: true,
confirmButtonText: "Yes, delete it!",
showLoaderOnConfirm: true,
preConfirm: () => {
return new Promise((resolve, reject) => {
$.ajax({
url: "/api/saves/rm/" + saveName,
dataType: "json",
success: (resp) => {
if (resp.success === true) {
resolve(resp.data);
} else {
reject("Unknown occurred!");
}
},
error: () => {
reject("Unknown occurred!");
}
});
});
},
}).then((result) => {
if(result.value) {
ReactSwalNormal.fire({
title: "Deleted!",
text: result.value,
type: "success"
});
}
self.updateSavesList();
}).catch((result) => {
ReactSwalNormal.fire({
title: "An error occurred!",
text: result,
type: "error"
});
});
}
render() {
let savesList;
if (this.props.saves.length === 0) {
savesList = <tr></tr>
} else {
savesList = this.props.saves.map ( (save, i) => {
return(
<Save
key={i}
saves={this.props.saves}
index={i}
save={save}
removeSave={this.removeSave}
/>
)
});
}
return(
<div className="box">
<div className="box-header">
<h3 className="box-title">Save Files</h3>
</div>
<div className="box-body">
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Filname</th>
<th>Last Modified Time</th>
<th>Filesize</th>
<th>Download</th>
</tr>
</thead>
<tbody>
{savesList}
</tbody>
</table>
</div>
</div>
</div>
)
}
}
SavesList.propTypes = {
saves: PropTypes.array.isRequired,
dlSave: PropTypes.func.isRequired,
getSaves: PropTypes.func.isRequired
};
export default SavesList

View File

@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class UploadSave extends React.Component {
constructor(props) {
super(props)
this.updateSavesList = this.updateSavesList.bind(this);
}
componentDidMount() {
$("#savefile").fileinput({
showPreview: false,
uploadUrl: '/api/saves/upload',
showCancel: false,
allowedFileExtensions: ['zip'],
theme: "fas",
removeClass: "btn btn-default",
uploadClass: "btn btn-default",
}).on('filebatchuploadsuccess fileuploaded', this.updateSavesList);
}
updateSavesList() {
$('#savefile').fileinput('reset');
this.props.getSaves();
}
render() {
return(
<div className="box">
<div className="box-header">
<h4 className="box-title">Upload Save File</h4>
</div>
<div className="box-body">
<input id="savefile" name="savefile" type="file" className="file"/>
</div>
</div>
)
}
}
UploadSave.propTypes = {
getSaves: PropTypes.func.isRequired,
}
export default UploadSave

View File

@ -1,79 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import SavesList from './Saves/SavesList.jsx';
import CreateSave from './Saves/CreateSave.jsx';
import UploadSave from './Saves/UploadSave.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
class SavesContent extends React.Component {
constructor(props) {
super(props);
this.dlSave = this.dlSave.bind(this);
}
componentDidMount() {
this.props.getSaves();
}
dlSave(saveName) {
$.ajax({
url: "/api/saves/dl/" + saveName,
dataType: "json",
success: (data) => {
console.log("Downloading save: " + saveName)
},
error: (xhr, status, err) => {
console.log('api/mods/list', status, err.toString());
}
})
}
render() {
return (
<div id="saves" className="content-wrapper">
<section className="content-header">
<h1>
Saves
<small>Factorio Save Files</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt" className="fa-fw"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="save" prefix="far"/>Saves
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<div className="row">
<div className="col-md-6">
<CreateSave
getSaves={this.props.getSaves}
/>
</div>
<div className="col-md-6">
<UploadSave
getSaves={this.props.getSaves}
/>
</div>
</div>
<SavesList
{...this.state}
saves={this.props.saves}
dlSave={this.dlSave}
getSaves={this.props.getSaves}
/>
</section>
</div>
)
}
}
export default SavesContent

View File

@ -1,205 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {ReactSwalNormal} from 'Utilities/customSwal';
import FontAwesomeIcon from "../FontAwesomeIcon";
class ServerCtl extends React.Component {
constructor(props) {
super(props);
this.startServer = this.startServer.bind(this);
this.stopServer = this.stopServer.bind(this);
this.killServer = this.killServer.bind(this);
this.incrementPort = this.incrementPort.bind(this);
this.decrementPort = this.decrementPort.bind(this);
this.state = {
gameBindIP: "0.0.0.0",
savefile: "",
port: 34197,
}
}
startServer(e) {
e.preventDefault();
let serverSettings = {
bindip: this.refs.gameBindIP.value,
savefile: this.refs.savefile.value,
port: Number(this.refs.port.value),
}
$.ajax({
type: "POST",
url: "/api/server/start",
dataType: "json",
data: JSON.stringify(serverSettings),
success: (resp) => {
this.props.facServStatus();
this.props.getStatus();
if (resp.success === true) {
ReactSwalNormal.fire({
title: "Factorio server started",
text: resp.data,
type: "success"
});
} else {
ReactSwalNormal.fire({
title: "Error starting Factorio server",
text: resp.data,
type: "error"
});
}
}
});
this.setState({
savefile: this.refs.savefile.value,
port: Number(this.refs.port.value),
});
}
stopServer(e) {
$.ajax({
type: "GET",
url: "/api/server/stop",
dataType: "json",
success: (resp) => {
this.props.facServStatus();
this.props.getStatus();
ReactSwalNormal.fire({
title: resp.data
});
}
});
e.preventDefault();
}
killServer(e) {
$.ajax({
type: "GET",
url: "/api/server/kill",
dataType: "json",
success: (resp) => {
this.props.facServStatus();
this.props.getStatus();
ReactSwalNormal.fire({
title: resp.data
});
}
});
e.preventDefault();
}
incrementPort() {
let port = this.state.port + 1;
this.setState({port: port})
}
decrementPort() {
let port = this.state.port - 1;
this.setState({port: port})
}
render() {
return (
<div id="serverCtl" className="box">
<div className="box-header">
<h3 className="box-title">Server Control</h3>
</div>
<div className="box-body">
<form action="" onSubmit={this.startServer}>
<div className="form-group">
<div className="row">
<div className="col-md-4">
<button className="btn btn-block btn-success" type="submit">
<FontAwesomeIcon icon="play" className="fa-fw"/>Start Factorio Server
</button>
</div>
<div className="col-md-4">
<button className="btn btn-block btn-warning" type="button" onClick={this.stopServer}>
<FontAwesomeIcon icon="stop" className="fa-fw"/>Stop &amp; Save Factorio Server
</button>
</div>
<div className="col-md-4">
<button className="btn btn-block btn-danger" type="button" onClick={this.killServer}>
<FontAwesomeIcon icon="close" className="fa-fw"/>Stop Factorio Server without Saving
</button>
</div>
</div>
<hr/>
<label>Select Save File</label>
<select ref="savefile" className="form-control">
{this.props.saves.map((save, i) => {
return (
<option key={save.name} value={save.name}>{save.name}</option>
)
})}
</select>
</div>
<div className="box box-success advanced">
<button type="button"
className="btn btn-box-tool"
data-toggle="collapse"
data-target="#serverCtlAdvanced"
aria-expanded="false"
aria-controls="serverCtlAdvanced"
>
<div className="box-header with-border">
<FontAwesomeIcon icon="plus" className="fa-fw"/>
<div className="box-title">Advanced</div>
</div>
</button>
<div id="serverCtlAdvanced" className="box-body collapse">
<label htmlFor="port">Factorio Server IP</label>
<div id="port" className="input-group">
<input ref="gameBindIP"
name="gameBindIP"
id="gameBindIP"
type="text"
className="form-control"
defaultValue={this.state.gameBindIP}
placeholder={this.state.gameBindIP}/>
</div>
<label htmlFor="port">Factorio Server Port</label>
<div id="port" className="input-group">
<input ref="port"
name="port"
id="port"
type="text"
className="form-control"
defaultValue={this.state.port}
placeholder={this.state.port}
/>
<div className="input-group-btn">
<button type="button" className="btn btn-primary" onClick={this.incrementPort}>
<FontAwesomeIcon icon="arrow-up"/>
</button>
<button type="button" className="btn btn-primary" onClick={this.decrementPort}>
<FontAwesomeIcon icon="arrow-down"/>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
)
}
}
ServerCtl.propTypes = {
facServStatus: PropTypes.func.isRequired,
getStatus: PropTypes.func.isRequired,
}
export default ServerCtl

View File

@ -1,67 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class ServerStatus extends React.Component {
constructor(props) {
super(props);
this.formatServerStatus = this.formatServerStatus.bind(this)
}
capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
formatServerStatus(serverStatus) {
var result = {};
if (serverStatus === "running") {
result = <span className="badge badge-success">Running</span>;
return result;
} else if (serverStatus == "stopped") {
result = <span className="badge badge-danger">Not Running</span>;
return result;
}
return serverStatus
}
render() {
return(
<div className="box">
<div className="box-header">
<h3 className="box-title">Server Status</h3>
</div>
<div className="box-body">
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{Object.keys(this.props.serverStatus).map(function(key) {
return(
<tr key={key}>
<td>{this.capitalizeFirstLetter(key)}</td>
<td>{this.formatServerStatus(this.props.serverStatus[key])}</td>
</tr>
)
}, this)}
</tbody>
</table>
</div>
</div>
</div>
)
}
}
ServerStatus.propTypes = {
serverStatus: PropTypes.object.isRequired,
};
export default ServerStatus

View File

@ -1,97 +0,0 @@
import React from 'react';
import {Link, NavLink} from 'react-router-dom';
import PropTypes from 'prop-types';
import FontAwesomeIcon from './FontAwesomeIcon.jsx';
class Sidebar extends React.Component {
constructor(props) {
super(props);
}
render() {
if(this.props.serverRunning === "running") {
var serverStatus = <Link to="/" className="d-block text-success"><FontAwesomeIcon icon="circle"/>Server Online</Link>
} else {
var serverStatus = <Link to="/" className="d-block text-danger"><FontAwesomeIcon icon="circle"/>Server Offline</Link>
}
return (
<aside className="main-sidebar sidebar-dark-primary elevation-4">
<Link className="brand-link" to="/">
<span className="logo-lg"><b>Factorio</b>SM</span>
</Link>
<div className="sidebar">
<div className="user-panel">
<div className="image">
<img src="./images/factorio.jpg" className="img-circle" alt="User Image"/>
</div>
<div className="info">
<div className="text-white">Factorio Server Manager</div>
{serverStatus}
</div>
</div>
{/*<form action="#" method="get" className="sidebar-form">
<div className="input-group">
<input type="text" name="q" className="form-control" placeholder="Search..."/>
<span className="input-group-btn">
<button type="submit" name="search" id="search-btn" className="btn btn-flat"><i
className="fa fa-search"></i>
</button>
</span>
</div>
</form>*/}
<nav className="mt-2">
<ul className="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li className="nav-header">MENU</li>
<li className="nav-item">
<NavLink exact to="/" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="tachometer-alt" className="nav-icon"/><p>Server Control</p>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/mods" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="pencil-alt" className="nav-icon"/><p>Mods</p>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/logs" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="file-alt" className="nav-icon" prefix="far"/><p>Logs</p>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/saves" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="save" className="nav-icon" prefix="far"/><p>Saves</p>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/config" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="cogs" className="nav-icon"/><p>Game Configuration</p>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/settings" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="cog" className="nav-icon"/><p>Settings</p>
</NavLink>
</li>
<li className="nav-item">
<NavLink to="/console" activeClassName="active" className="nav-link">
<FontAwesomeIcon icon="terminal" className="nav-icon"/><p>Console</p>
</NavLink>
</li>
</ul>
</nav>
</div>
</aside>
)
}
}
Sidebar.propTypes = {
serverStatus: PropTypes.func.isRequired,
serverRunning: PropTypes.string.isRequired,
}
export default Sidebar

View File

@ -1,130 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {ReactSwalNormal} from 'Utilities/customSwal';
import FontAwesomeIcon from "../FontAwesomeIcon";
class AddUser extends React.Component {
constructor(props) {
super(props);
this.createUser = this.createUser.bind(this);
this.validateEmail = this.validateEmail.bind(this);
}
validateEmail(email) {
var re = /\S+@\S+\.\S+/;
return re.test(email)
}
createUser(e) {
e.preventDefault();
console.log(this.refs);
let user = {
username: this.refs.username.value,
// Add handler for listing roles
role: "admin",
password: this.refs.password.value,
email: this.refs.email.value,
}
if (user.password !== this.refs.passwordConfirm.value) {
console.log("passwords do not match");
return
}
if (!this.validateEmail(user.email)) {
console.log("invalid email address");
return
}
$.ajax({
type: "POST",
url: "/api/user/add",
dataType: "json",
data: JSON.stringify(user),
success: (resp) => {
if (resp.success === true) {
ReactSwalNormal.fire({
title: <p>User: {user.username} added successfully</p>,
type: "success"
});
this.props.listUsers();
} else {
ReactSwalNormal.fire({
title: <p>Error adding user: {resp.data}</p>,
type: "error"
})
}
}
})
}
render() {
return(
<div className="box">
<div className="box-header">
<h3 className="box-title">Manage Users</h3>
</div>
<div className="box-body">
<h4>Add user</h4>
<div className="row">
<div className="col-md-4">
<form action="" onSubmit={this.createUser}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input
ref="username"
type="text"
className="form-control"
id="username"
placeholder="Enter username"
/>
</div>
<div className="form-group">
<label htmlFor="email">Email address</label>
<input
ref="email"
type="text"
className="form-control"
id="email"
placeholder="Enter email"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
ref="password"
type="password"
className="form-control"
id="password"
placeholder="Enter password"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password confirmation</label>
<input
ref="passwordConfirm"
type="password"
className="form-control"
id="password"
placeholder="Enter password again"
/>
</div>
<button className="btn btn-block btn-success" type="submit">
<FontAwesomeIcon icon="plus" className="fa-fw"/>
Add User
</button>
</form>
</div>
</div>
</div>
</div>
)
}
}
AddUser.propTypes = {
listUsers: PropTypes.func.isRequired,
}
export default AddUser

View File

@ -1,103 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {ReactSwalDanger, ReactSwalNormal} from 'Utilities/customSwal';
class UserTable extends React.Component {
constructor(props) {
super(props);
this.removeUser = this.removeUser.bind(this);
}
removeUser(user) {
ReactSwalDanger.fire({
title: "Are you sure?",
html: <p>User {user} will be deleted!</p>,
type: "question",
showCancelButton: true,
confirmButtonText: "Yes, delete it!",
showLoaderOnConfirm: true,
preConfirm: () => {
return new Promise((resolve, reject) => {
user = {username: user};
$.ajax({
type: "POST",
url: "/api/user/remove",
dataType: "json",
data: JSON.stringify(user),
success: (resp) => {
if (resp.success === true) {
resolve(resp.data);
} else {
reject("Unknown error");
}
},
error: () => {
reject("Unknown error");
}
});
});
}
}).then((result) => {
if (result.value) {
ReactSwalNormal.fire({
title: "Deleted!",
text: result.value,
type: "success"
});
this.props.listUsers();
}
}).catch((result) => {
ReactSwalNormal.fire({
title: "An error occurred!",
text: result,
type: "error"
});
});
}
render() {
return(
<div className="box">
<div className="box-header">
<h3 className="box-title">Users</h3>
</div>
<div className="box-body">
<table className="table table-striped">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Email</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{this.props.users.map( (user, i) => {
return(
<tr key={user.username}>
<td>{user.username}</td>
<td>{user.role}</td>
<td>{user.email}</td>
<td>
<button className="btn btn-danger" onClick={()=>{this.removeUser(user.username)}}>Delete</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}
}
UserTable.propTypes = {
users: PropTypes.array.isRequired,
listUsers: PropTypes.func.isRequired,
}
export default UserTable

View File

@ -1,72 +0,0 @@
import React from 'react';
import {Link} from 'react-router-dom';
import UserTable from './Users/UserTable.jsx';
import AddUser from './Users/AddUser.jsx';
import FontAwesomeIcon from "./FontAwesomeIcon";
class UsersContent extends React.Component {
constructor(props) {
super(props);
this.listUsers = this.listUsers.bind(this);
this.state = {
users: [],
}
}
componentDidMount() {
this.listUsers();
}
listUsers() {
// $.ajax({
// type: "GET",
// url: "/api/user/list",
// dataType: "json",
// success: (resp) => {
// if (resp.success === true) {
// this.setState({users: resp.data})
// } else {
// console.log("error listing users.")
// }
// }
// })
this.setState({users: []});
}
render() {
return(
<div className="content-wrapper">
<section className="content-header">
<h1>
Settings
<small>Manage Factorio Server Manager settings</small>
<small className="float-sm-right">
<ol className="breadcrumb">
<li className="breadcrumb-item">
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
</li>
<li className="breadcrumb-item active">
<FontAwesomeIcon icon="cog"/>Settings
</li>
</ol>
</small>
</h1>
</section>
<section className="content">
<UserTable
users={this.state.users}
listUsers={this.listUsers}
/>
<AddUser
listUsers={this.listUsers}
/>
</section>
</div>
)
}
}
export default UsersContent;

View File

@ -1,35 +0,0 @@
import Button from "../elements/Button";
import React from "react";
import {useForm} from "react-hook-form";
import saves from "../../api/resources/saves";
const UploadSaveForm = ({onSuccess}) => {
const {register, handleSubmit, errors} = useForm();
const onSubmit = async data => {
const res = await saves.upload(data.savefile);
if (res.success) {
onSuccess();
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-6">
<label className="block text-white text-sm font-bold mb-2" htmlFor="password">
Savefile Name
</label>
<input
className="shadow appearance-none w-full py-2 px-3 text-black"
ref={register({required: true})}
name="savefile"
id="savefile" type="file"/>
{errors.savefile && <span className="block text-red">Savefile Name is required</span>}
</div>
<Button type="success">Upload Save</Button>
</form>
)
}
export default UploadSaveForm;

45
ui/App/views/Console.jsx Normal file
View File

@ -0,0 +1,45 @@
import Panel from "../components/Panel";
import React, {useEffect, useRef, useState} from "react";
import Socket from "../../api/socket"
const Console = () => {
const socket = new Socket();
const [logs, setLogs] = useState([]);
const consoleInput = useRef(null);
useEffect(() => {
// todo: maybe check for ready state
socket.emit("log subscribe")
socket.on('log update', log => {
console.log(log)
setLogs((logs) => {
[...logs].push(log)
});
})
consoleInput.current?.focus();
}, []);
return (
<Panel
title="Console"
content={
<>
<ul>
{logs?.map(log => (<li key={log}>{log}</li>))}
</ul>
<input type="text" ref={consoleInput} className="shadow appearance-none border w-full py-2 px-3 text-black" onKeyPress={e => {
if (e.key === "Enter") {
socket.emit("command send", consoleInput.current.value);
consoleInput.current.value = ""
}
}
}/>
</>
}
/>
)
}
export default Console;

View File

@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useMemo, useState} from "react";
import Panel from "../elements/Panel";
import Button from "../elements/Button";
import React, {useEffect, useState} from "react";
import Panel from "../components/Panel";
import Button from "../components/Button";
import server from "../../api/resources/server";
const Controls = ({serverStatus, updateServerStatus, s}) => {

View File

@ -0,0 +1,13 @@
import Panel from "../components/Panel";
import React from "react";
const GameSettings = () => {
return (
<Panel
title="Game Settings"
content={null}
/>
)
}
export default GameSettings;

13
ui/App/views/Help.jsx Normal file
View File

@ -0,0 +1,13 @@
import Panel from "../components/Panel";
import React from "react";
const Help = () => {
return (
<Panel
title="Help"
content={null}
/>
)
}
export default Help;

View File

@ -1,9 +1,9 @@
import React, {useEffect} from 'react';
import {useForm} from "react-hook-form";
import user from "../../api/resources/user";
import Button from "../elements/Button";
import Button from "../components/Button";
import {useHistory, useLocation} from "react-router";
import Panel from "../elements/Panel";
import Panel from "../components/Panel";
const Login = ({handleLogin}) => {
const {register, handleSubmit, errors} = useForm();
@ -44,7 +44,7 @@ const Login = ({handleLogin}) => {
id="username"
name="username"
type="text" placeholder="Username"/>
{errors.password && <span className="block text-red">Username is required</span>}
{errors.username && <span className="block text-red">Username is required</span>}
</div>
<div className="mb-6">
<label className="block text-white text-sm font-bold mb-2" htmlFor="password">

View File

@ -1,7 +1,7 @@
import React, {useEffect, useState} from "react";
import Layout from "../components/Layout";
import log from "../../api/resources/log";
import Panel from "../elements/Panel";
import Panel from "../components/Panel";
const Logs = () => {

View File

@ -1,4 +1,4 @@
import Panel from "../elements/Panel";
import Panel from "../components/Panel";
import React from "react";
const Mods = () => {

View File

@ -1,16 +1,14 @@
import React, {useEffect, useState} from "react";
import saveClient from "../../api/resources/saves";
import Panel from "../elements/Panel";
import ButtonLink from "../elements/ButtonLink";
import Button from "../elements/Button";
import {useForm} from "react-hook-form";
import CreateSaveForm from "../forms/CreateSaveForm";
import UploadSaveForm from "../forms/UploadSaveForm";
import saveClient from "../../../api/resources/saves";
import Panel from "../../components/Panel";
import ButtonLink from "../../components/ButtonLink";
import Button from "../../components/Button";
import CreateSaveForm from "./components/CreateSaveForm";
import UploadSaveForm from "./components/UploadSaveForm";
const Saves = ({serverStatus}) => {
const [saves, setSaves] = useState([]);
const {register, handleSubmit, errors} = useForm();
const updateList = async () => {
const res = await saveClient.list();
@ -48,7 +46,7 @@ const Saves = ({serverStatus}) => {
<Panel
title="Upload Save"
className="w-1/2 ml-3"
content={<UploadSaveForm/>}
content={<UploadSaveForm onSuccess={updateList}/>}
/>
</div>

View File

@ -1,15 +1,16 @@
import {useForm} from "react-hook-form";
import Button from "../elements/Button";
import Button from "../../../components/Button";
import React from "react";
import saves from "../../api/resources/saves";
import saves from "../../../../api/resources/saves";
const CreateSaveForm = ({onSuccess}) => {
const {register, handleSubmit, errors} = useForm();
const onSubmit = async data => {
const onSubmit = async (data, e) => {
const res = await saves.create(data.savefile);
if (res.success) {
e.target.reset();
onSuccess();
}
};

View File

@ -0,0 +1,42 @@
import Button from "../../../components/Button";
import React, {useState} from "react";
import {useForm} from "react-hook-form";
import saves from "../../../../api/resources/saves";
const UploadSaveForm = ({onSuccess}) => {
const {register, handleSubmit, errors} = useForm();
const [fileName, setFileName] = useState('Select File ...');
const onSubmit = async (data, e) => {
const res = await saves.upload(data.savefile);
if (res.success) {
e.target.reset();
onSuccess();
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-6">
<label className="block text-white text-sm font-bold mb-2" htmlFor="password">
Save File
</label>
<div className="relative bg-white shadow text-black w-full">
<input
className="absolute left-0 top-0 opacity-0 cursor-pointer w-full h-full"
ref={register({required: true})}
onChange={e => setFileName(e.currentTarget.files[0].name)}
name="savefile"
id="savefile" type="file"/>
<div className="px-2 py-3">{fileName}</div>
</div>
{errors.savefile && <span className="block text-red">Savefile is required</span>}
</div>
<Button type="success" isSubmit={true}>Upload</Button>
</form>
)
}
export default UploadSaveForm;

View File

@ -0,0 +1,13 @@
import Panel from "../components/Panel";
import React from "react";
const ServerSettings = () => {
return (
<Panel
title="Server Settings"
content={null}
/>
)
}
export default ServerSettings;

View File

@ -0,0 +1,65 @@
import Panel from "../../components/Panel";
import React, {useCallback, useEffect, useState} from "react";
import Button from "../../components/Button";
import user from "../../../api/resources/user";
import CreateUserForm from "./components/CreateUserForm";
const UserManagement = () => {
const [users, setUsers] = useState([]);
const updateList = useCallback(async () => {
const res = await user.list();
if (res.success) {
setUsers(res.data);
}
}, []);
const deleteUser = useCallback(async (username) => {
user.delete(username)
.then(updateList);
}, []);
useEffect(() => {
updateList()
}, []);
return (
<>
<Panel
title="List of Users"
content={
<table className="w-full">
<thead>
<tr className="text-left py-1">
<th>Name</th>
<th>Role</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map(user =>
<tr className="py-1" key={user.username}>
<td className="pr-4">{user.username}</td>
<td className="pr-4">{user.role}</td>
<td className="pr-4">{user.email}</td>
<td>
<Button size="sm" onClick={() => deleteUser(user.username)} type="danger">Delete</Button>
</td>
</tr>
)}
</tbody>
</table>
}
className="mb-4"
/>
<Panel
title="Create User"
content={<CreateUserForm updateUserList={updateList}/>}
/>
</>
)
}
export default UserManagement;

View File

@ -0,0 +1,82 @@
import {useForm} from "react-hook-form";
import React from "react";
import user from "../../../../api/resources/user";
import Button from "../../../components/Button";
const CreateUserForm = ({updateUserList}) => {
const {register, handleSubmit, errors, watch} = useForm();
const password = watch('password');
const onSubmit = async (data) => {
const res = user.add(data);
if (res.success) {
updateUserList()
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4">
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
Username
</label>
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
ref={register({required: true})}
id="username"
name="username"
type="text" placeholder="Username"/>
{errors.username && <span className="block text-red">Username is required</span>}
</div>
<div className="mb-4">
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
Role
</label>
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
ref={register({required: true})}
id="role"
name="role"
value="admin"
disabled={true}
type="text" placeholder="Role"/>
{errors.role && <span className="block text-red">Role is required</span>}
</div>
<div className="mb-4">
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
Email
</label>
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
ref={register({required: true})}
id="email"
name="email"
type="text" placeholder="Email"/>
{errors.email && <span className="block text-red">Email is required</span>}
</div>
<div className="mb-4">
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
Password
</label>
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
ref={register({required: true})}
id="password"
name="password"
type="password" placeholder="Password"/>
{errors.password && <span className="block text-red">Password is required</span>}
</div>
<div className="mb-4">
<label className="block text-white text-sm font-bold mb-2" htmlFor="username">
Password Confirmation
</label>
<input className="shadow appearance-none border w-full py-2 px-3 text-black"
ref={register({required: true, validate: confirmation => confirmation === password})}
id="password_confirmation"
name="password_confirmation"
type="password" placeholder="Password Confirmation"/>
{errors.password_confirmation && <span className="block text-red">Password Confirmation is required and must match the Password</span>}
</div>
<Button isSubmit={true} type="success">Save</Button>
</form>
)
}
export default CreateUserForm;

View File

@ -1,20 +0,0 @@
import Panel from "../elements/Panel";
import React from "react";
const UserManagement = () => {
return (
<>
<Panel
title="List of Users"
content="test"
className="mb-4"
/>
<Panel
title="Create User"
content="test"
/>
</>
)
}
export default UserManagement;

View File

@ -12,5 +12,16 @@ export default {
create: async (name) => {
const response = await client.get(`/api/saves/create/${name}`);
return response.data;
},
upload: async (data) => {
let formData = new FormData();
formData.append("savefile", data[0]);
const response = await client.post(`/api/saves/upload`, formData, {
headers: {
"Content-Type": "multipart/form-data"
}
});
return response.data;
}
}

View File

@ -12,5 +12,17 @@ export default {
logout: async () => {
const response = await client.get('/api/logout');
return response.data;
},
list: async () => {
const response = await client.get('/api/user/list');
return response.data;
},
add: async (user) => {
const response = await client.post('/api/user/add', user);
return response.data;
},
delete: async (username) => {
const response = await client.post('/api/user/remove', JSON.stringify({username}));
return response.data;
}
}

View File

@ -1,12 +1,14 @@
import EventEmitter from 'events';
class Socket {
constructor(ws, ee = new EventEmitter()){
this.ws = ws;
this.ee = ee;
ws.onmessage = this.message.bind(this);
ws.onopen = this.open.bind(this);
ws.onclose = this.close.bind(this);
constructor(){
let ws_scheme = window.location.protocol === "https:" ? "wss" : "ws";
this.ws = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
this.ee = new EventEmitter();
this.ws.onmessage = this.message.bind(this);
this.ws.onopen = this.open.bind(this);
this.ws.onclose = this.close.bind(this);
this.opened = false;
}
@ -17,7 +19,7 @@ class Socket {
this.ee.removeListener(name, fn);
}
emit(name, data){
if(this.ws.readyState == WebSocket.OPEN) {
if(this.ws.readyState === WebSocket.OPEN) {
const message = JSON.stringify({name, data});
this.ws.send(message);
}
@ -27,7 +29,6 @@ class Socket {
message(e){
try{
let message = JSON.parse(e.data);
// console.log(message.name, message.data);
this.ee.emit(message.name, message.data);
}
catch(err){

View File

@ -1,20 +0,0 @@
import Swal from 'sweetalert2/dist/sweetalert2';
import withReactContent from 'sweetalert2-react-content';
const ReactSwal = withReactContent(Swal);
const ReactSwalTemp = ReactSwal.mixin({
confirmButtonClass: "swal-btn btn-primary",
cancelButtonClass: "swal-btn btn-secondary",
customClass: "swal-design",
buttonsStyling: false,
allowOutsideClick: () => !ReactSwalDanger.isLoading()
});
export const ReactSwalNormal = ReactSwalTemp.mixin({
});
export const ReactSwalDanger = ReactSwalTemp.mixin({
confirmButtonClass: "swal-btn btn-danger"
});