feat: use vite as bundler

This commit is contained in:
Anton Bracke 2021-05-27 09:23:01 +02:00
parent 35f7772cbb
commit b64ae40dcf
21 changed files with 171 additions and 1799 deletions

5
webui/.gitignore vendored
View File

@ -17,3 +17,8 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
package-lock.json package-lock.json
node_modules
.DS_Store
dist
dist-ssr
*.local

File diff suppressed because it is too large Load Diff

View File

@ -3,20 +3,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> <link rel="shortcut icon" href="/src/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>AES67 Daemon WebUI</title> <title>AES67 Daemon WebUI</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.

View File

@ -2,19 +2,21 @@
"name": "aes67-daemon-webui", "name": "aes67-daemon-webui",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": {
"start": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": { "dependencies": {
"react": "^16.11.0", "react": "^17.0.0",
"react-dom": "^16.11.0", "react-dom": "^17.0.0",
"react-modal": "^3.11.1", "react-modal": "^3.11.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "0.9.5", "react-scripts": "0.9.5",
"react-toastify": "^5.5.0" "react-toastify": "^5.5.0"
}, },
"devDependencies": {}, "devDependencies": {
"scripts": { "@vitejs/plugin-react-refresh": "^1.3.1",
"start": "react-scripts start", "vite": "^2.3.4"
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
} }
} }

View File

@ -1,5 +1,5 @@
// //
// index.js // App.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -26,6 +26,8 @@ import 'react-toastify/dist/ReactToastify.css';
import ConfigTabs from './ConfigTabs'; import ConfigTabs from './ConfigTabs';
import './styles.css';
toast.configure() toast.configure()
function App() { function App() {
@ -45,6 +47,4 @@ function App() {
); );
} }
const container = document.createElement('div'); export default App
document.body.appendChild(container);
render( <App/>, container);

View File

@ -1,5 +1,5 @@
// //
// Config.js // Config.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -19,13 +19,11 @@
// //
import React, {Component} from 'react'; import React, {Component} from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import RestAPI from './Services'; import RestAPI from './Services';
import Loader from './Loader'; import Loader from './Loader';
require('./styles.css');
class Config extends Component { class Config extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -107,7 +105,7 @@ class Config extends Component {
})) }))
.catch(err => this.setState({isConfigLoading: false})); .catch(err => this.setState({isConfigLoading: false}));
} }
inputIsValid() { inputIsValid() {
return !this.state.playoutDelayErr && return !this.state.playoutDelayErr &&
!this.state.maxTicFrameSizeErr && !this.state.maxTicFrameSizeErr &&
@ -124,22 +122,22 @@ class Config extends Component {
onSubmit(event) { onSubmit(event) {
event.preventDefault(); event.preventDefault();
RestAPI.setConfig( RestAPI.setConfig(
this.state.logSeverity, this.state.logSeverity,
this.state.syslogProto, this.state.syslogProto,
this.state.syslogServer, this.state.syslogServer,
this.state.rtpMcastBase, this.state.rtpMcastBase,
this.state.rtpPort, this.state.rtpPort,
this.state.rtspPort, this.state.rtspPort,
this.state.playoutDelay, this.state.playoutDelay,
this.state.ticFrameSizeAt1fs, this.state.ticFrameSizeAt1fs,
this.state.sampleRate, this.state.sampleRate,
this.state.maxTicFrameSize, this.state.maxTicFrameSize,
this.state.sapMcastAddr, this.state.sapMcastAddr,
this.state.sapInterval, this.state.sapInterval,
this.state.mdnsEnabled) this.state.mdnsEnabled)
.then(response => toast.success('Config updated, daemon restart ...')); .then(response => toast.success('Config updated, daemon restart ...'));
} }
render() { render() {
return ( return (
<div className='config'> <div className='config'>
@ -175,7 +173,7 @@ class Config extends Component {
</tr> </tr>
<tr> <tr>
<th align="left"> <label>Initial Sample rate</label> </th> <th align="left"> <label>Initial Sample rate</label> </th>
<th align="left"> <th align="left">
<select value={this.state.sampleRate} onChange={e => this.setState({sampleRate: e.target.value})}> <select value={this.state.sampleRate} onChange={e => this.setState({sampleRate: e.target.value})}>
<option value="44100">44.1 kHz</option> <option value="44100">44.1 kHz</option>
<option value="48000">48 kHz</option> <option value="48000">48 kHz</option>
@ -237,7 +235,7 @@ class Config extends Component {
<table><tbody> <table><tbody>
<tr> <tr>
<th align="left"> <label>Syslog protocol</label> </th> <th align="left"> <label>Syslog protocol</label> </th>
<th align="left"> <th align="left">
<select value={this.state.syslogProto} onChange={e => this.setState({syslogProto: e.target.value})}> <select value={this.state.syslogProto} onChange={e => this.setState({syslogProto: e.target.value})}>
<option value="none">none</option> <option value="none">none</option>
<option value="">local</option> <option value="">local</option>
@ -251,7 +249,7 @@ class Config extends Component {
</tr> </tr>
<tr> <tr>
<th align="left"> <label>Log severity</label> </th> <th align="left"> <label>Log severity</label> </th>
<th align="left"> <th align="left">
<select value={this.state.logSeverity} onChange={e => this.setState({logSeverity: e.target.value})}> <select value={this.state.logSeverity} onChange={e => this.setState({logSeverity: e.target.value})}>
<option value="1">debug</option> <option value="1">debug</option>
<option value="2">info</option> <option value="2">info</option>

View File

@ -1,5 +1,5 @@
// //
// ConfigTabs.js // ConfigTabs.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -28,15 +28,13 @@ import Sources from './Sources';
import Sinks from './Sinks'; import Sinks from './Sinks';
import RemoteSources from './RemoteSources'; import RemoteSources from './RemoteSources';
require('./styles.css');
class ConfigTabs extends Component { class ConfigTabs extends Component {
static propTypes = { static propTypes = {
currentTab: PropTypes.string.isRequired currentTab: PropTypes.string.isRequired
}; };
render() { render() {
return ( return (
<div> <div>
<h1>AES67 Daemon</h1> <h1>AES67 Daemon</h1>
<Tabs currentTab={this.props.currentTab}> <Tabs currentTab={this.props.currentTab}>

View File

@ -1,5 +1,5 @@
// //
// Loader.js // Loader.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //

View File

@ -1,5 +1,5 @@
// //
// PTP.js // PTP.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -25,8 +25,6 @@ import {toast} from 'react-toastify';
import RestAPI from './Services'; import RestAPI from './Services';
import Loader from './Loader'; import Loader from './Loader';
require('./styles.css');
class PTPConfig extends Component { class PTPConfig extends Component {
static propTypes = { static propTypes = {
@ -141,9 +139,9 @@ class PTP extends Component {
.then(response => response.json()) .then(response => response.json())
.then( .then(
data => this.setState({ data => this.setState({
status: data.status, status: data.status,
gmid: data.gmid, gmid: data.gmid,
jitter: parseInt(data.jitter, 10), jitter: parseInt(data.jitter, 10),
isStatusLoading: false isStatusLoading: false
})) }))
.catch(err => this.setState({isStatusLoading: false})); .catch(err => this.setState({isStatusLoading: false}));
@ -155,19 +153,19 @@ class PTP extends Component {
.then(response => response.json()) .then(response => response.json())
.then( .then(
data => this.setState({ data => this.setState({
domain: parseInt(data.domain, 10), domain: parseInt(data.domain, 10),
dscp: parseInt(data.dscp, 10), dscp: parseInt(data.dscp, 10),
isConfigLoading: false isConfigLoading: false
})) }))
.catch(err => this.setState({isConfigLoading: false})); .catch(err => this.setState({isConfigLoading: false}));
} }
componentDidMount() { componentDidMount() {
this.fetchStatus(); this.fetchStatus();
this.fetchConfig(); this.fetchConfig();
this.interval = setInterval(() => { this.fetchStatus() }, 10000) this.interval = setInterval(() => { this.fetchStatus() }, 10000)
} }
componentWillUnmount() { componentWillUnmount() {
clearInterval(this.interval); clearInterval(this.interval);
} }
@ -175,10 +173,10 @@ class PTP extends Component {
render() { render() {
return ( return (
<div className='ptp'> <div className='ptp'>
{ this.state.isConfigLoading ? <Loader/> : { this.state.isConfigLoading ? <Loader/> :
<PTPConfig domain={this.state.domain} dscp={this.state.dscp}/> } <PTPConfig domain={this.state.domain} dscp={this.state.dscp}/> }
<br/> <br/>
{ this.state.isStatusLoading ? <Loader/> : { this.state.isStatusLoading ? <Loader/> :
<PTPStatus status={this.state.status} gmid={this.state.gmid} jitter={this.state.jitter}/> } <PTPStatus status={this.state.status} gmid={this.state.gmid} jitter={this.state.jitter}/> }
</div> </div>
) )

View File

@ -1,5 +1,5 @@
// //
// RemoteSource.js // RemoteSource.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -25,8 +25,6 @@ import RestAPI from './Services';
import Loader from './Loader'; import Loader from './Loader';
import SourceInfo from './SourceInfo'; import SourceInfo from './SourceInfo';
require('./styles.css');
class RemoteSourceEntry extends Component { class RemoteSourceEntry extends Component {
static propTypes = { static propTypes = {
source: PropTypes.string.isRequired, source: PropTypes.string.isRequired,
@ -42,9 +40,9 @@ class RemoteSourceEntry extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
rtp_address: 'n/a', rtp_address: 'n/a',
port: 'n/a' port: 'n/a'
}; };
} }
@ -91,7 +89,7 @@ class RemoteSourceList extends Component {
return ( return (
<div id='remote-sources-table'> <div id='remote-sources-table'>
<table className="table-stream"><tbody> <table className="table-stream"><tbody>
{this.props.sources.length > 0 ? {this.props.sources.length > 0 ?
<tr className='tr-stream'> <tr className='tr-stream'>
<th>Source</th> <th>Source</th>
<th>Address</th> <th>Address</th>
@ -118,11 +116,11 @@ class RemoteSourceList extends Component {
class RemoteSources extends Component { class RemoteSources extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
sources: [], sources: [],
isLoading: false, isLoading: false,
infoIsOpen: false, infoIsOpen: false,
infoTitle: '' infoTitle: ''
}; };
this.onInfoClick = this.onInfoClick.bind(this); this.onInfoClick = this.onInfoClick.bind(this);
this.onReloadClick = this.onReloadClick.bind(this); this.onReloadClick = this.onReloadClick.bind(this);
@ -152,7 +150,7 @@ class RemoteSources extends Component {
this.setState({infoIsOpen: false}); this.setState({infoIsOpen: false});
this.fetchRemoteSources(); this.fetchRemoteSources();
} }
onInfoClick(id) { onInfoClick(id) {
const source = this.state.sources.find(s => s.id === id); const source = this.state.sources.find(s => s.id === id);
this.openInfo("Announced Source Info", source, true); this.openInfo("Announced Source Info", source, true);
@ -179,14 +177,14 @@ class RemoteSources extends Component {
)); ));
return ( return (
<div id='sources'> <div id='sources'>
{ this.state.isLoading ? <Loader/> { this.state.isLoading ? <Loader/>
: <RemoteSourceList : <RemoteSourceList
onReloadClick={this.onReloadClick} onReloadClick={this.onReloadClick}
sources={sources} /> } sources={sources} /> }
{ this.state.infoIsOpen ? { this.state.infoIsOpen ?
<SourceInfo infoIsOpen={this.state.infoIsOpen} <SourceInfo infoIsOpen={this.state.infoIsOpen}
closeInfo={this.closeInfo} closeInfo={this.closeInfo}
infoTitle={this.state.infoTitle} infoTitle={this.state.infoTitle}
id={this.state.source.id} id={this.state.source.id}
source={this.state.source.source} source={this.state.source.source}
name={this.state.source.name} name={this.state.source.name}

View File

@ -1,5 +1,5 @@
// //
// SinkEdit.js // SinkEdit.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -25,8 +25,6 @@ import Modal from 'react-modal';
import RestAPI from './Services'; import RestAPI from './Services';
require('./styles.css');
const editCustomStyles = { const editCustomStyles = {
content : { content : {
top: '50%', top: '50%',
@ -102,9 +100,9 @@ class SinkEdit extends Component {
this.state.source ? this.state.source : '', this.state.source ? this.state.source : '',
this.state.sdp ? this.state.sdp : '', this.state.sdp ? this.state.sdp : '',
this.state.ignoreRefclkGmid, this.state.ignoreRefclkGmid,
this.state.map, this.state.map,
this.props.isEdit) this.props.isEdit)
.then(function(response) { .then(function(response) {
this.props.applyEdit(); this.props.applyEdit();
toast.success(message); toast.success(message);
}.bind(this)); }.bind(this));
@ -148,8 +146,8 @@ class SinkEdit extends Component {
} }
inputIsValid() { inputIsValid() {
return !this.state.nameErr && return !this.state.nameErr &&
!this.state.sourceErr && !this.state.sourceErr &&
(this.state.useSdp || this.state.source) && (this.state.useSdp || this.state.source) &&
(!this.state.useSdp || this.state.sdp); (!this.state.useSdp || this.state.sdp);
} }
@ -157,7 +155,7 @@ class SinkEdit extends Component {
render() { render() {
return ( return (
<div id='sink-edit'> <div id='sink-edit'>
<Modal ariaHideApp={false} <Modal ariaHideApp={false}
isOpen={this.props.editIsOpen} isOpen={this.props.editIsOpen}
onRequestClose={this.props.closeEdit} onRequestClose={this.props.closeEdit}
style={editCustomStyles} style={editCustomStyles}
@ -185,7 +183,7 @@ class SinkEdit extends Component {
<th align="left"> <th align="left">
<select value={this.state.sdp} onChange={this.onChangeRemoteSourceSDP} disabled={this.state.useSdp ? undefined : true}> <select value={this.state.sdp} onChange={this.onChangeRemoteSourceSDP} disabled={this.state.useSdp ? undefined : true}>
<option key='' value=''> -- select a remote source SDP -- </option> <option key='' value=''> -- select a remote source SDP -- </option>
{ {
this.state.sources.map((v) => <option key={v.id} value={v.sdp}>{v.source + ' from ' + v.address + ' - ' + v.name}</option>) this.state.sources.map((v) => <option key={v.id} value={v.sdp}>{v.source + ' from ' + v.address + ' - ' + v.name}</option>)
} }
</select> </select>
@ -197,7 +195,7 @@ class SinkEdit extends Component {
</tr> </tr>
<tr> <tr>
<th align="left"> <label>Delay (samples) </label> </th> <th align="left"> <label>Delay (samples) </label> </th>
<th align="left"> <th align="left">
<select value={this.state.delay} onChange={e => this.setState({delay: e.target.value})}> <select value={this.state.delay} onChange={e => this.setState({delay: e.target.value})}>
<option value="192">192 - 4ms@48KHz</option> <option value="192">192 - 4ms@48KHz</option>
<option value="384">384 - 8ms@48KHz</option> <option value="384">384 - 8ms@48KHz</option>
@ -219,8 +217,8 @@ class SinkEdit extends Component {
<th align="left">Audio Channels map</th> <th align="left">Audio Channels map</th>
<th align="left"> <th align="left">
<select value={this.state.map[0]} onChange={this.onChangeChannelsMap}> <select value={this.state.map[0]} onChange={this.onChangeChannelsMap}>
{ this.state.channels > 1 ? { this.state.channels > 1 ?
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Input ' + parseInt(v + 1, 10) + ' -> ALSA Input ' + parseInt(v + this.state.channels, 10)}</option>) : this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Input ' + parseInt(v + 1, 10) + ' -> ALSA Input ' + parseInt(v + this.state.channels, 10)}</option>) :
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Input ' + parseInt(v + 1, 10)}</option>) this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Input ' + parseInt(v + 1, 10)}</option>)
} }
</select> </select>

View File

@ -1,5 +1,5 @@
// //
// SinkRemove.js // SinkRemove.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -25,8 +25,6 @@ import Modal from 'react-modal';
import RestAPI from './Services'; import RestAPI from './Services';
require('./styles.css');
const removeCustomStyles = { const removeCustomStyles = {
content : { content : {
top: '50%', top: '50%',
@ -58,7 +56,7 @@ class SinkRemove extends Component {
} }
removeSink(message) { removeSink(message) {
RestAPI.removeSink(this.props.sink.id).then(function(response) { RestAPI.removeSink(this.props.sink.id).then(function(response) {
toast.success(message); toast.success(message);
this.props.applyEdit(); this.props.applyEdit();
}.bind(this)); }.bind(this));
@ -75,7 +73,7 @@ class SinkRemove extends Component {
render() { render() {
return ( return (
<div id='sink-remove'> <div id='sink-remove'>
<Modal ariaHideApp={false} <Modal ariaHideApp={false}
isOpen={this.props.removeIsOpen} isOpen={this.props.removeIsOpen}
onRequestClose={this.props.closeEdit} onRequestClose={this.props.closeEdit}
style={removeCustomStyles} style={removeCustomStyles}

View File

@ -1,5 +1,5 @@
// //
// Sinks.js // Sinks.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -26,8 +26,6 @@ import Loader from './Loader';
import SinkEdit from './SinkEdit'; import SinkEdit from './SinkEdit';
import SinkRemove from './SinkRemove'; import SinkRemove from './SinkRemove';
require('./styles.css');
class SinkEntry extends Component { class SinkEntry extends Component {
static propTypes = { static propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
@ -39,10 +37,10 @@ class SinkEntry extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
min_time: 'n/a', min_time: 'n/a',
flags: 'n/a', flags: 'n/a',
errors: 'n/a' errors: 'n/a'
}; };
} }
@ -76,8 +74,8 @@ class SinkEntry extends Component {
flags += (flags ? ',' : '') + 'all muted'; flags += (flags ? ',' : '') + 'all muted';
if (status.sink_flags._muted) if (status.sink_flags._muted)
flags += (flags ? ',' : '') + 'muted'; flags += (flags ? ',' : '') + 'muted';
this.setState({ this.setState({
min_time: status.sink_min_time + ' ms', min_time: status.sink_min_time + ' ms',
flags: flags ? flags : 'idle', flags: flags ? flags : 'idle',
errors: errors ? errors : 'none' errors: errors ? errors : 'none'
}); });
@ -119,7 +117,7 @@ class SinkList extends Component {
return ( return (
<div id='sinks-table'> <div id='sinks-table'>
<table className="table-stream"><tbody> <table className="table-stream"><tbody>
{this.props.sinks.length > 0 ? {this.props.sinks.length > 0 ?
<tr className='tr-stream'> <tr className='tr-stream'>
<th>ID</th> <th>ID</th>
<th>Name</th> <th>Name</th>
@ -137,7 +135,7 @@ class SinkList extends Component {
<span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span> <span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span>
&nbsp;&nbsp; &nbsp;&nbsp;
{this.props.sinks.length < 64 ? {this.props.sinks.length < 64 ?
<span className='pointer-area' onClick={this.handleAddClick}> <img width='30' height='30' src='/plus.png' alt=''/> </span> <span className='pointer-area' onClick={this.handleAddClick}> <img width='30' height='30' src='/plus.png' alt=''/> </span>
: undefined} : undefined}
</div> </div>
); );
@ -147,14 +145,14 @@ class SinkList extends Component {
class Sinks extends Component { class Sinks extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
sinks: [], sinks: [],
sink: {}, sink: {},
isLoading: false, isLoading: false,
isEdit: false, isEdit: false,
editIsOpen: false, editIsOpen: false,
removeIsOpen: false, removeIsOpen: false,
editTitle: '' editTitle: ''
}; };
this.onEditClick = this.onEditClick.bind(this); this.onEditClick = this.onEditClick.bind(this);
this.onTrashClick = this.onTrashClick.bind(this); this.onTrashClick = this.onTrashClick.bind(this);
@ -191,13 +189,13 @@ class Sinks extends Component {
onReloadClick() { onReloadClick() {
this.fetchSinks(); this.fetchSinks();
} }
closeEdit() { closeEdit() {
this.setState({editIsOpen: false}); this.setState({editIsOpen: false});
this.setState({removeIsOpen: false}); this.setState({removeIsOpen: false});
this.fetchSinks(); this.fetchSinks();
} }
onEditClick(id) { onEditClick(id) {
const sink = this.state.sinks.find(s => s.id === id); const sink = this.state.sinks.find(s => s.id === id);
this.openEdit("Edit Sink " + id, sink, true); this.openEdit("Edit Sink " + id, sink, true);
@ -209,15 +207,15 @@ class Sinks extends Component {
} }
onAddClick() { onAddClick() {
let id; let id;
/* find first free id */ /* find first free id */
for (id = 0; id < 63; id++) { for (id = 0; id < 63; id++) {
if (this.state.sinks[id] === undefined || if (this.state.sinks[id] === undefined ||
this.state.sinks[id].id !== id) { this.state.sinks[id].id !== id) {
break; break;
} }
} }
const defaultSink = { const defaultSink = {
'id': id, 'id': id,
'name': 'ALSA Sink ' + id, 'name': 'ALSA Sink ' + id,
'io': 'Audio Device', 'io': 'Audio Device',
@ -230,7 +228,7 @@ class Sinks extends Component {
}; };
this.openEdit('Add Sink ' + id, defaultSink, false); this.openEdit('Add Sink ' + id, defaultSink, false);
} }
render() { render() {
this.state.sinks.sort((a, b) => (a.id > b.id) ? 1 : -1); this.state.sinks.sort((a, b) => (a.id > b.id) ? 1 : -1);
const sinks = this.state.sinks.map((sink) => ( const sinks = this.state.sinks.map((sink) => (
@ -244,15 +242,15 @@ class Sinks extends Component {
)); ));
return ( return (
<div id='sinks'> <div id='sinks'>
{ this.state.isLoading ? <Loader/> { this.state.isLoading ? <Loader/>
: <SinkList onAddClick={this.onAddClick} : <SinkList onAddClick={this.onAddClick}
onReloadClick={this.onReloadClick} onReloadClick={this.onReloadClick}
sinks={sinks} /> } sinks={sinks} /> }
{ this.state.editIsOpen ? { this.state.editIsOpen ?
<SinkEdit editIsOpen={this.state.editIsOpen} <SinkEdit editIsOpen={this.state.editIsOpen}
closeEdit={this.closeEdit} closeEdit={this.closeEdit}
applyEdit={this.applyEdit} applyEdit={this.applyEdit}
editTitle={this.state.editTitle} editTitle={this.state.editTitle}
isEdit={this.state.isEdit} isEdit={this.state.isEdit}
sink={this.state.sink} /> sink={this.state.sink} />
: undefined } : undefined }
@ -260,7 +258,7 @@ class Sinks extends Component {
<SinkRemove removeIsOpen={this.state.removeIsOpen} <SinkRemove removeIsOpen={this.state.removeIsOpen}
closeEdit={this.closeEdit} closeEdit={this.closeEdit}
applyEdit={this.applyEdit} applyEdit={this.applyEdit}
sink={this.state.sink} sink={this.state.sink}
key={this.state.sink.id} /> key={this.state.sink.id} />
: undefined } : undefined }
</div> </div>

View File

@ -1,5 +1,5 @@
// //
// SourceEdit.js // SourceEdit.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -25,8 +25,6 @@ import Modal from 'react-modal';
import RestAPI from './Services'; import RestAPI from './Services';
require('./styles.css');
const editCustomStyles = { const editCustomStyles = {
content : { content : {
top: '50%', top: '50%',
@ -38,7 +36,7 @@ const editCustomStyles = {
} }
}; };
const max_packet_size = 1440; //bytes const max_packet_size = 1440; //bytes
class SourceEdit extends Component { class SourceEdit extends Component {
static propTypes = { static propTypes = {
@ -114,7 +112,7 @@ class SourceEdit extends Component {
this.state.refclkPtpTraceable, this.state.refclkPtpTraceable,
this.state.map, this.state.map,
this.props.isEdit) this.props.isEdit)
.then(function(response) { .then(function(response) {
toast.success(message); toast.success(message);
this.props.applyEdit(); this.props.applyEdit();
}.bind(this)); }.bind(this));
@ -127,7 +125,7 @@ class SourceEdit extends Component {
onCancel() { onCancel() {
this.props.closeEdit(); this.props.closeEdit();
} }
getMaxChannels(codec, samples) { getMaxChannels(codec, samples) {
let maxChannels = Math.floor(max_packet_size / (samples * (codec === 'L16' ? 2 : 3))); let maxChannels = Math.floor(max_packet_size / (samples * (codec === 'L16' ? 2 : 3)));
return maxChannels > 64 ? 64 : maxChannels; return maxChannels > 64 ? 64 : maxChannels;
@ -144,7 +142,7 @@ class SourceEdit extends Component {
let maxChannels = this.getMaxChannels(this.state.codec, this.state.maxSamplesPerPacket); let maxChannels = this.getMaxChannels(this.state.codec, this.state.maxSamplesPerPacket);
this.setState({ codec: codec, maxChannels: maxChannels, channelsErr: this.state.channels > maxChannels }); this.setState({ codec: codec, maxChannels: maxChannels, channelsErr: this.state.channels > maxChannels });
} }
onChangeChannels(e) { onChangeChannels(e) {
if (e.currentTarget.checkValidity()) { if (e.currentTarget.checkValidity()) {
let channels = parseInt(e.target.value, 10); let channels = parseInt(e.target.value, 10);
@ -194,7 +192,7 @@ class SourceEdit extends Component {
} }
getMaxSamplesPerPacket() { getMaxSamplesPerPacket() {
return (this.props.source.max_samples_per_packet > (this.props.ticFrameSizeAt1fs * this.getnFS())) ? return (this.props.source.max_samples_per_packet > (this.props.ticFrameSizeAt1fs * this.getnFS())) ?
(this.props.ticFrameSizeAt1fs * this.getnFS()) : this.props.source.max_samples_per_packet; (this.props.ticFrameSizeAt1fs * this.getnFS()) : this.props.source.max_samples_per_packet;
} }
@ -202,7 +200,7 @@ class SourceEdit extends Component {
let duration = (samples * 1000000) / this.props.sampleRate; let duration = (samples * 1000000) / this.props.sampleRate;
if (duration >= 1000) { if (duration >= 1000) {
duration /= 1000; duration /= 1000;
if (duration == Math.round(duration)) if (duration == Math.round(duration))
return Math.round(duration).toString() + 'ms'; return Math.round(duration).toString() + 'ms';
else else
return (Math.round(duration * 1000) / 1000).toString() + 'ms'; return (Math.round(duration * 1000) / 1000).toString() + 'ms';
@ -222,7 +220,7 @@ class SourceEdit extends Component {
render() { render() {
return ( return (
<div id='source-edit'> <div id='source-edit'>
<Modal ariaHideApp={false} <Modal ariaHideApp={false}
isOpen={this.props.editIsOpen} isOpen={this.props.editIsOpen}
onRequestClose={this.props.closeEdit} onRequestClose={this.props.closeEdit}
style={editCustomStyles} style={editCustomStyles}
@ -243,7 +241,7 @@ class SourceEdit extends Component {
</tr> </tr>
<tr> <tr>
<th align="left"> <label>Max samples per packet </label> </th> <th align="left"> <label>Max samples per packet </label> </th>
<th align="left"> <th align="left">
<select value={this.state.maxSamplesPerPacket} onChange={this.onChangeMaxSamplesPerPacket}> <select value={this.state.maxSamplesPerPacket} onChange={this.onChangeMaxSamplesPerPacket}>
<option value="6" disabled={this.checkMaxSamplesPerPacket(6) ? undefined : true}>6 - {this.getPacketDuration(6)}</option> <option value="6" disabled={this.checkMaxSamplesPerPacket(6) ? undefined : true}>6 - {this.getPacketDuration(6)}</option>
<option value="12" disabled={this.checkMaxSamplesPerPacket(12) ? undefined : true}>12 - {this.getPacketDuration(12)}</option> <option value="12" disabled={this.checkMaxSamplesPerPacket(12) ? undefined : true}>12 - {this.getPacketDuration(12)}</option>
@ -256,7 +254,7 @@ class SourceEdit extends Component {
</tr> </tr>
<tr> <tr>
<th align="left"> <label>Codec</label> </th> <th align="left"> <label>Codec</label> </th>
<th align="left"> <th align="left">
<select value={this.state.codec} onChange={this.onChangeCodec}> <select value={this.state.codec} onChange={this.onChangeCodec}>
<option value="L16">L16</option> <option value="L16">L16</option>
<option value="L24">L24</option> <option value="L24">L24</option>
@ -299,8 +297,8 @@ class SourceEdit extends Component {
<th align="left">Audio Channels map</th> <th align="left">Audio Channels map</th>
<th align="left"> <th align="left">
<select value={this.state.map[0]} onChange={this.onChangeChannelsMap}> <select value={this.state.map[0]} onChange={this.onChangeChannelsMap}>
{ this.state.channels > 1 ? { this.state.channels > 1 ?
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Output ' + parseInt(v + 1, 10) + ' -> ALSA Output ' + parseInt(v + this.state.channels, 10)}</option>) : this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Output ' + parseInt(v + 1, 10) + ' -> ALSA Output ' + parseInt(v + this.state.channels, 10)}</option>) :
this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Output ' + parseInt(v + 1, 10)}</option>) this.state.audioMap.map((v) => <option key={v} value={v}>{'ALSA Output ' + parseInt(v + 1, 10)}</option>)
} }
</select> </select>

View File

@ -1,5 +1,5 @@
// //
// SourceInfo.js // SourceInfo.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -22,8 +22,6 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-modal'; import Modal from 'react-modal';
require('./styles.css');
const infoCustomStyles = { const infoCustomStyles = {
content : { content : {
top: '50%', top: '50%',
@ -59,11 +57,11 @@ class SourceInfo extends Component {
onClose() { onClose() {
this.props.closeInfo(); this.props.closeInfo();
} }
render() { render() {
return ( return (
<div id='source-info'> <div id='source-info'>
<Modal ariaHideApp={false} <Modal ariaHideApp={false}
isOpen={this.props.infoIsOpen} isOpen={this.props.infoIsOpen}
onRequestClose={this.props.closeInfo} onRequestClose={this.props.closeInfo}
style={infoCustomStyles} style={infoCustomStyles}

View File

@ -1,5 +1,5 @@
// //
// SourceRemove.js // SourceRemove.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -25,8 +25,6 @@ import Modal from 'react-modal';
import RestAPI from './Services'; import RestAPI from './Services';
require('./styles.css');
const removeCustomStyles = { const removeCustomStyles = {
content : { content : {
top: '50%', top: '50%',
@ -58,7 +56,7 @@ class SourceRemove extends Component {
} }
removeSource(message) { removeSource(message) {
RestAPI.removeSource(this.props.source.id).then(function(response) { RestAPI.removeSource(this.props.source.id).then(function(response) {
toast.success(message); toast.success(message);
this.props.applyEdit(); this.props.applyEdit();
}.bind(this)); }.bind(this));
@ -75,7 +73,7 @@ class SourceRemove extends Component {
render() { render() {
return ( return (
<div id='source-remove'> <div id='source-remove'>
<Modal ariaHideApp={false} <Modal ariaHideApp={false}
isOpen={this.props.removeIsOpen} isOpen={this.props.removeIsOpen}
onRequestClose={this.props.closeEdit} onRequestClose={this.props.closeEdit}
style={removeCustomStyles} style={removeCustomStyles}

View File

@ -1,5 +1,5 @@
// //
// Sources.js // Sources.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -27,8 +27,6 @@ import SourceEdit from './SourceEdit';
import SourceRemove from './SourceRemove'; import SourceRemove from './SourceRemove';
import SourceInfo from './SourceInfo'; import SourceInfo from './SourceInfo';
require('./styles.css');
class SourceEntry extends Component { class SourceEntry extends Component {
static propTypes = { static propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
@ -41,10 +39,10 @@ class SourceEntry extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
address: 'n/a', address: 'n/a',
port: 'n/a', port: 'n/a',
sdp: '' sdp: ''
}; };
} }
@ -108,7 +106,7 @@ class SourceList extends Component {
return ( return (
<div id='sources-table'> <div id='sources-table'>
<table className="table-stream"><tbody> <table className="table-stream"><tbody>
{this.props.sources.length > 0 ? {this.props.sources.length > 0 ?
<tr className='tr-stream'> <tr className='tr-stream'>
<th>ID</th> <th>ID</th>
<th>Name</th> <th>Name</th>
@ -125,7 +123,7 @@ class SourceList extends Component {
<span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span> <span className='pointer-area' onClick={this.handleReloadClick}> <img width='30' height='30' src='/reload.png' alt=''/> </span>
&nbsp;&nbsp; &nbsp;&nbsp;
{this.props.sources.length < 64 ? {this.props.sources.length < 64 ?
<span className='pointer-area' onClick={this.handleAddClick}> <img width='30' height='30' src='/plus.png' alt=''/> </span> <span className='pointer-area' onClick={this.handleAddClick}> <img width='30' height='30' src='/plus.png' alt=''/> </span>
: undefined} : undefined}
</div> </div>
); );
@ -135,19 +133,19 @@ class SourceList extends Component {
class Sources extends Component { class Sources extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
sources: [], sources: [],
source: {}, source: {},
isLoading: false, isLoading: false,
isConfigLoading: false, isConfigLoading: false,
isEdit: false, isEdit: false,
isInfo: false, isInfo: false,
editIsOpen: false, editIsOpen: false,
infoIsOpen: false, infoIsOpen: false,
removeIsOpen: false, removeIsOpen: false,
ticFrameSizeAt1fs: '', ticFrameSizeAt1fs: '',
sampleRate: '', sampleRate: '',
editTitle: '' editTitle: ''
}; };
this.onInfoClick = this.onInfoClick.bind(this); this.onInfoClick = this.onInfoClick.bind(this);
this.onEditClick = this.onEditClick.bind(this); this.onEditClick = this.onEditClick.bind(this);
@ -192,7 +190,7 @@ class Sources extends Component {
this.closeEdit(); this.closeEdit();
this.fetchSources(); this.fetchSources();
} }
closeEdit() { closeEdit() {
this.setState({editIsOpen: false}); this.setState({editIsOpen: false});
this.setState({removeIsOpen: false}); this.setState({removeIsOpen: false});
@ -202,7 +200,7 @@ class Sources extends Component {
closeInfo() { closeInfo() {
this.setState({infoIsOpen: false}); this.setState({infoIsOpen: false});
} }
onInfoClick(id, sdp) { onInfoClick(id, sdp) {
const source = this.state.sources.find(s => s.id === id); const source = this.state.sources.find(s => s.id === id);
this.openInfo("Local Source Info", source, sdp, true); this.openInfo("Local Source Info", source, sdp, true);
@ -223,15 +221,15 @@ class Sources extends Component {
} }
onAddClick() { onAddClick() {
let id; let id;
/* find first free id */ /* find first free id */
for (id = 0; id < 63; id++) { for (id = 0; id < 63; id++) {
if (this.state.sources[id] === undefined || if (this.state.sources[id] === undefined ||
this.state.sources[id].id !== id) { this.state.sources[id].id !== id) {
break; break;
} }
} }
const defaultSource = { const defaultSource = {
'id': id, 'id': id,
'enabled': true, 'enabled': true,
'name': 'ALSA Source ' + id, 'name': 'ALSA Source ' + id,
@ -246,7 +244,7 @@ class Sources extends Component {
}; };
this.openEdit('Add Source ' + id, defaultSource, false); this.openEdit('Add Source ' + id, defaultSource, false);
} }
render() { render() {
this.state.sources.sort((a, b) => (a.id > b.id) ? 1 : -1); this.state.sources.sort((a, b) => (a.id > b.id) ? 1 : -1);
const sources = this.state.sources.map((source) => ( const sources = this.state.sources.map((source) => (
@ -261,14 +259,14 @@ class Sources extends Component {
)); ));
return ( return (
<div id='sources'> <div id='sources'>
{ this.state.isLoading || this.state.isConfigLoading ? <Loader/> { this.state.isLoading || this.state.isConfigLoading ? <Loader/>
: <SourceList onAddClick={this.onAddClick} : <SourceList onAddClick={this.onAddClick}
onReloadClick={this.onReloadClick} onReloadClick={this.onReloadClick}
sources={sources} /> } sources={sources} /> }
{ this.state.infoIsOpen ? { this.state.infoIsOpen ?
<SourceInfo infoIsOpen={this.state.infoIsOpen} <SourceInfo infoIsOpen={this.state.infoIsOpen}
closeInfo={this.closeInfo} closeInfo={this.closeInfo}
infoTitle={this.state.infoTitle} infoTitle={this.state.infoTitle}
isInfo={this.state.isInfo} isInfo={this.state.isInfo}
id={this.state.source.id.toString()} id={this.state.source.id.toString()}
name={this.state.source.name} name={this.state.source.name}
@ -279,7 +277,7 @@ class Sources extends Component {
<SourceEdit editIsOpen={this.state.editIsOpen} <SourceEdit editIsOpen={this.state.editIsOpen}
closeEdit={this.closeEdit} closeEdit={this.closeEdit}
applyEdit={this.applyEdit} applyEdit={this.applyEdit}
editTitle={this.state.editTitle} editTitle={this.state.editTitle}
isEdit={this.state.isEdit} isEdit={this.state.isEdit}
ticFrameSizeAt1fs={this.state.ticFrameSizeAt1fs} ticFrameSizeAt1fs={this.state.ticFrameSizeAt1fs}
sampleRate={this.state.sampleRate} sampleRate={this.state.sampleRate}
@ -289,7 +287,7 @@ class Sources extends Component {
<SourceRemove removeIsOpen={this.state.removeIsOpen} <SourceRemove removeIsOpen={this.state.removeIsOpen}
closeEdit={this.closeEdit} closeEdit={this.closeEdit}
applyEdit={this.applyEdit} applyEdit={this.applyEdit}
source={this.state.source} source={this.state.source}
key={this.state.source.id} /> key={this.state.source.id} />
: undefined } : undefined }
</div> </div>

View File

@ -1,5 +1,5 @@
// //
// Tab.js // Tab.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -51,7 +51,7 @@ class Tab extends Component {
className += ' tab-list-active'; className += ' tab-list-active';
} }
return ( return (
<li className={className} onClick={onClick}>{label}</li> <li className={className} onClick={onClick}>{label}</li>
); );
} }

View File

@ -1,5 +1,5 @@
// //
// Tabs.js // Tabs.jsx
// //
// Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved. // Copyright (c) 2019 2020 Andrea Bondavalli. All rights reserved.
// //
@ -46,11 +46,11 @@ class Tabs extends Component {
state: { activeTab, } state: { activeTab, }
} = this; } = this;
return ( return (
<div className="tabs"> <div className="tabs">
<ol className="tab-list"> { children.map((child) => { <ol className="tab-list"> { children.map((child) => {
const { label } = child.props; const { label } = child.props;
return ( return (
<Tab activeTab={ activeTab } <Tab activeTab={ activeTab }
key={ label } key={ label }
label={ label } label={ label }
@ -58,11 +58,11 @@ class Tabs extends Component {
/> />
); }) ); })
} }
</ol> </ol>
<div className="tab-content"> { children.map((child) => { <div className="tab-content"> { children.map((child) => {
if (child.props.label !== activeTab) return undefined; if (child.props.label !== activeTab) return undefined;
return child.props.children; return child.props.children;
}) } }) }
</div> </div>
</div> </div>
); );

11
webui/src/main.jsx Normal file
View File

@ -0,0 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './styles.css'
import App from './app'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

7
webui/vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh()]
})