diff --git a/.vscode/settings.json b/.vscode/settings.json index 719d509..2b238b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,12 +2,15 @@ "cSpell.words": [ "alsa", "asound", + "gethostname", "hifistreamer", "httpclient", "levelname", "mopidy", "psutil", + "pyconnman", "qrcode", + "sethostname", "tidalapi" ] } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3b7e007..6eb3135 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.2.3", + "is-ip": "^5.0.0", "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-bootstrap": "^2.7.2", @@ -5893,6 +5894,31 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-regexp/node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6063,6 +6089,17 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -8538,6 +8575,17 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/function.prototype.name": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", @@ -9260,6 +9308,17 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -9423,6 +9482,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.0.tgz", + "integrity": "sha512-uhmKwcdWJ1nTmBdoBxdHilfJs4qdLBIvVHKRels2+UCZmfcfefuQWziadaYLpN7t/bUrJOjJHv+R1di1q7Q1HQ==", + "dependencies": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -15816,6 +15890,22 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -16146,6 +16236,20 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -21696,6 +21800,21 @@ "wrap-ansi": "^7.0.0" } }, + "clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "requires": { + "is-regexp": "^3.0.0" + }, + "dependencies": { + "is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==" + } + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -21837,6 +21956,11 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, + "convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==" + }, "convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -23630,6 +23754,11 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==" + }, "function.prototype.name": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", @@ -24152,6 +24281,11 @@ "loose-envify": "^1.0.0" } }, + "ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==" + }, "ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -24255,6 +24389,15 @@ "is-extglob": "^2.1.1" } }, + "is-ip": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.0.tgz", + "integrity": "sha512-uhmKwcdWJ1nTmBdoBxdHilfJs4qdLBIvVHKRels2+UCZmfcfefuQWziadaYLpN7t/bUrJOjJHv+R1di1q7Q1HQ==", + "requires": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + } + }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -28717,6 +28860,16 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" }, + "super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "requires": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -28964,6 +29117,14 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "requires": { + "convert-hrtime": "^5.0.0" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9734275..03557e2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.2.3", + "is-ip": "^5.0.0", "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-bootstrap": "^2.7.2", @@ -16,7 +17,7 @@ "web-vitals": "^2.1.4" }, "scripts": { - "start": "REACT_APP_API_SERVER='hifistreamer' react-scripts start", + "start": "BROWSER=safari REACT_APP_API_SERVER='hifistreamer' react-scripts start", "build": "BUILD_PATH='../hifistreamer_app/www' react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" diff --git a/frontend/src/components/Network.js b/frontend/src/components/Network.js new file mode 100644 index 0000000..ad90066 --- /dev/null +++ b/frontend/src/components/Network.js @@ -0,0 +1,320 @@ +/** + * The network configuration page + * @author Oren Sokoler + */ + +import {getAPIurl} from './Config.js' + +import React, { Component } from 'react'; + +import Form from 'react-bootstrap/Form'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import { Button } from 'react-bootstrap'; + +import {isIP} from 'is-ip'; + +import '../css/Page.css'; + +/** + * The networking page, handles configuration of the network devices + */ +class Network extends Component { + + constructor(props) { + super(props); + this.state = { + isSaveEnabled : false, + isDHCP : true, + IP : '', + isIPValid : true, + Netmask : '', + isNetmaskValid : true, + Gateway : '', + isGatewayValid : true, + DNS : '', + isDNSValid : true, + Hostname : '', + timer : null + } + } + + /** + * GET the network configuration state + */ + getStatus() { + fetch(getAPIurl() + "/network") + .then(response => response.json()) + .then( + (result) => { + if (result.enabled) { + this.stopTimer() + this.setState({ + isDHCP : result.method === 'dhcp', + IP : result.address, + Netmask : result.netmask, + Gateway : result.gateway, + DNS : result.dns, + Hostname : result.name + }) + } + else { + this.startTimer() + this.setState({ + IP : '', + Netmask : '', + Gateway : '', + DNS : '', + Hostname : result.name + }) + } + } + ) + } + + /** + * Start the GET status polling + */ + startTimer() { + if (this.state.timer === null) { + this.setState({ + timer : setInterval(() => {this.getStatus()}, 2000) + }) + } + } + + /** + * Stop the GET status polling + */ + stopTimer() { + if (this.state.timer !== null) { + clearInterval(this.state.timer) + this.setState({ + timer : null + }) + } + } + + componentDidMount() { + this.getStatus() + } + + componentWillUnmount() { + this.stopTimer() + } + + /** + * Change the network configuration + */ + onConfigChange() { + this.setState( { + isSaveEnabled : true, + isDHCP : !this.state.isDHCP + }) + } + + /** + * Save button clicker + */ + onSaveClicked() { + this.stopTimer() + this.setState({ isSaveEnabled : false }) + const requestOptions = { + method: 'POST', + }; + var Params = 'method=' + if (this.state.isDHCP) { + Params += 'dhcp' + } + else { + Params += 'manual' + Params += '&address=' + document.getElementById('IP').value + Params += '&netmask=' + document.getElementById('Netmask').value + Params += '&gateway=' + document.getElementById('Gateway').value + Params += '&dns=' + document.getElementById('DNS').value + } + Params += '&name=' + document.getElementById('Hostname').value + fetch(getAPIurl() + "/network?" + Params, requestOptions) + .then(this.getStatus()) + } + + /** + * Hostname changed + */ + onHostnameChange(event) { + this.setState({ Hostname : event.target.value, + isSaveEnabled : true + }) + } + + /** + * IP Address changed + */ + onIPChange(event) { + switch (event.target.id) { + case 'IP': + if (isIP(event.target.value)) { + this.setState({ isIPValid : true, + IP : event.target.value + }) + } + else { + this.setState({ isIPValid : false, + IP : event.target.value + }) + } + break; + case 'Netmask': + if (isIP(event.target.value)) { + this.setState({ isNetmaskValid : true, + Netmask : event.target.value + }) + } + else { + this.setState({ isNetmaskValid : false, + Netmask : event.target.value + }) + } + break; + case 'Gateway': + if (isIP(event.target.value)) { + this.setState({ isGatewayValid : true, + Gateway : event.target.value + }) + } + else { + this.setState({ isGatewayValid : false, + Gateway : event.target.value + }) + } + break; + case 'DNS': + if (isIP(event.target.value)) { + this.setState({ isDNSValid : true, + DNS : event.target.value + }) + } + else { + this.setState({ isDNSValid : false, + DNS : event.target.value + }) + } + break; + default: + break; + } + if (isIP(document.getElementById('IP').value) && + isIP(document.getElementById('Netmask').value) && + isIP(document.getElementById('Gateway').value) && + isIP(document.getElementById('DNS').value)) { + this.setState({ isSaveEnabled : true }) + } + else { + this.setState({ isSaveEnabled : false }) + } + } + + render() { + return ( +