Changes from SPA to MPA

This commit is contained in:
Henry Whitaker
2020-12-20 00:09:18 +00:00
parent 95325db128
commit a7652af2ba
24 changed files with 154099 additions and 904 deletions

10899
public/css/app.css vendored

File diff suppressed because one or more lines are too long

142482
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Container, Row, Col, Collapse, Button, Modal } from 'react-bootstrap'; import { Row, Col} from 'react-bootstrap';
import SessionsTable from './SessionsTable'; import SessionsTable from './SessionsTable';
import ResetPassword from './ResetPassword'; import ResetPassword from './ResetPassword';
@@ -44,20 +44,6 @@ export default class Authentication extends Component {
if( (window.config.auth == true && window.authenticated == true)) { if( (window.config.auth == true && window.authenticated == true)) {
return ( return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<div className="mouse" aria-controls="testsTable" onClick={this.toggleCollapse} aria-expanded={showCollapse}>
<h4 className="d-inline mr-2">Authentication</h4>
{(showCollapse) ?
<span className="ti-angle-up"></span>
:
<span className="ti-angle-down"></span>
}
</div>
</Col>
</Row>
<Collapse in={showCollapse}>
<div> <div>
<Row> <Row>
<Col sm={{ span: 12 }} className="text-center"> <Col sm={{ span: 12 }} className="text-center">
@@ -70,8 +56,6 @@ export default class Authentication extends Component {
</Col> </Col>
</Row> </Row>
</div> </div>
</Collapse>
</Container>
); );
} else { } else {
return ( return (

View File

@@ -37,6 +37,10 @@ export default class HistoryGraph extends Component {
}); });
} }
componentWillUnmount() {
clearInterval(this.state.interval);
}
getDLULPing = (days) => { getDLULPing = (days) => {
var url = 'api/speedtest/time/' + days; var url = 'api/speedtest/time/' + days;

View File

@@ -26,6 +26,10 @@ export default class LatestResults extends Component {
}); });
} }
componentWillUnmount() {
clearInterval(this.state.interval);
}
getData = () => { getData = () => {
var url = 'api/speedtest/latest'; var url = 'api/speedtest/latest';

View File

@@ -26,6 +26,10 @@ export default class TestsTable extends Component {
}); });
} }
componentWillUnmount() {
clearInterval(this.state.interval);
}
getData = (page = this.state.page, refresh = true) => { getData = (page = this.state.page, refresh = true) => {
var url = 'api/speedtest/?page=' + page; var url = 'api/speedtest/?page=' + page;
@@ -83,26 +87,16 @@ export default class TestsTable extends Component {
if(data.length > 0) { if(data.length > 0) {
return ( return (
<Container className="mb-4 mt-4" fluid> <div>
<Container className="mb-4 mt-4 px-5">
<Row> <Row>
<Col sm={{ span: 12 }} className="mb-3 text-center"> <Col sm={{ span: 12 }} className="mb-3 text-center">
<div className="mouse" aria-controls="testsTable" onClick={this.toggleCollapse} aria-expanded={show}> <div>
<h4 className="d-inline mr-2">All tests</h4> <h4 className="d-inline mr-2">All tests</h4>
{(show) ?
<span className="ti-angle-up"></span>
:
<span className="ti-angle-down"></span>
}
</div>
{(show) &&
<div className="my-1">
<span className="text-muted">Auto refresh: {(refresh) ? 'On' : 'Off'}</span> <span className="text-muted">Auto refresh: {(refresh) ? 'On' : 'Off'}</span>
</div> </div>
}
</Col> </Col>
</Row> </Row>
<Collapse in={show}>
<div>
<Row> <Row>
<Col sm={{ span: 12 }} id="testsTable"> <Col sm={{ span: 12 }} id="testsTable">
<Table responsive> <Table responsive>
@@ -133,9 +127,8 @@ export default class TestsTable extends Component {
</Col> </Col>
</Row> </Row>
} }
</div>
</Collapse>
</Container> </Container>
</div>
); );
} else { } else {
return ( return (

View File

@@ -5,7 +5,6 @@ import LatestResults from '../Graphics/LatestResults';
import Footer from './Footer'; import Footer from './Footer';
import DataRow from '../Data/DataRow'; import DataRow from '../Data/DataRow';
import TestsTable from '../Graphics/TestsTable'; import TestsTable from '../Graphics/TestsTable';
import Settings from '../Settings/Settings';
import Login from '../Login'; import Login from '../Login';
import Authentication from '../Authentication/Authentication'; import Authentication from '../Authentication/Authentication';
import Navbar from '../Navbar'; import Navbar from '../Navbar';
@@ -22,10 +21,6 @@ export default class HomePage extends Component {
} }
<LatestResults /> <LatestResults />
<HistoryGraph /> <HistoryGraph />
<TestsTable />
<Settings />
<Authentication />
<DataRow />
</div> </div>
<Footer /> <Footer />
</div> </div>

View File

@@ -39,6 +39,9 @@ export default class Login extends Component {
Cookies.set('auth', token, { expires: expires }) Cookies.set('auth', token, { expires: expires })
window.location.reload(true); window.location.reload(true);
}) })
.catch((err) => {
toast.error('Something went wrong logging in.');
})
} }
toggleShow = () => { toggleShow = () => {

View File

@@ -9,26 +9,38 @@ export default class Navbar extends Component {
this.state = { this.state = {
brand: { brand: {
name: "Speedtest Tracker", name: window.config.name,
url: window.config.base url: window.config.base
}, },
pages: [ }
}
generatePagesArray() {
var pages = [
{ {
name: 'Home', name: 'Home',
url: window.config.base, url: window.config.base,
authRequired: false authRequired: false
}, },
{
name: 'All Tests',
url: window.config.base + 'speedtests',
authRequired: false
},
{ {
name: 'Settings', name: 'Settings',
url: window.config.base + 'settings', url: window.config.base + 'settings',
authRequired: true authRequired: true
} },
] ]
}
return pages;
} }
generateLinks = () => { generateLinks = () => {
return this.state.pages.map(page => { var pages = this.generatePagesArray();
return pages.map(page => {
if( if(
page.authRequired === false || page.authRequired === false ||
( (
@@ -51,8 +63,8 @@ export default class Navbar extends Component {
var pages = this.generateLinks(); var pages = this.generateLinks();
return ( return (
<BootstrapNavbar variant="dark" bg="dark"> <BootstrapNavbar variant="dark" bg="dark" expand="sm">
<BootstrapNavbar.Brand as={Link} to={brand.url}>{brand.name}</BootstrapNavbar.Brand> <BootstrapNavbar.Brand as={Link} to={brand.url}><img style={{width: '15%'}} src={window.config.base + 'files/icons/fav/android-icon-192x192.png'} /> {brand.name}</BootstrapNavbar.Brand>
<BootstrapNavbar.Toggle aria-controls="basic-navbar-nav" /> <BootstrapNavbar.Toggle aria-controls="basic-navbar-nav" />
<BootstrapNavbar.Collapse id="basic-navbar-nav"> <BootstrapNavbar.Collapse id="basic-navbar-nav">
<Nav className="ml-auto"> <Nav className="ml-auto">

View File

@@ -1,77 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Card, Form, Button } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class Setting extends Component {
constructor(props) {
super(props)
this.state = {
name: this.props.name,
value: this.props.value,
description: this.props.description,
}
}
ucfirst(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
update = () => {
var url = 'api/settings?token=' + window.token;
var data = {
name: this.state.name,
value: this.state.value
};
Axios.post(url, data)
.then((resp) => {
toast.success(this.ucfirst(this.state.name) + ' updated');
})
.catch((err) => {
if(err.response.status == 422) {
var errors = err.response.data.error;
for(var key in errors) {
var error = errors[key];
toast.error(error[0])
}
} else {
toast.error('Something went wrong')
}
})
}
updateValue = (e) => {
this.setState({
value: e.target.value
});
}
render() {
var name = this.state.name;
var value = this.state.value;
var description = this.state.description;
return (
<Card className="m-2 setting-card">
<Card.Body className="d-flex align-items-center">
<div>
<h4>{this.ucfirst(name)}</h4>
<div dangerouslySetInnerHTML={{ __html: description}} />
<Form.Group controlId={name}>
<Form.Label>{this.ucfirst(name)}</Form.Label>
<Form.Control type="text" label={name} defaultValue={value} onInput={this.updateValue} />
</Form.Group>
<Button variant="primary" onClick={this.update}>Save</Button>
</div>
</Card.Body>
</Card>
);
}
}
if (document.getElementById('Setting')) {
ReactDOM.render(<Setting />, document.getElementById('Setting'));
}

View File

@@ -1,296 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Card, Form, Button, Modal, Row, Col } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsModalCard from '../Settings/SettingsModalCard';
export default class SettingWithModal extends Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title,
description: this.props.description,
settings: this.props.settings,
show: false,
autoClose: this.props.autoClose
}
}
ucfirst(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
update = () => {
var url = 'api/settings/bulk?token=' + window.token;
var data = [];
var settings = this.state.settings;
settings.forEach(e => {
if(e.type !== 'button-get') {
var res = {
name: e.obj.name,
value: e.obj.value
};
data.push(res);
}
});
data = {
data: data
};
Axios.post(url, data)
.then((resp) => {
toast.success(this.state.title + ' updated');
if(this.state.autoClose) {
this.toggleShow();
}
Axios.get('api/settings/config')
.then((resp) => {
window.config = resp.data;
})
})
.catch((err) => {
if(err.response.status == 422) {
toast.error('Your input was invalid');
} else {
toast.error('Something went wrong')
}
})
}
updateValue = (e) => {
var name = e.target.id;
if(e.target.type == 'checkbox') {
var val = e.target.checked;
} else {
var val = e.target.value;
}
var settings = this.state.settings;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
settings: settings
});
}
toggleShow = () => {
var show = this.state.show;
if(show) {
this.setState({
show: false
});
} else {
this.setState({
show: true
});
}
}
render() {
var title = this.state.title;
var description = this.state.description;
var show = this.state.show;
var settings = this.state.settings;
return (
<>
<SettingsModalCard title={title} description={description} toggleShow={this.toggleShow} />
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>
{settings.map((e,i) => {
var name = e.obj.name.split('_');
name[0] = this.ucfirst(name[0]);
name = name.join(' ');
if(e.obj.description == null || e.obj.description == '') {
var sm = { span: 12 };
var md = { span: 12 };
} else {
var sm = { span: 12 };
var md = { span: 6 };
}
var readonly = false;
if(window.config.editable[e.obj.name] == false) {
readonly = true;
}
if(e.type == 'info') {
return (
<Row key={e.obj.id} className="d-flex align-items-center">
<Col md={md} sm={sm}>
<p>{e.obj.content}</p>
</Col>
</Row>
)
} else if(e.type == 'checkbox') {
return (
<Row key={e.obj.id} className="d-flex align-items-center">
<Col md={md} sm={sm}>
<Form.Group controlId={e.obj.name}>
{readonly ?
<>
<Form.Check type="checkbox" disabled label={name} defaultChecked={Boolean(Number(e.obj.value))} onInput={this.updateValue} />
<Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text>
</>
:
<Form.Check type="checkbox" label={name} defaultChecked={Boolean(Number(e.obj.value))} onInput={this.updateValue} />
}
</Form.Group>
</Col>
{e.description == null &&
<Col md={md} sm={sm}>
<p>{e.obj.description}</p>
</Col>
}
</Row>
);
} else if(e.type == 'number') {
return (
<Row key={e.obj.id}>
<Col md={md} sm={sm}>
<Form.Group controlId={e.obj.name}>
<Form.Label>{name}</Form.Label>
{readonly ?
<>
<Form.Control type="number" disabled min={e.min} max={e.max} defaultValue={e.obj.value} onInput={this.updateValue} />
<Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text>
</>
:
<Form.Control type="number" min={e.min} max={e.max} defaultValue={e.obj.value} onInput={this.updateValue} />
}
</Form.Group>
</Col>
{e.description == null &&
<Col md={md} sm={sm}>
<p>{e.obj.description}</p>
</Col>
}
</Row>
);
} else if(e.type == 'text') {
return (
<Row key={e.obj.id}>
<Col md={md} sm={sm}>
<Form.Group controlId={e.obj.name}>
<Form.Label>{name}</Form.Label>
{readonly ?
<>
<Form.Control type="text" disabled defaultValue={e.obj.value} onInput={this.updateValue} />
<Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text>
</>
:
<Form.Control type="text" defaultValue={e.obj.value} onInput={this.updateValue} />
}
</Form.Group>
</Col>
{e.description == null &&
<Col md={md} sm={sm}>
<p dangerouslySetInnerHTML={{ __html: e.obj.description}}></p>
</Col>
}
</Row>
);
} else if(e.type == 'select') {
return (
<Row key={e.obj.id}>
<Col md={md} sm={sm}>
<Form.Group controlId={e.obj.name}>
<Form.Label>{name}</Form.Label>
{readonly ?
<>
<Form.Control as="select" disabled defaultValue={e.obj.value} onInput={this.updateValue}>
{e.options.map((e,i) => {
return (
<option key={i} value={e.value}>{e.name}</option>
)
})}
</Form.Control>
<Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text>
</>
:
<Form.Control as="select" defaultValue={e.obj.value} onInput={this.updateValue}>
{e.options.map((e,i) => {
return (
<option key={i} value={e.value}>{e.name}</option>
)
})}
</Form.Control>
}
</Form.Group>
</Col>
{e.description == null &&
<Col md={md} sm={sm}>
<p>{e.obj.description}</p>
</Col>
}
</Row>
)
} else if(e.type == 'button-get') {
return (
<Row key={e.obj.id}>
<Col md={md} sm={sm}>
<p>{name}</p>
<Button onClick={() => { Axios.get(e.url) }} >{name}</Button>
</Col>
{e.description == null &&
<Col md={md} sm={sm}>
<p>{e.obj.description}</p>
</Col>
}
</Row>
)
} else if(e.type == 'group') {
return (
<div key={e.obj.id}>
<Row>
<Col md={md} sm={sm}>
<p className="mb-0">{name}</p>
</Col>
{e.description == null &&
<Col md={md} sm={sm}>
<p>{e.obj.description}</p>
</Col>
}
</Row>
<Row>
<Col sm={{ span: 12 }}>
{e.children.map((ee,ii) => {
if(ee.type == 'button-get') {
return (
<Button key={ii} variant={ee.btnType} className={'mr-2 mb-3'} onClick={() => { Axios.get(ee.url)
.then((resp) => { toast.success('Healthcheck sent') })
.catch((resp) => { resp = resp.response; toast.error(resp.data.error) })
}} >{ee.text}</Button>
)
}
})}
</Col>
</Row>
</div>
)
}
})}
<Button variant="primary" type="submit" onClick={this.update} >Save</Button>
</Modal.Body>
</Modal>
</>
);
}
}
if (document.getElementById('Setting')) {
ReactDOM.render(<Setting />, document.getElementById('Setting'));
}

View File

@@ -1,323 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Container, Row, Col, Collapse } from 'react-bootstrap';
import Loader from '../Loader';
import Axios from 'axios';
import Setting from './Setting';
import SettingWithModal from './SettingWithModal';
import ResetSettings from './ResetSettings';
export default class Settings extends Component {
constructor(props) {
super(props)
this.state = {
show: false,
loading: true,
data: [],
}
}
componentDidMount = () => {
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
this.getData();
}
}
toggleShow = () => {
if(this.state.show) {
var show = false;
} else {
var show = true;
}
this.setState({
show: show
});
}
getData = () => {
var url = 'api/settings/?token=' + window.token;
Axios.get(url)
.then((resp) => {
this.setState({
loading: false,
data: resp.data
});
})
.catch((err) => {
if(err.response) {
}
})
}
buildSettingsCards = () => {
var e = this.state.data;
return (
<Row>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="General settings" description="Configure general settings for the app." autoClose={true} settings={[
{
obj: e.schedule,
type: 'text'
},
{
obj: e.server,
type: 'text'
},
{
obj: e.show_average,
type: 'checkbox'
},
{
obj: e.show_max,
type: 'checkbox'
},
{
obj: e.show_min,
type: 'checkbox'
},
]} />
</Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="Graph settings" description="Control settings for the graphs." autoClose={true} settings={[
{
obj: e.download_upload_graph_enabled,
type: 'checkbox'
},
{
obj: e.download_upload_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: e.ping_graph_enabled,
type: 'checkbox'
},
{
obj: e.ping_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: e.failure_graph_enabled,
type: 'checkbox'
},
{
obj: e.failure_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: e.show_failed_tests_on_graph,
type: 'checkbox'
}
]} />
</Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="Notification settings" description="Control which types of notifications the server sends." autoClose={false} settings={[
{
obj: e.slack_webhook,
type: 'text'
},
{
obj: e.telegram_bot_token,
type: 'text'
},
{
obj: e.telegram_chat_id,
type: 'text'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Test notifications",
description: "After saving your updated notification settings, use this to check your settings are correct."
},
type: 'button-get',
url: 'api/settings/test-notification?token=' + window.token
},
{
obj: e.speedtest_notifications,
type: 'checkbox'
},
{
obj: e.speedtest_overview_notification,
type: 'checkbox'
},
{
obj: e.speedtest_overview_time,
type: 'number',
min: 0,
max: 23
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Conditional Notifications",
description: ""
},
type: 'group',
children: [
]
},
{
obj: e.threshold_alert_percentage_notifications,
type: 'checkbox',
},
{
obj: e.threshold_alert_percentage,
type: 'number',
min: 0,
max: 100
},
{
obj: e.threshold_alert_absolute_notifications,
type: 'checkbox',
},
{
obj: e.threshold_alert_absolute_download,
type: 'number',
},
{
obj: e.threshold_alert_absolute_upload,
type: 'number',
},
{
obj: e.threshold_alert_absolute_ping,
type: 'number',
}
]} />
</Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<SettingWithModal title="healthchecks.io settings" description="Control settings for healthchecks.io" autoClose={false} settings={[
{
obj: e.healthchecks_uuid,
type: 'text'
},
{
obj: e.healthchecks_enabled,
type: 'checkbox'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Test healthchecks (after saving)",
description: ""
},
type: 'group',
children: [
{
type: 'button-get',
url: 'api/settings/test-healthchecks/start?token=' + window.token,
btnType: 'outline-success',
text: 'Start',
inline: true,
},
{
type: 'button-get',
url: 'api/settings/test-healthchecks/success?token=' + window.token,
btnType: 'success',
text: 'Success',
inline: true,
},
{
type: 'button-get',
url: 'api/settings/test-healthchecks/fail?token=' + window.token,
btnType: 'danger',
text: 'Fail',
inline: true,
},
]
},
]} />
</Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<ResetSettings />
</Col>
</Row>
)
}
render() {
var show = this.state.show;
var loading = this.state.loading;
var data = this.state.data;
if(!loading) {
var cards = this.buildSettingsCards();
}
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return (
<div>
<Container className="my-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<div className="mouse" onClick={this.toggleShow}>
<h4 className="mb-0 mr-2 d-inline">Settings</h4>
{(show) ?
<span className="ti-angle-up"></span>
:
<span className="ti-angle-down"></span>
}
</div>
</Col>
</Row>
<Collapse in={show}>
<div>
<Row>
<Col sm={{ span: 12 }}>
{loading ?
<Loader small />
:
cards
}
</Col>
</Row>
</div>
</Collapse>
</Container>
</div>
);
} else {
return(
<></>
)
}
}
}
if (document.getElementById('Settings')) {
ReactDOM.render(<Settings />, document.getElementById('Settings'));
}

View File

@@ -1,6 +1,8 @@
import Axios from 'axios'; import Axios from 'axios';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Footer from '../Home/Footer';
import Loader from '../Loader';
import Navbar from '../Navbar'; import Navbar from '../Navbar';
import SettingsTabs from './SettingsTabs'; import SettingsTabs from './SettingsTabs';
@@ -32,6 +34,10 @@ export default class SettingsIndex extends Component {
sortSettings = (data) => { sortSettings = (data) => {
return { return {
General: [ General: [
{
obj: data.app_name,
type: 'text',
},
{ {
obj: data.schedule, obj: data.schedule,
type: 'text', type: 'text',
@@ -53,7 +59,189 @@ export default class SettingsIndex extends Component {
type: 'checkbox', type: 'checkbox',
} }
], ],
Graphs: {} Graphs: [
{
obj: data.download_upload_graph_enabled,
type: 'checkbox',
hideDescription: true
},
{
obj: data.download_upload_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: data.ping_graph_enabled,
type: 'checkbox',
hideDescription: true
},
{
obj: data.ping_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: data.failure_graph_enabled,
type: 'checkbox',
hideDescription: true
},
{
obj: data.failure_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: data.show_failed_tests_on_graph,
type: 'checkbox',
},
],
Notifications: [
{
obj: data.slack_webhook,
type: 'text'
},
{
obj: data.telegram_bot_token,
type: 'text'
},
{
obj: data.telegram_chat_id,
type: 'text'
},
{
type: 'btn-get',
url: 'api/settings/test-notification?token=' + window.token,
btnType: 'primary',
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: 'Test notifications',
description: 'After saving your updated notification settings, use this to check your settings are correct.'
}
},
{
obj: data.speedtest_notifications,
type: 'checkbox'
},
{
obj: data.speedtest_overview_notification,
type: 'checkbox'
},
{
obj: data.speedtest_overview_time,
type: 'number',
min: 0,
max: 23,
},
// Add handling for title stuff
{
obj: data.threshold_alert_percentage,
type: 'number',
min: 0,
max: 100
},
{
obj: data.threshold_alert_absolute_notifications,
type: 'checkbox'
},
{
obj: data.threshold_alert_absolute_download,
type: 'number'
},
{
obj: data.threshold_alert_absolute_upload,
type: 'number'
},
{
obj: data.threshold_alert_absolute_ping,
type: 'number'
},
],
healthchecks: [
{
obj: data.healthchecks_enabled,
type: 'checkbox'
},
{
obj: data.healthchecks_uuid,
type: 'text'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Test healthchecks.io integration",
description: ""
},
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Start",
description: ""
},
type: 'btn-get',
url: 'api/settings/test-healthchecks/start?token=' + window.token,
btnType: 'outline-success',
inline: true,
earlyReturn: true,
classes: 'mr-2'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Success",
description: ""
},
type: 'btn-get',
url: 'api/settings/test-healthchecks/success?token=' + window.token,
btnType: 'success',
text: 'Success',
inline: true,
earlyReturn: true,
classes: 'mr-2'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Fail",
description: ""
},
type: 'btn-get',
url: 'api/settings/test-healthchecks/fail?token=' + window.token,
btnType: 'danger',
text: 'Fail',
inline: true,
earlyReturn: true,
classes: 'mr-2'
},
]
}; };
} }
@@ -65,18 +253,17 @@ export default class SettingsIndex extends Component {
var data = this.state.data; var data = this.state.data;
var loading = this.state.loading; var loading = this.state.loading;
if(loading) {
return (
<div>Loading</div>
);
}
return ( return (
<div> <div>
<Navbar /> <Navbar />
<div className="container my-5"> <div className="container my-5">
{loading ?
<Loader />
:
<SettingsTabs data={data} /> <SettingsTabs data={data} />
}
</div> </div>
<Footer />
</div> </div>
); );
} }

View File

@@ -9,12 +9,21 @@ export default class SettingsInput extends Component {
this.state = { this.state = {
type: this.props.type, type: this.props.type,
name: this.props.name, name: this.props.name,
displayName: (this.props.name) ? this.formatName(this.props.name) : '',
value: (this.props.value) ? this.props.value : '', value: (this.props.value) ? this.props.value : '',
classes: this.props.classes, classes: this.props.classes,
id: this.props.id, id: this.props.id,
label: (this.props.label) ? this.props.label : false, label: (this.props.label) ? this.props.label : false,
readonly: true, readonly: true,
description: (this.props.description) ? this.props.description : false, description: (this.props.description) ? this.props.description : false,
options: this.props.options ? this.props.options : [],
hideDescription: this.props.hideDescription ? true : false,
min: this.props.min ? this.props.min : null,
max: this.props.max ? this.props.max : null,
url: this.props.url,
inline: this.props.inline ? 'd-inline-block' : 'd-block',
btnType: this.props.btnType,
earlyReturn: this.props.earlyReturn ? true : false,
} }
} }
@@ -24,15 +33,21 @@ export default class SettingsInput extends Component {
}); });
} }
formatName(name) {
name = name.split('_').join(' ');
return name.charAt(0).toUpperCase() + name.slice(1);
}
handleInput = (evt) => { handleInput = (evt) => {
var val = evt.target.value; var val = evt.target.value;
if(this.state.type === 'checkbox') { if(this.state.type === 'checkbox') {
val = e.target.checked; val = evt.target.checked;
} }
this.props.handleInput( this.props.handler(
this.state.name.split(' ').join('_'), this.state.name,
val val
); );
@@ -49,9 +64,46 @@ export default class SettingsInput extends Component {
return false; return false;
} }
generateInput = () => { generateNumberInput(disabled) {
var disabled = (this.state.readonly) ? true : false; return <Form.Control
name={this.state.name}
type={this.state.type}
defaultValue={this.state.value}
disabled={disabled}
min={this.state.min}
max={this.state.max}
onInput={this.handleInput} />
}
generateSelectInput(disabled) {
return (
<Form.Control
as="select"
name={this.state.name}
type={this.state.type}
defaultValue={this.state.value}
disabled={disabled}
onInput={this.handleInput}
>
{this.state.options.map((option,i) => {
return <option key={i} value={option.value}>{option.name}</option>
})}
</Form.Control>
);
}
generateCheckboxInput(disabled) {
return <Form.Control
custom
className="ml-2"
name={this.state.name}
type={this.state.type}
defaultChecked={this.state.value}
disabled={disabled}
onInput={this.handleInput} />
}
generateTextInput(disabled) {
return <Form.Control return <Form.Control
name={this.state.name} name={this.state.name}
type={this.state.type} type={this.state.type}
@@ -60,31 +112,72 @@ export default class SettingsInput extends Component {
onInput={this.handleInput} /> onInput={this.handleInput} />
} }
render() { generateButtonGetInput() {
var input = this.generateInput(); var url = this.state.url;
var id = this.state.id;
var readonly = this.state.readonly;
var label = this.state.label;
var description = this.state.description;
return ( return (
<Form.Group controlId={id}> <button
{label && type="button"
<Form.Label dangerouslySetInnerHTML={{ __html: label }} /> className={"btn btn-" + this.state.btnType + ' ' + this.state.inline + ' ' + this.state.classes}
onClick={() => {
window.axios.get(url)
}}
>{this.state.displayName}</button>
);
}
generateInput = () => {
var disabled = (this.state.readonly) ? true : false;
var input = null;
if(this.state.type === 'number') {
input = this.generateNumberInput(disabled);
}
if(this.state.type === 'select') {
input = this.generateSelectInput(disabled);
}
if(this.state.type === 'checkbox') {
input = this.generateCheckboxInput(disabled);
}
if(this.state.type === 'text') {
input = this.generateTextInput(disabled);
}
if(this.state.type === 'btn-get') {
input = this.generateButtonGetInput();
}
if(this.state.earlyReturn) {
return input;
}
return (
<Form.Group controlId={this.state.id}>
{this.state.label &&
<Form.Label style={{fontSize: '1.25rem'}}>{this.formatName(this.state.name)}</Form.Label>
} }
{input} {input}
{description && {this.state.description && !this.state.hideDescription &&
<p dangerouslySetInnerHTML={{ __html: description }}></p> <p className="mt-1 text-muted" dangerouslySetInnerHTML={{ __html: this.state.description }}></p>
} }
{readonly && {this.state.readonly &&
<Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text> <Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text>
} }
</Form.Group> </Form.Group>
); );
} }
render() {
var input = this.generateInput();
return input;
}
} }
if (document.getElementById('SettingsInput')) { if (document.getElementById('SettingsInput')) {

View File

@@ -1,37 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Card, Button } from 'react-bootstrap';
export default class SettingsModalCard extends Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title,
description: this.props.description,
toggleShow: this.props.toggleShow,
}
}
render() {
var title = this.state.title;
var description = this.state.description;
var toggleShow = this.state.toggleShow;
return (
<Card className="m-2 setting-card">
<Card.Body className="d-flex align-items-center">
<div>
<h4>{title}</h4>
<p>{description}</p>
<Button variant="primary" onClick={toggleShow}>Edit</Button>
</div>
</Card.Body>
</Card>
);
}
}
if (document.getElementById('SettingModalCard')) {
ReactDOM.render(<SettingsModalCard />, document.getElementById('SettingModalCard'));
}

View File

@@ -1,8 +1,16 @@
import Axios from 'axios';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Nav, Tab, Tabs } from 'react-bootstrap'; import { Nav, Tab, Tabs } from 'react-bootstrap';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { toast } from 'react-toastify';
import SettingsInput from './SettingsInput';
import ResetSettings from './tabs/ResetSettings';
import BackupSettings from './tabs/BackupSettings';
import GeneralSettings from './tabs/GeneralSettings'; import GeneralSettings from './tabs/GeneralSettings';
import GraphsSettings from './tabs/GraphsSettings'; import GraphsSettings from './tabs/GraphsSettings';
import HealthchecksSettings from './tabs/HealthchecksSettings';
import NotificationsSettings from './tabs/NotificationsSettings';
import Authentication from '../Authentication/Authentication';
export default class SettingsTabs extends Component { export default class SettingsTabs extends Component {
constructor(props) { constructor(props) {
@@ -21,8 +29,13 @@ export default class SettingsTabs extends Component {
'Notifications', 'Notifications',
'healthchecks.io', 'healthchecks.io',
'Reset', 'Reset',
'Backup/Restore',
]; ];
if(window.config.auth) {
tabs.push('Authentication');
}
return tabs.map((tab) => { return tabs.map((tab) => {
return <Tab key={tab} eventKey={tab} title={tab} /> return <Tab key={tab} eventKey={tab} title={tab} />
}); });
@@ -34,17 +47,105 @@ export default class SettingsTabs extends Component {
}); });
} }
save = (settings, name) => {
var url = 'api/settings/bulk?token=' + window.token;
var data = [];
settings.forEach(e => {
if(e.type !== 'btn-get') {
var res = {
name: e.obj.name,
value: e.obj.value
};
data.push(res);
}
});
data = {
data: data
};
Axios.post(url, data)
.then((resp) => {
toast.success(name + ' settings updated');
Axios.get('api/settings/config')
.then((resp) => {
window.config = resp.data;
})
})
.catch((err) => {
if(err.response.status == 422) {
toast.error('Your input was invalid');
} else {
toast.error('Something went wrong')
}
})
}
generateInputs = (settings, handler) => {
return settings.map((setting) => {
return <SettingsInput
key={setting.obj.id}
name={setting.obj.name}
id={setting.obj.id}
type={setting.type}
value={setting.obj.value}
description={setting.obj.description}
handler={handler}
label={setting.obj.name}
description={setting.obj.description}
options={setting.type == 'select' ? setting.options : []}
hideDescription={setting.hideDescription ? setting.hideDescription : false}
min={setting.min ? setting.min : false}
min={setting.max ? setting.max : false}
btnType={setting.btnType}
inline={setting.inline}
url={setting.url}
earlyReturn={setting.earlyReturn ? true : false}
classes={setting.classes ? setting.classes : ''}
/>
})
}
getTabContent = () => { getTabContent = () => {
var data = this.state.data; var data = this.state.data;
console.log(data);
switch(this.state.tab) { switch(this.state.tab) {
case 'General': case 'General':
return <GeneralSettings data={data.General} /> return <GeneralSettings
break; data={data.General}
generateInputs={this.generateInputs}
save={this.save} />
case 'Graphs': case 'Graphs':
return <GraphsSettings /> return <GraphsSettings
break; data={data.Graphs}
generateInputs={this.generateInputs}
save={this.save} />
case 'Notifications':
return <NotificationsSettings
data={data.Notifications}
generateInputs={this.generateInputs}
save={this.save} />
case 'healthchecks.io':
return <HealthchecksSettings
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
case 'Reset':
return <ResetSettings
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
case 'Backup/Restore':
return <BackupSettings
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
case 'Authentication':
return <Authentication
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
} }
} }

View File

@@ -0,0 +1,26 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import DataRow from '../../Data/DataRow';
export default class BackupSettings extends Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
return (
<Tab.Content>
<DataRow />
</Tab.Content>
);
}
}
if (document.getElementById('BackupSettings')) {
ReactDOM.render(<BackupSettings />, document.getElementById('BackupSettings'));
}

View File

@@ -2,8 +2,6 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap'; import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios'; import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsInput from '../SettingsInput';
export default class GeneralSettings extends Component { export default class GeneralSettings extends Component {
constructor(props) { constructor(props) {
@@ -14,7 +12,7 @@ export default class GeneralSettings extends Component {
} }
} }
inputHandler = () => { inputHandler = (name, val) => {
var settings = this.state.data; var settings = this.state.data;
var i = 0; var i = 0;
settings.forEach(ele => { settings.forEach(ele => {
@@ -30,21 +28,14 @@ export default class GeneralSettings extends Component {
} }
render() { render() {
var settings = this.state.data[0]; var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return ( return (
<Tab.Content> <Tab.Content>
General {settings}
<SettingsInput <div className="mt-3">
name={settings.obj.name} <button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'General') }}>Save</button>
id={settings.obj.id} </div>
type={settings.type}
value={settings.obj.value}
description={settings.obj.description}
handler={this.inputHandler}
label={settings.obj.name.split('_').join(' ')}
description={settings.obj.description}
/>
</Tab.Content> </Tab.Content>
); );
} }

View File

@@ -0,0 +1,48 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsInput from '../SettingsInput';
export default class GraphsSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'General') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('GraphsSettings')) {
ReactDOM.render(<GraphsSettings />, document.getElementById('GraphsSettings'));
}

View File

@@ -0,0 +1,48 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsInput from '../SettingsInput';
export default class HealthchecksSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'healthchecks.io') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('HealthchecksSettings')) {
ReactDOM.render(<HealthchecksSettings />, document.getElementById('HealthchecksSettings'));
}

View File

@@ -0,0 +1,46 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
export default class NotificationsSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'Notifications') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('NotificationsSettings')) {
ReactDOM.render(<NotificationsSettings />, document.getElementById('NotificationsSettings'));
}

View File

@@ -1,7 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Modal, Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import SettingsModalCard from './SettingsModalCard';
import Axios from 'axios'; import Axios from 'axios';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -10,15 +9,6 @@ export default class ResetSettings extends Component {
super(props) super(props)
this.state = { this.state = {
show: false,
}
}
toggleShow = () => {
if(this.state.show) {
this.setState({ show: false });
} else {
this.setState({ show:true });
} }
} }
@@ -45,17 +35,9 @@ export default class ResetSettings extends Component {
return ( return (
<> <>
<SettingsModalCard title={title} description="Bulk delete speedtests from the database." toggleShow={this.toggleShow} />
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>
<h4>Clear all speedtests</h4> <h4>Clear all speedtests</h4>
<p className="text-muted">If using SQLite, a backup of the database will be stored in the location of the current database.</p> <p className="text-muted">If using SQLite, a backup of the database will be stored in the location of the current database.</p>
<Button onClick={this.deleteAll} variant="danger">Delete all</Button> <Button onClick={this.deleteAll} variant="danger">Delete all</Button>
</Modal.Body>
</Modal>
</> </>
); );
} }

View File

@@ -0,0 +1,29 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import TestsTable from './Graphics/TestsTable';
import Footer from './Home/Footer';
import Navbar from './Navbar';
export default class SpeedtestsPage extends Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
return (
<div>
<Navbar />
<TestsTable />
<Footer />
</div>
);
}
}
if (document.getElementById('SpeedtestsPage')) {
ReactDOM.render(<SpeedtestsPage />, document.getElementById('SpeedtestsPage'));
}

View File

@@ -9,6 +9,7 @@ import 'react-toastify/dist/ReactToastify.css';
import HomePage from './components/Home/HomePage'; import HomePage from './components/Home/HomePage';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import SettingsIndex from './components/Settings/SettingsIndex'; import SettingsIndex from './components/Settings/SettingsIndex';
import SpeedtestsPage from './components/SpeedtestsPage';
export default class Index extends Component { export default class Index extends Component {
constructor(props) { constructor(props) {
@@ -85,6 +86,12 @@ export default class Index extends Component {
<HomePage /> <HomePage />
</div> </div>
)} /> )} />
<Route exact path={window.config.base + 'speedtests'} render={(props) => (
<div>
<SpeedtestsPage />
</div>
)} />
<Route exact path={window.config.base + 'settings'} render={(props) => ( <Route exact path={window.config.base + 'settings'} render={(props) => (
<div> <div>
<SettingsIndex /> <SettingsIndex />