From a589741ec63dedeef824777c0d410775358456ee Mon Sep 17 00:00:00 2001 From: rasta5man Date: Mon, 15 Apr 2024 19:28:54 +0200 Subject: [PATCH] Initial commit --- .gitignore | 26 + README.md | 9 + doc/LM Terminal zadanie-1.docx | Bin 0 -> 41060 bytes jsconfig.json | 6 + package.json | 49 ++ public/index.html | 43 ++ src/App.css | 38 ++ src/App.js | 683 +++++++++++++++++++ src/App.test.js | 8 + src/assets/img/business-manager.svg | 17 + src/components/AlertDialog/AlertDialog.js | 110 ++++ src/components/nodes/menu.js | 106 +++ src/components/nodes/nodes.js | 207 ++++++ src/components/relays/menu.js | 106 +++ src/components/relays/relays.js | 208 ++++++ src/components/table/menu.js | 106 +++ src/components/table/table.js | 401 +++++++++++ src/components/table/tableDb.js | 582 ++++++++++++++++ src/config/config.js | 138 ++++ src/controls.js | 770 ++++++++++++++++++++++ src/index.css | 13 + src/index.js | 22 + src/logo.svg | 1 + src/output.js | 108 +++ src/reportWebVitals.js | 13 + src/setupTests.js | 5 + src/store/actions.js | 2 + src/store/reducer.js | 23 + src/util/db/SqlQueryBuilder.js | 248 +++++++ src/util/db/db.js | 300 +++++++++ src/util/findGetParameterInUrl.js | 14 + src/util/isLocalhost.js | 11 + src/util/isMobile.js | 15 + 33 files changed, 4388 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 doc/LM Terminal zadanie-1.docx create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 public/index.html create mode 100644 src/App.css create mode 100644 src/App.js create mode 100644 src/App.test.js create mode 100644 src/assets/img/business-manager.svg create mode 100644 src/components/AlertDialog/AlertDialog.js create mode 100644 src/components/nodes/menu.js create mode 100644 src/components/nodes/nodes.js create mode 100644 src/components/relays/menu.js create mode 100644 src/components/relays/relays.js create mode 100644 src/components/table/menu.js create mode 100644 src/components/table/table.js create mode 100644 src/components/table/tableDb.js create mode 100644 src/config/config.js create mode 100644 src/controls.js create mode 100644 src/index.css create mode 100644 src/index.js create mode 100644 src/logo.svg create mode 100644 src/output.js create mode 100644 src/reportWebVitals.js create mode 100644 src/setupTests.js create mode 100644 src/store/actions.js create mode 100644 src/store/reducer.js create mode 100644 src/util/db/SqlQueryBuilder.js create mode 100644 src/util/db/db.js create mode 100644 src/util/findGetParameterInUrl.js create mode 100644 src/util/isLocalhost.js create mode 100644 src/util/isMobile.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6521517 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +test.js +package-lock.json + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a6672e --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +This is a terminal for citysys project. + +Download the project. Install it with `npm install`. + +Start the project: `npm start` +View the project in browser: http://localhost:3000 +Run tests: `npm test` +Build project: `npm run build` + diff --git a/doc/LM Terminal zadanie-1.docx b/doc/LM Terminal zadanie-1.docx new file mode 100644 index 0000000000000000000000000000000000000000..00a340c2082d82706d03f069bf7151bbd755dca6 GIT binary patch literal 41060 zcmeFYgL`D%w=Npn>6jhcwmL?~ww;b`8y(xW?W8+4E4Dj!a;v}J*?WKIx%avMz&Z8I z!dkWFsxe0myyKm7ewP7<_yPh20s{g9@)e};s*}JJ6a+*G5(ESt1O`l7)ZWh7)XrI7 z#lykWNteOh)`qwM0*opT1Ppln|9$))tbwMV<5q)ADBm7KK7wXjR-`+qiids|#F^q* zynsfsxr*J3DCzlaDQ2aiC1T&KU{|GkUE!?G88pq&X!|42WhCB35&rITpe-twxnX_& zl`Dlw!Wu|zokwO3W5U+eizmB;Bob!`0q2V`C>8G~Ut84u3a)LF4m{1i4duXxHsRzW3$k2n(NU z?|3_K&|M*p4M6yyAkeTrKfyu1|8L$&n1I`Q1^h-H=zQ=%@6>lRwQ*u(`1|^Qz4HHH zSNzw|s}lQxUWp)d74#l7+pW0Pk6kFwXgsryy$TJZEhUS-zGAif@y@rh0;YRnC^0@a zpETp?lqK%6{Zsc3Zi*^mWH;>6v)-WgYnMAna!6M(v-^_6E<)zvgW3BSsTB2CP?RP{ zF?O%W$6mv0&~>=+drgha5;ydKtPW1KtND|N^!Gy zG+{KcH+HcFrm(-cYeGZLew_om2YJPh*!k3-yy14INFyIaUs<7+yrjO#$EXj4 zm`CM*%Xlzg0jqg7lx;ROSB`$^7G8QU{W#>Ck%lB**^%}kw7TE?j!2|A30kcR2BzjD zdA1l|v#L{wJ2%ERC(d59=utFjCFykebBec3MYeCSWr4bRW|2U!xFfTi97Jv522-K! z0^F>cIW%9#nU*O~6Fu0Oy@PWgGS zG6i(2lYS#aQ~bb?jvNVJbtDw>9ikePTX1fOW=;9vgaCgQ5G13NbiE69Nb|m z>5?_P2EPI#O_5^P>9A(|W8W_>H9(YqsUMQfK{K~!H z&W$L=_{(zcPUIJdte3#%;_x7*^IOA_Ldh4wTT;&e+Gwys3z(*#W}zop9@?+a`@tta z;WOg>{N7`>ZS97Rk#7Q;e_eL+gBio_u7FKm&nva)TO!`s=!51c(n-bAcjZdQa?Y=K z!rJHU^=QJ}B!^!@AutH%W2%Q#(X1{pvK0h2YOAH$L}vRXP~-T>n*bWhNPY zDKBK5rv)Qt&F2W-i`~VAoX*&Y5R)cODvK7Y7AbYp%>vyd2IEpeqDwU z!Muy}m6Js51s}$h&u|)*MzRg<&HW;0<~C1}fPFQ5Zs7P}Pe;;&X9-&dFACH!b|h;s zwa5}4n`bdx;txLM>SVRlQ?QUQzb@v(R%u>FEmXwFZ+VM^_niT3j
H4q3pu{9+( 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
XP|p3moV-t&E)bJlIIU3diT-AqQCHRE-- z*K06(?&Ew`Y39O9&g3kT=IH_*^p1ZStFh1F`WvNI98za6dl4h7Stui@{CE|-{q?WkoW=+6KP~#rXba><+5E={iKRKACfQ$2Y^ydgzB6lT{v#AaX?Q z`|E2)TwRh|xGi1ojyoALRflWZDS?D;Vll2=iaSGz)PBQ)O^U!Jrv)~XmAcVrZGD}l zr2WZ}l~cUUKQm&=q1nhTpsRlYY=4>WUG0hY3q+vYe9?7y}~=bOXb zJGoB_keD2tU<})$(!!S>$+4MS7h>AF zw7nt+F$pr*Xzla@iCGSoM7<-t-#``rAX&uNHajV^=_X{h{0Uu%c2=l#qX@2ibV5ad zWy3^y8ew-kBlAmNIXS6klyNLb>i0jFIEA)pM%wApj|JA2Ogs7>-Jx9N;@`}kalQ%b z+{RM$Laf0YUw|CPyLKAxQ~_lnsX~VW(7Ub&`U|n`U=qa>di!5@9JWM-Gz>aWA4PrG zV|GT!S&XZjSP0o9cI;#T!Z7jaa{?mdC70^?5Tul^e3fI;f9pkjQC(y!eP1O$IeH;3 zo2f#6aj_@X`mNinC&h`)iS^N%p}RaIX^QUi)2fqmx9;7ThW34RwInu_HiWuuYa=H` zQW8>8^P`?W;s2nk1sQWvK=PbJ2#fM{J4thoQM6f`0<1bq8AEd}?im@*zSXhS0`0#L zgwXZ%E~!jt;N?Efj;o7*^s#zFazH%|uXbL@z+wLm?Q^^oW8i0u26$LXq5Qr=OH;Dm zr0wOBy5Ha1z!nm>Pyh8H(%*w3HSt?pVq}1M&55vPu}ub#xxoAMkCt50X0zvkfD%Ro z7cIH90v>pGMj_B*x57=G2X}qw|Ai z-Q3&vT5qWSje~yMCJLvB#(v#K4J|YV_)0+qN?9|6rb5Q#zeg;a!49t_>GL1xry zC8m13q<3mF6hiTDj519H?!6OSP@9Ts%$Zz&oCiqlN2y9E^oj-pI&1`_D!o#wC<0+I zWc{bqf1gmwk%REs-O?2j^B34uXbuZ*#_Z}j~QR^=n`#FQ91lJaDhZFBSE@P|J}|YC2%6tekWQY`5P;{_O1gZB1@6$Hd-g`Arzp!x%|P{ZLqv zGB#n8ZAVFcSsE{+P{Zhx)d|P$Bl-S0;P}&CH+&Se#~bRY5S$w$*BQ#Q@sj^T)>LdL zZ*bOl&Iw^?ICH7#Yi}y#f?_D%(mcY{J|l)$utvYou}sDOLFju`mTZ(Y%WX?ZVH6lF zH$W8g)k3w$D|zHfO|)SJd}9aLp?SA$id-V{(FE4s9icP26sgJTA*U#Fghn6PA=KeW zz*=fo$~nE$?gtvlUgyVpXuZ+dDK-6<2Pc2M9#qK`DMAG_5gsW0`~ zJ*xv8A8g+itDEyu3&P*kDj=v0R4$^RwZM%~2IY;GZAAlmJt6`E$^H;LT`Yifi}njV zg$N;_16&sc>CDJJ(+BZOF*W#KVX!lUjpcY8&!$9)i zumT1GRRVkeF;js!AA|#%|5>{pk*+N!z=`_<7q(>;yijTh{%_+eNmiVQ_!34Lgp93| zzk&mS78m(dOi<}1CLG!Wi_*d3P#83H7r_t1e=Yo@6P8)NgfCI>k5%BO;Q#EjOmQ-L ziDKnPD1JD<8NCd@5ceJaKWmyF%wNjuUt(BQx$)yyXZU3_ r2sC~T1o|Q0^TSt1-IZ`k#tQgS40eQz015+vB!O8N5UJhoi(7vK$P!Mk literal 0 HcmV?d00001 diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..ec9aa3f --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "baseUrl": "src" + }, + "include": ["src"] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d2e0103 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "terminal-oms.app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@material-ui/core": "^4.11.3", + "@material-ui/icons": "^4.11.2", + "@testing-library/jest-dom": "^5.11.9", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.7.0", + "axios": "^0.21.1", + "bitwise": "^2.1.0", + "easy-crc": "0.0.2", + "lodash.clonedeep": "^4.5.0", + "material-ui-icons": "^1.0.0-beta.36", + "moment": "^2.29.1", + "react": "^17.0.1", + "react-animated-list": "^0.1.4", + "react-dom": "^17.0.1", + "react-draggable": "^4.4.4", + "react-redux": "^7.2.2", + "react-scripts": "4.0.2", + "redux": "^4.0.5", + "web-vitals": "^1.1.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..bdabb19 --- /dev/null +++ b/src/App.js @@ -0,0 +1,683 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; + +import Grid from '@material-ui/core/Grid'; + +import Controls from './controls' +import Output from './output' + +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; +import Box from '@material-ui/core/Box'; +import CircularProgress from '@material-ui/core/CircularProgress'; + +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; + +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import CloseIcon from '@material-ui/icons/Close'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; + +import logo from './assets/img/business-manager.svg' +import axios from 'axios' + +import Nodes from './components/nodes/nodes'; +import Relays from './components/relays/relays'; +import DB from './util/db/db' +import findGetParameterInUrl from './util/findGetParameterInUrl'; + +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; + +import Radio from '@material-ui/core/Radio'; +import RadioGroup from '@material-ui/core/RadioGroup'; +import FormControl from '@material-ui/core/FormControl'; +import FormLabel from '@material-ui/core/FormLabel'; + +const divOutputStyle = { + backgroundColor: 'black', + overflow: "auto", + alignItems: "left", + display: "flex", + textAlign: "left", + height: "100vh", +}; + +function TabPanel(props) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.any.isRequired, + value: PropTypes.any.isRequired, +}; + +function a11yProps(index) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + +const useStyles = makeStyles((theme) => ({ + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + }, +})); + +const App = (props) => { + + document.title = 'IoT Terminal 2.0.4'; + // ver 2.0.4 - pridany cct do config.js + + + const controlsRef = useRef(null); + const outputRef = useRef(null); + + const [tabValue, setTabValue] = useState(0); + const [loggedIn, setLoggedIn] = useState(false); + const [username, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const [baseUrl, setBaseUrl] = useState(""); + const [error, setError] = useState(""); + const [showprogress, setShowprogress] = useState(false); + const [showDialog, setShowDialog] = useState(false); + const [autoLogin, setAutoLogin] = useState(false); + const [formType, setFormType] = useState(""); + + // if inputError[key] == null, we disable button so we can not send new settings + const [settings, setSettings] = useState({}); + const [disableButton, setDisableButton] = useState(false); + const [inputError, setInputError] = useState({}); + + const size = useWindowSize(); + + useEffect(() => { + + if(username !== "" & password !== "") + { + login(); + } + + }, [autoLogin, username, password, baseUrl]); + + const validate = (event) =>{ + + if(username === "" || password === "") return true; + return false; + }; + + const handleCloseDialog = () =>{ + setShowDialog(false); + } + + const handleCommand = (command) =>{ + setFormType(command); + setShowDialog(true); + } + + const handleSettingsChange = (event) => + { + let { name, value } = event.target; + + if(event.target.type == "number") + { + if(isNaN(parseInt(value)) || parseInt(value) < 0 ) + { + setDisableButton(true); + setInputError({ + ...inputError, + [name]: null + }); + } + else + { + setDisableButton(false); + setInputError({ + ...inputError, + [name]: value + }); + + } + } + + if(event.target.type == "checkbox") + { + value = event.target.checked; + } + + setSettings({ + ...settings, + [name]: value + }); + } + + + const makeAxiosRequest = (url, table, action, body) => + { + return new Promise((resolve, reject) => { + + let instance = axios.create(); + instance(url, + { + method: 'POST', + mode: 'no-cors', + data:{ + hostname: "localhost", + table: table, + //where: ["error_type", "MANUAL"], + action: action, + body: body + }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + //"Content-Type": "text/html;charset=UTF-8" + }, + withCredentials: false, + credentials: 'same-origin', + }) + .then(response => {resolve(response)}) + .catch(error => { + const message = error + ""; + console.error(message); + }); + + }); + } + + + const loadSettings = () => + { + let url = baseUrl + `/db`; + + makeAxiosRequest(url, "settings", "read","") + .then(response => { + + if(!response.data.success) + { + //throw response.data.message; + } + + let a = response.data; + console.log("loadSettings", a); + + setSettings(a[0]); + setInputError(a[0]); + }) + .catch(error => { + const message = error + ""; + console.error(message); + }); + } + + + const handleSetNewSettings = () => + { + let url = baseUrl + `/db`; + + makeAxiosRequest(url, "settings", "update", settings) + .then(response => { + + if(!response.data.sucess) + { + //throw response.data.message; + } + + if(response.data === 1) + // if(response.data.data === 1) + { + alert("Nove nastavenia uspesne zapisane do databazy") + } + }) + .catch(error => { + const message = error + ""; + console.error(message); + }); + } + + const login = (event) =>{ + + axios(baseUrl + "/validate", + { + method: 'POST', + mode: 'no-cors', + data:{ + username: username, + password: password, + }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }, + withCredentials: false, + credentials: 'same-origin', + }) + .then(response => { + + console.log(response); + + if(response.data.valid) + { + setLoggedIn(true); + setShowprogress(false); + setError(""); + + loadSettings(); + } + else + { + setError("Nesprávne meno, alebo heslo"); + setShowprogress(false); + } + + }) + .catch(error => { + const message = error + ""; + + setError(message); + setShowprogress(false); + }); + + }; + + const handleLogin = (event) =>{ + + if(username === "" || password === "") + { + setError("Nie je zadané meno a heslo"); + return; + } + + setShowprogress(true); + login(); + }; + + const handleTabChange = (event, newValue) => { + setTabValue(newValue); + }; + + const handleChange = event => { + + if(event.target.name === "username") setUserName(event.target.value); + if(event.target.name === "password") setPassword(event.target.value); + }; + + const buildUi = () => + { + if(loggedIn) + { + let widgets = []; + let mainUi = ( + + + +
+ +
+
+ + +
+
+ +
+
+
+ +
+ ) + + widgets.push(mainUi); + + if(showDialog) + { + + // push all data from settings.table to grid + let allSettings = []; + + if(settings !== undefined) + { + Object.keys(settings).forEach(function(key) { + // console.log(key, settings[key]); + + if(key == "backup_on_failure" || key == "maintanace_mode") + { + allSettings.push( + ( + + } + name={key} + label={key} + /> + + ) + ) + } + else if(key == "controller_type") + { + allSettings.push( + ( + + {key} + + } label="LM" /> + } label="Unipi" /> + + + ) + ) + } + else if(['restore_from_backup', 'restore_backup_wait', 'mqtt_port', 'projects_id'].includes(key)) + { + allSettings.push( + ( + + ) + ) + } + else + { + allSettings.push( + ( + + ) + ) + } + }); + } + + let dialog = ( +
+ + + + + + + + + Nastavenia FLOW + + + + + + + + + + + + + Modal title + + + + + + {/* //! tato form robi tuto chybu v konzole: Warning: validateDOMNesting(...):
cannot appear as a descendant of

. */} + + + {allSettings} + +

+ +

+
+ +
+
+ +
+
+ +
+ + + + +
+
+ ) + + widgets.push(dialog); + } + + return widgets; + } + + if(showprogress) + { + return( +
+ + + logo + + + + +
Prihlasujem...
+

+ + + +
+
+
+ ) + } + + return loginDialog(); + } + + const loginDialog = () => + { + + return ( + +
+ +

+ logo + + + + + + +
+ +
+
+ + + + +
+ +
+ {error} +
+
+ +
+ +
+ + + + + + + + +
+
+ ); + } + + return ( +
+ {buildUi()} +
+); + +// Hook +function useWindowSize() { + // Initialize state with undefined width/height so server and client renders match + // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ + + const [windowSize, setWindowSize] = useState({ + width: undefined, + height: undefined, + }); + + useEffect(() => { + + let baseUrl = ''; + if(window.location.hostname === "localhost") + { + //baseUrl = "http://localhost:5000"; + baseUrl = "http://10.0.0.5:5000"; + } + + setBaseUrl(baseUrl); + DB.backendUrl = baseUrl + `/db`; + + let username = findGetParameterInUrl("username"); + let password = findGetParameterInUrl("password"); + + if(username === undefined) username = ""; + if(username === undefined) username = ""; + + if(username !== "" && password !== "") + { + setUserName(username); + setPassword(password); + + setAutoLogin(true); + } + + + + // Handler to call on window resize + function handleResize() { + // Set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + + // Add event listener + window.addEventListener("resize", handleResize); + + // Call handler right away so state gets updated with initial window size + handleResize(); + + // Remove event listener on cleanup + return () => window.removeEventListener("resize", handleResize); + + }, []); // Empty array ensures that effect is only run on mount + + if(controlsRef.current == null || outputRef == null); + else + { + if(windowSize.width > 600) + { + outputRef.current.style.height = "100vh"; + } + else + { + outputRef.current.style.height = (windowSize.height - controlsRef.current.clientHeight) + "px"; + } + } + + return windowSize; +} + +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/assets/img/business-manager.svg b/src/assets/img/business-manager.svg new file mode 100644 index 0000000..f351dc5 --- /dev/null +++ b/src/assets/img/business-manager.svg @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/src/components/AlertDialog/AlertDialog.js b/src/components/AlertDialog/AlertDialog.js new file mode 100644 index 0000000..9013669 --- /dev/null +++ b/src/components/AlertDialog/AlertDialog.js @@ -0,0 +1,110 @@ +import React, { useEffect } from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Paper from '@material-ui/core/Paper'; +import Draggable from 'react-draggable'; + +import WarningIcon from '@material-ui/icons/Warning'; +import { green } from '@material-ui/core/colors'; +import { red } from '@material-ui/core/colors'; +import { blue } from '@material-ui/core/colors'; + +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import Typography from '@material-ui/core/Typography'; +import MenuItem from '@material-ui/core/MenuItem'; + +import ListItemText from '@material-ui/core/ListItemText'; +import ListItem from '@material-ui/core/ListItem'; +import List from '@material-ui/core/List'; +import Divider from '@material-ui/core/Divider'; + +function PaperComponent(props) { + return ( + + + + ); +} + +export default function AlertDialog(props) { + const [open, setOpen] = React.useState(props.open); + + useEffect(() => { + setOpen(props.open); +}, [props.open]) + + const handleClickOpen = () => { + //setOpen(true); + }; + + const handleClose = () => { + //setOpen(false); + props.onConfirm(); + }; + + return ( +
+ + + + + + + + {props.title} + + + + + { + Array.isArray(props.content) + ? + ( + + { + props.content.map((message) => ( + <> + + + + + + )) + } + + ) + : + ( + + + {props.content} + + ) + } + + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/nodes/menu.js b/src/components/nodes/menu.js new file mode 100644 index 0000000..ceee4ab --- /dev/null +++ b/src/components/nodes/menu.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; + +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; + +import MenuList from '@material-ui/core/MenuList'; +import { grey } from '@material-ui/core/colors'; +import { green } from '@material-ui/core/colors'; +import { red } from '@material-ui/core/colors'; +import { blue } from '@material-ui/core/colors'; + +import AddCircleIcon from '@material-ui/icons/AddCircle'; +import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; +import EditIcon from '@material-ui/icons/Edit'; +import SearchIcon from '@material-ui/icons/Search'; +import Typography from '@material-ui/core/Typography'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; + +const StyledMenu = withStyles({ + paper: { + border: '1px solid #d3d4d5', + }, +})((props) => ( + +)); + +/* +const StyledMenuItem = withStyles((theme) => ({ + root: { + '&:focus': { + backgroundColor: theme.palette.primary.main, + '& .MuiListItemIcon-root, & .MuiListItemText-primary': { + color: theme.palette.common.white, + }, + }, + }, +}))(MenuItem); +*/ + +export default function CustomizedMenus(props) { + const [anchorEl, setAnchorEl] = React.useState(props.anchorref); + + const handleClick = (event, menu) => { + setAnchorEl(event.currentTarget); + props.menuClicked(menu); + }; + + const handleClose = () => { + setAnchorEl(null); + props.menuClosed(); + }; + + return ( +
+ + + + handleClick(event, "add")}> + + + + pridať + + handleClick(event, "edit")}> + + + + editovat + + handleClick(event, "delete")}> + + + + zmazať + + handleClick(event, "exit")}> + + + + logout + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/nodes/nodes.js b/src/components/nodes/nodes.js new file mode 100644 index 0000000..8d2e602 --- /dev/null +++ b/src/components/nodes/nodes.js @@ -0,0 +1,207 @@ +import React from 'react'; + +import TableDb from 'components/table/tableDb'; +//import TableDb from '../table/tableDb'; +import DB from "util/db/db"; +//import { LogicOperator } from '../../util/db/SqlQueryBuilder'; + +//import Find from './find'; +//import Form from './form'; +import CustomizedMenus from './menu' + +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +//import Card from "components/Card/Card.js"; +//import CardBody from "components/Card/CardBody.js"; +//import CardHeader from "components/Card/CardHeader.js"; + +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import Paper from '@material-ui/core/Paper'; + +import SearchIcon from '@material-ui/icons/Search'; +import AddIcon from '@material-ui/icons/Add'; +import EditIcon from '@material-ui/icons/Edit'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; + +//----------------------------------------------------- + +class Nodes extends TableDb { + constructor(props) { + super(props); + + this.title = "Zoznam nodov"; + this.dbTable = "nodes"; + + this.state.columns = ["actions", "node", "tbname", "line", "status"]; + this.state.visibleColumns = ["actions", "node", "tbname", "line", "status"]; + + this.renamedColumns = { + node: "node", + note: "poznámka", + num: "počet", + + }; + } + + menuClicked(menu) + { + if(menu === "delete") + { + if(this.selectedRows.length === 0) + { + this.menuClosed(); + this.showAlertDialog("Mazanie", "Nie sú označené žiadne riadky"); + return; + } + + var r = window.confirm("Naozaj chcete zmazať?"); + if (r === false) return; + + let db = new DB(); + db.dbStartTransaction(); + + //default - odstranit z subject + let conditions = {"id": this.id}; + db.dbDelete(this.getTableName(false), conditions); + db.dbCommitTransaction(); + + let index = this.index; + + db.exec().then( + result => { + this.deleteRow(index); + }, + error => { + console.log(error); + this.showAlertDialog("Chyba", error); + } + ); + + return; + } + else if(menu === "exit") + { + this.props.logout(); + } + + let processed = super.menuClicked(menu); + if(processed) return; + } + + getTableName(withView = false) + { + //if(withView) return "view_subject"; + //return this.dbTable; + return this.dbTable; + } + + //add or edit + /* + renderForm() + { + let data = undefined; + if(this.state.data == null); + else data = this.state.data[this.index]; + + //getIndex + //console.log(data);alert(); + + let status = TableDb.TABLE_STATUS.EDIT; + if(data === undefined) + { + data = {}; + status = TableDb.TABLE_STATUS.ADD; + } + + return ( +
+ ) + + } + */ + + renderUiTable() + { + return( + + + + + + + + + + + + + + + + {this.renderCancelFilterIcon()} + + this.menuClicked("edit")} aria-label="search" color="inherit"> + + + + + + + + this.menuClicked("add")} aria-label="search" color="inherit"> + + + + + + + + + + +
+ {this.renderTable()} +
+
+
+ ) + } + + renderUiForm() + { + return this.renderForm(); + } + + renderMenu() + { + return ( + + ); + } +} + +export default Nodes; \ No newline at end of file diff --git a/src/components/relays/menu.js b/src/components/relays/menu.js new file mode 100644 index 0000000..ceee4ab --- /dev/null +++ b/src/components/relays/menu.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; + +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; + +import MenuList from '@material-ui/core/MenuList'; +import { grey } from '@material-ui/core/colors'; +import { green } from '@material-ui/core/colors'; +import { red } from '@material-ui/core/colors'; +import { blue } from '@material-ui/core/colors'; + +import AddCircleIcon from '@material-ui/icons/AddCircle'; +import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; +import EditIcon from '@material-ui/icons/Edit'; +import SearchIcon from '@material-ui/icons/Search'; +import Typography from '@material-ui/core/Typography'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; + +const StyledMenu = withStyles({ + paper: { + border: '1px solid #d3d4d5', + }, +})((props) => ( + +)); + +/* +const StyledMenuItem = withStyles((theme) => ({ + root: { + '&:focus': { + backgroundColor: theme.palette.primary.main, + '& .MuiListItemIcon-root, & .MuiListItemText-primary': { + color: theme.palette.common.white, + }, + }, + }, +}))(MenuItem); +*/ + +export default function CustomizedMenus(props) { + const [anchorEl, setAnchorEl] = React.useState(props.anchorref); + + const handleClick = (event, menu) => { + setAnchorEl(event.currentTarget); + props.menuClicked(menu); + }; + + const handleClose = () => { + setAnchorEl(null); + props.menuClosed(); + }; + + return ( +
+ + + + handleClick(event, "add")}> + + + + pridať + + handleClick(event, "edit")}> + + + + editovat + + handleClick(event, "delete")}> + + + + zmazať + + handleClick(event, "exit")}> + + + + logout + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/relays/relays.js b/src/components/relays/relays.js new file mode 100644 index 0000000..121d664 --- /dev/null +++ b/src/components/relays/relays.js @@ -0,0 +1,208 @@ +import React from 'react'; + +import TableDb from 'components/table/tableDb'; +//import TableDb from '../table/tableDb'; +import DB from "util/db/db"; +//import { LogicOperator } from '../../util/db/SqlQueryBuilder'; + +//import Find from './find'; +//import Form from './form'; +import CustomizedMenus from './menu' + +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +//import Card from "components/Card/Card.js"; +//import CardBody from "components/Card/CardBody.js"; +//import CardHeader from "components/Card/CardHeader.js"; + +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import Paper from '@material-ui/core/Paper'; + +import SearchIcon from '@material-ui/icons/Search'; +import AddIcon from '@material-ui/icons/Add'; +import EditIcon from '@material-ui/icons/Edit'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; + +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; + +//----------------------------------------------------- + +class Relays extends TableDb { + constructor(props) { + super(props); + + this.title = "Zoznam línií"; + this.dbTable = "relays"; + +//node:number|tbname:string|line:number|profile:string|processed:boolean|status:boolean + + this.state.columns = ["actions", "line", "tbname", "contactor"]; + this.state.visibleColumns = ["actions", "line", "tbname", "contactor"]; + + this.renamedColumns = { + line: "línia", + contactor: "spínač", + tbname: "tbname", + + }; + } + + menuClicked(menu) + { + if(menu === "delete") + { + if(this.selectedRows.length === 0) + { + this.menuClosed(); + this.showAlertDialog("Mazanie", "Nie sú označené žiadne riadky"); + return; + } + + var r = window.confirm("Naozaj chcete zmazať?"); + if (r === false) return; + + let db = new DB(); + db.dbStartTransaction(); + + //default - odstranit z subject + let conditions = {"id": this.id}; + db.dbDelete(this.getTableName(false), conditions); + db.dbCommitTransaction(); + + let index = this.index; + + db.exec().then( + result => { + this.deleteRow(index); + }, + error => { + console.log(error); + this.showAlertDialog("Chyba", error); + } + ); + + return; + } + else if(menu === "exit") + { + this.props.logout(); + } + + let processed = super.menuClicked(menu); + if(processed) return; + } + + getTableName(withView = false) + { + //if(withView) return "view_subject"; + //return this.dbTable; + return this.dbTable; + } + + //add or edit + /* + renderForm() + { + let data = undefined; + if(this.state.data == null); + else data = this.state.data[this.index]; + + //getIndex + //console.log(data);alert(); + + let status = TableDb.TABLE_STATUS.EDIT; + if(data === undefined) + { + data = {}; + status = TableDb.TABLE_STATUS.ADD; + } + + return ( + + ) + + } + */ + + renderUiTable() + { + return( + + + + + + + + + + + + + + + + {this.renderCancelFilterIcon()} + + this.menuClicked("edit")} aria-label="search" color="inherit"> + + + + + + + + this.menuClicked("add")} aria-label="search" color="inherit"> + + + + + + + + + +
+ {this.renderTable()} +
+
+
+ ) + } + + renderUiForm() + { + return this.renderForm(); + } + + renderMenu() + { + return ( + + ); + } +} + +export default Relays; \ No newline at end of file diff --git a/src/components/table/menu.js b/src/components/table/menu.js new file mode 100644 index 0000000..93a3d25 --- /dev/null +++ b/src/components/table/menu.js @@ -0,0 +1,106 @@ +/* default menu */ + +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; + +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; + +import MenuList from '@material-ui/core/MenuList'; +import { green } from '@material-ui/core/colors'; +import { red } from '@material-ui/core/colors'; +import { blue } from '@material-ui/core/colors'; + +import AddCircleIcon from '@material-ui/icons/AddCircle'; +import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; +import EditIcon from '@material-ui/icons/Edit'; +import SearchIcon from '@material-ui/icons/Search'; +import Typography from '@material-ui/core/Typography'; + +const StyledMenu = withStyles({ + paper: { + border: '1px solid #d3d4d5', + }, +})((props) => ( + +)); + +/* +const StyledMenuItem = withStyles((theme) => ({ + root: { + '&:focus': { + backgroundColor: theme.palette.primary.main, + '& .MuiListItemIcon-root, & .MuiListItemText-primary': { + color: theme.palette.common.white, + }, + }, + }, +}))(MenuItem); +*/ + +export default function CustomizedMenus(props) { + const [anchorEl, setAnchorEl] = React.useState(props.anchorref); + + const handleClick = (event, menu) => { + setAnchorEl(event.currentTarget); + props.menuClicked(menu); + }; + + const handleClose = () => { + setAnchorEl(null); + props.menuClosed(); + }; + + return ( +
+ + + + handleClick(event, "add")}> + + + + pridať + + handleClick(event, "edit")}> + + + + editovať + + handleClick(event, "delete")}> + + + + zmazať + + handleClick(event, "find")}> + + + + hľadať + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/table/table.js b/src/components/table/table.js new file mode 100644 index 0000000..0117705 --- /dev/null +++ b/src/components/table/table.js @@ -0,0 +1,401 @@ +import React from 'react'; +import { withStyles, makeStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; + +import Grid from '@material-ui/core/Grid'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Switch from '@material-ui/core/Switch'; + +import { blue } from '@material-ui/core/colors'; + +import TableSortLabel from '@material-ui/core/TableSortLabel'; + +import IconButton from '@material-ui/core/IconButton'; +import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; + +let sorted = []; + +function getMaxHeight(isMobile = true) +{ + let h = window.innerHeight - 250; + if(!isMobile) h = h - 40;//setDense widget + + if(h < 200) h = 200; + return h; +} + +const useStyles = makeStyles({ + root: { + width: '100%' + }, + container: { + maxHeight: getMaxHeight(), + }, +}); + +const StyledTableCell = withStyles((theme) => ({ + head: { + backgroundColor: '#6bb7fd', + color: theme.palette.common.white, + }, + body: { + fontSize: 14, + }, +}))(TableCell); + +const StyledTableRow = withStyles((theme) => ({ + root: { + '&:nth-of-type(even)': { + backgroundColor: 'gainsboro', + }, + }, +}))(TableRow); + + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +export default function StickyHeadTable(props) { + const classes = useStyles(); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(10); + const [order, setOrder] = React.useState('asc'); + const [orderBy, setOrderBy] = React.useState('id'); + const [dense, setDense] = React.useState(props.dense); + const [selectedRows, setSelectedRows] = React.useState([]); + const [lastIndex, setLastIndex] = React.useState(-1); + const [maxHeight, setMaxHeight] = React.useState(getMaxHeight(props.isMobile)); + const [selectableText, setSelectableText] = React.useState("none"); + + + const resizeFunction = () => { + setMaxHeight(getMaxHeight(props.isMobile)); + }; + + React.useEffect(() => { + + window.addEventListener("resize", resizeFunction); + window.addEventListener("orientationchange", resizeFunction); + // Specify how to clean up after this effect: + return function cleanup() { + + window.removeEventListener("resize", resizeFunction); + window.removeEventListener("orientationchange", resizeFunction); + }; + }, []); + + const handleChangeDense = (event) => { + setDense(event.target.checked); + + if(props.handleChangeDense !== undefined) + { + props.handleChangeDense(dense); + } + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(+event.target.value); + setPage(0); + }; + + const actionStyle={ + whiteSpace: "nowrap", + textOverflow: "ellipsis", + width: "70px", + display: "block", + overflow: "hidden" + } + + const showDataFromColumns = (row) => { + + let result = []; + if(props.columns.length > 0){ + for (let column of props.columns) { + if(!props.visibleColumns.includes(column)) continue; + + if(column === "actions") + { + + result.push( +
+ handleEditIconClicked(row)}> + + + handleDeleteIconClicked(row)}> + + +
+
); + // setOpen(!open)}> + //{open ? : } + // + } + else if(row.hasOwnProperty(column)){ + result.push( + {row[column]} + ); + } else { + result.push( + {null} + ); + } + } + } + return result; + } + + + const createSortHandler = (property) => (event) => { + handleRequestSort(event, property); + }; + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const renameTableColumn = (column) =>{ + if(column in props.renamedColumns) return props.renamedColumns[column]; + return column; + } + + const isSelected = (_index) => selectedRows.indexOf(_index) !== -1; + + const handleEditIconClicked = (row) => { + + let newSelected = []; + newSelected.push(row._index); + setSelectedRows(newSelected); + + props.handleTableClick(newSelected); + props.handleTableDoubleClick(null, row.id, row._index) + }; + + const handleDeleteIconClicked = (row) => { + + let newSelected = []; + newSelected.push(row._index); + setSelectedRows(newSelected); + + props.handleTableClick(newSelected); + props.handleTableDeleteClick(null, row.id, row._index); + }; + + const handleTableClick = (event, row, index) => { + + let newSelected = []; + + //console.log("table - handleClick", row, row._index, index); + + if (event.ctrlKey) { + const indx = selectedRows.indexOf(row._index); + + newSelected = [...selectedRows]; + + if (indx === -1) { + newSelected.push(row._index); + } + else + { + newSelected.splice(indx, 1); + } + + setLastIndex(index); + + }else if (event.shiftKey) { + + if(selectedRows.length === 0) + { + newSelected.push(row._index); + } + + let fromValue = lastIndex; + let toValue = index; + + if( fromValue > toValue ) + { + fromValue = index; + toValue = lastIndex; + } + + //console.log(fromValue, toValue); + + newSelected = [...selectedRows]; + for(let indx = fromValue; indx <= toValue; indx++) + { + let row = sorted[indx]; + const t = selectedRows.indexOf(row._index); + + if (t === -1) { + newSelected.push(row._index); + } + + } + + }else { + + const indx = selectedRows.indexOf(row._index); + + if (indx === -1) { + newSelected.push(row._index); + } + else + { + selectedRows.splice(indx, 1); + } + + setLastIndex(index); + + } + + if(newSelected.length === 0) setLastIndex(index); + + setSelectedRows(newSelected); + props.handleTableClick(newSelected); + + } + + if(props.data === undefined || props.data === null) + { + return ; + } + + sorted = stableSort(props.data, getComparator(order, orderBy)).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + const handleSelectText = () => + { + selectableText == 'none' ? setSelectableText("text"): setSelectableText("none"); + } + + return ( + + + + + + + {Object.keys(props.columns).map((key, index) => { + + let column = props.columns[key]; + + //use filter? + if(!props.visibleColumns.includes(column)) return null; + + if(column === "actions") + { + return ( + + + ) + } + + return ( + + + {renameTableColumn(column)} + + + ) + }) + } + + + + {sorted.map((row, index) => { + const isItemSelected = isSelected(row._index); + + let backgroundColor; + if(isItemSelected){ + backgroundColor = blue[200]; + } + + return ( + handleTableClick(event, row, index)} + onDoubleClick={(event) => props.handleTableDoubleClick(event, row.id, row._index)} + > + {showDataFromColumns(row)} + + ) + })} + +
+
+ +
+ + { + props.isMobile ? null : } + label="Kompaktné zobrazenie" + /> + } + + } + label="Oznacovat text ?" + /> + +
+ ); +} diff --git a/src/components/table/tableDb.js b/src/components/table/tableDb.js new file mode 100644 index 0000000..bf23dff --- /dev/null +++ b/src/components/table/tableDb.js @@ -0,0 +1,582 @@ +import React, { Component } from 'react'; + +import DB from 'util/db/db'; +import cloneDeep from 'lodash.clonedeep' +import SqlQueryBuilder, { LogicOperator } from 'util/db/SqlQueryBuilder'; +import AlertDialog from 'components/AlertDialog/AlertDialog'; +import StickyHeadTable from 'components/table/table'; + +import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; +import { red } from '@material-ui/core/colors'; +import _ from 'lodash'; + +import isMobile from 'util/isMobile'; + +import Button from '@material-ui/core/Button'; +import Tooltip from '@material-ui/core/Tooltip'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import Typography from '@material-ui/core/Typography'; + +// card core components +//import Card from './../Card/Card'; +//import CardBody from './../Card/CardBody.js'; +//import CardHeader from './../Card/CardHeader.js'; + +import Card from '@material-ui/core/Card'; +import CardBody from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; + +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +//----------------------------------------------------- + + +const initialState = { + data: null, + columns: [], + visibleColumns: [], + usePagination: true, + index: null, + deleteConfirmed: false, + showFindDialog: false, + showFilter: false, + showAlert: false, + alertTitle: "", + alertType: "warning", + alertContent: "", +}; + + + +class TableDb extends React.Component { + + static TABLE_STATUS = { + EDIT: 'EDIT', + ADD: 'ADD', + SHOWTABLE: 'SHOWTABLE', + } + + constructor(props) { + super(props); + + this.state = initialState; + this.state.status = TableDb.TABLE_STATUS.SHOWTABLE; + + this.db = new DB(); + this.dbTable = ""; + this.selectedRows = [];//list of indexes + + this.showFormInDialog = false; + this.fullscreen = false; + + this.id = null; + this.index = null; + + this.anchorMenuRef = React.createRef(); + + if(props.isMobile !== undefined) this.setDense(!props.isMobile); + else this.setDense(!isMobile()); + } + + display(type) + { + //to prevent rendering when add/edit is finished + if(type === "EDIT") + { + //if(this.state.rowID > 0) return null; + //if(this.state.status === "EDIT") return null; + if(this.state.status === TableDb.TABLE_STATUS.ADD) return null; + if(this.state.status === TableDb.TABLE_STATUS.EDIT) return null; + + return "none"; + } + + if(type === "TABLE") + { + //if(this.state.status === "EDIT") return "none"; + if(this.state.status === TableDb.TABLE_STATUS.ADD) return "none"; + if(this.state.status === TableDb.TABLE_STATUS.EDIT) return "none"; + return null; + + //if(this.state.rowID > 0) return "none"; + //return null; + } + } + + setDense(dense) + { + this.dense = dense; + } + + setStatus(status) + { + this.setState({"status": status}); + } + + getStatus() + { + if(this.state.status === TableDb.TABLE_STATUS.EDIT || this.state.status === TableDb.TABLE_STATUS.ADD) return "EDIT"; + else return "SHOWTABLE"; + } + + renderForm() + { + return

Editacia riadku

; + } + + renderTable() + { + //console.log(this.state.data); + //alert("render table"); + return( + + ) + } + + renderUiForm(status) + { + + let form = this.renderForm(); + + let txt = "editácia"; + if(this.state.status === TableDb.TABLE_STATUS.ADD) txt = "nový záznam"; + //TODO set vytvorenie + + return ( + + + + + + + + + + + +
+ {form} +
+
+
+ ) + } + + renderUiTable() + { + return( + + + + + + {this.renderCancelFilterIcon()} + + +
+ {this.renderTable()} +
+
+
+ ) + } + + renderUi() + { + let status = this.getStatus(); + let form = null; + + //add or edit record + if(status !== TableDb.TABLE_STATUS.SHOWTABLE) + { + form = this.renderUiForm(); + } + + return( + <> +
+ {form} +
+ +
+ {this.renderUiTable()} +
+ + ) + } + + render() { + + let findDialog = null; + + if(this.state.showFindDialog) + { + findDialog = this.renderFindDialog(); + } + + return ( +
+ {this.renderUi()} + {this.renderMainMenu()} + {findDialog} + {this.renderAlertDialog()} +
+ ) + } + + componentDidMount() + { + this.showTable(); + } + + renderCancelFilterIcon() + { + if (this.state.showFilter) return ; + return null; + } + + handleCancelFilter() + { + //let newState = {...this.state}; + //newState.showFilter = false; + //this.setState(newState); + //this.state.showFilter = false; + + this.setState(prevstate => ({ showFilter: false})); + + this.showTable(); + } + + setRow(row) + { + + } + + showTable(sqlQueryBuilder) + { + + if(sqlQueryBuilder === undefined) sqlQueryBuilder = this.getDefaultQueryBuilder(); + + //!force to show progress + this.setState(prevstate => ({ showFilter: prevstate.showFilter, data: null})); + + this.db.dbSelect(sqlQueryBuilder, true).then( + result => { + result = this.db.updateJsonTypes(result, this.getTableName(true)); + //console.log(result); + + //modify data + for(let i = 0; i < result.length; ++i) + { + this.setRow(result[i]); + } + + let newState = {...this.state}; + newState.data = result; + this.setState(newState); + //this.showAlertDialog("Info", "Data boli načítané") + }, + error => { + //alert(error); + this.showAlertDialog("Chyba", error); + } + ); + } + + getTableName(withView = false) + { + return this.dbTable; + } + + getDefaultQueryBuilder() + { + + let table = this.getTableName(true); + if(table === "") new Error("table name or view is not specified"); + + let sqlQueryBuilder = new SqlQueryBuilder(); + sqlQueryBuilder.from( table ); + + return sqlQueryBuilder; + } + + buildFindSqlQuery(findDialogParams) + { + if(_.isEmpty(findDialogParams)) + { + this.setState(prevstate => ({ showFindDialog: false, showFilter: false})); + return null; + } + + this.setState(prevstate => ({ showFindDialog: false, showFilter: true})); + + return this.getDefaultQueryBuilder(); + } + + handleToggleMenu() + { + this.setState(prevstate => ({ openMenu: true})); + } + + //run edit mode + handleTableDoubleClick(event, id, index){ + console.log("handleTableDoubleClick:", id, index); + + this.id = id; + this.index = index; + + this.setStatus(TableDb.TABLE_STATUS.EDIT); + + //this.setState(prevstate => ({ rowID: id, index: index})); + } + + handleTableClick(selectedRows){ + //console.log("tableDb::handleTableClick", selectedRows); + this.selectedRows = selectedRows; + }; + + + handleTableDeleteClick(event, id, index){ + + this.id = id; + this.index = index; + + this.menuClicked("delete"); + } + + + //end of AddRecord/EditRecord + handleBack(params){ + + //TODO refactor it + + //console.log(params, this.selectedRows); + + if(params === undefined) + { + this.setState({"status": TableDb.TABLE_STATUS.SHOWTABLE}); + return; + } + + let index = this.index; + //if(this.selectedRows.length === 1) index = this.selectedRows[0]; + + if(index === undefined || index === null) + { + for(let i = 0; i < this.state.data.length; i++) + { + if(this.state.data[i]["id"] === this.id) + { + index = i; + break; + } + } + } + + //TODO clone??? + let newState = this.state;//{...this.state}; + //let newState = {}; + newState.status = TableDb.TABLE_STATUS.SHOWTABLE; +// + //update current row; + if(index !== null) + { + //newState.data = {...this.state.data[index]}; + for (var prop in params) { + newState.data[index][prop] = params[prop]; + } + } + else + { + //console.log("handle back - index is null"); + + //TODO fix get last element and it's _index!!!!! + var last_element = this.state.data[this.state.data.length - 1]; + if(last_element === undefined) + { + //WHAT + //alert("tableDb.js fix"); + } + else{ + params["_index"] = last_element._index + 1; + newState.data.push(params); + } + + + + newState.status = TableDb.TABLE_STATUS.SHOWTABLE; + } + + this.setState(newState); + + } + + //reimplement it + renderMenu() + { + return null; + } + + renderMainMenu() + { + if(this.anchorMenuRef == null) return null; + if(this.anchorMenuRef.current == null) return null; + if(!this.state.openMenu) return null; + + return this.renderMenu(); + } + + renderFindDialog() + { + return null; + } + + renderAlertDialog() + { + return ( + + ) + } + + hideAlertDialog() + { + this.setState({showAlert: false}); + } + + showAlertDialog(title, message, type) + { + if(type === undefined) type = "warning"; + this.setState(prevstate => ({showAlert: true, alertTitle: title, alertContent: message, alertType: type})); + } + + menuClicked(menu) + { + this.menuClosed(); + + if(menu === "add") + { + this.index = null; + this.id = null; + + this.setState(prevstate => ({ + openMenu: false, + index: null, + status: TableDb.TABLE_STATUS.ADD + } + )); + + + } + else if(menu === "find") + { + this.setState(prevstate => ({ showFindDialog: true})); + return true; + } + else if(menu === "edit") + { + if(this.selectedRows.length === 0) + { + this.showAlertDialog("Editácia", "Nie sú označené žiadne riadky"); + return; + } + else if(this.selectedRows.length > 1) + { + this.showAlertDialog("Editácia", "Označte jeden riadkok"); + return; + } + + let index = this.selectedRows[0]; + if(index === undefined) + { + alert("menuClicked index is undefined"); + return; + } + + this.id = this.state.data[index].id; + this.index = index; + + this.setStatus(TableDb.TABLE_STATUS.EDIT); + } + else if(menu === "delete") + { + if(this.selectedRows.length === 0) + { + this.showAlertDialog("Mazanie", "Nie sú označené žiadne riadky"); + } + + let data = cloneDeep(this.state.data); + data.splice(this.index, 1) + + //update _index!!! + for (let i = 0; i < data.length; i++) { + data[i]._index = i; + } + + //this.setState(prevstate => ({ data: this.state.data.splice(this.index, 1)})); + + this.setState({data: data}); + //this.showAlertDialog("Mazanie", "pripravujeme..."); + + return true; + + } + + return false; + } + + deleteRow(index) + { + let data = cloneDeep(this.state.data); + data.splice(index, 1) + + //update _index!!! + for (let i = 0; i < data.length; i++) { + data[i]._index = i; + } + + this.setState({data: data}); + } + + menuClosed() + { + let newState = this.state; + newState.openMenu = false; + + this.setState(newState); + + //this.setState(prevstate => ({ openMenu: false})); + }; + +} + +export default TableDb; \ No newline at end of file diff --git a/src/config/config.js b/src/config/config.js new file mode 100644 index 0000000..bdf9832 --- /dev/null +++ b/src/config/config.js @@ -0,0 +1,138 @@ +/* +0 - cislo registra / prikaz +1 - recepient - 0 = master, 1 = slave (slave je automaticky group a broadcast) +2 - r/rw - read/write +3- register name - nazov registra / prikazu (len pre info - zobrazenie v aplikacii) + +4,5,6,7, - jednotlive byte-y - nazov byte-u +4-7 - RES. a prazdny string "" sa nezobrazia!!! +*/ + +//124 zaznamov +export const config = [ + ["0","1","R","Status","","",""], + ["1","1","RW","Dimming","R-Channel ","G-Channel","B-Channel ","W - Channel"], + ["2","1","RW","Device types","","","",""], + ["3","1","RW","Group addresses 1-4","Groups Add. 4","Groups Add. 3","Groups Add. 2","Groups Add. 1"], + ["4","1","RW","Group addresses 5-8","Groups Add. 8","Groups Add. 7","Groups Add. 6","Groups Add. 5"], + ["5","1","RW","Serial number (MAC)","","","",""], + ["6","1","RW","Time of dusk","HH","MM","SS","EXTRA"], + ["7","1","RW","Time of dawn","HH","MM","SS","EXTRA"], + ["8","1","RW","Time Schedule settings","TBD","TBD","Movement sensor","Time Schedule"], + ["9","1","RW","TS1 Time point 1","HH","MM","SS","Ext. Device"], + ["10","1","RW","TS1 Time point 1 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["11","1","RW","TS1 Time point 2","HH","MM","SS","Ext. Device"], + ["12","1","RW","TS1 Time point 2 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["13","1","RW","TS1 Time point 3","HH","MM","SS","Ext. Device"], + ["14","1","RW","TS1 Time point 3 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["15","1","RW","TS1 Time point 4","HH","MM","SS","Ext. Device"], + ["16","1","RW","TS1 Time point 4 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["17","1","RW","TS1 Time point 5","HH","MM","SS","Ext. Device"], + ["18","1","RW","TS1 Time point 5 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["19","1","RW","TS1 Time point 6","HH","MM","SS","Ext. Device"], + ["20","1","RW","TS1 Time point 6 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["21","1","RW","TS1 Time point 7","HH","MM","SS","Ext. Device"], + ["22","1","RW","TS1 Time point 7 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["23","1","RW","TS1 Time point 8","HH","MM","SS","Ext. Device"], + ["24","1","RW","TS1 Time point 8 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["25","1","RW","TS1 Time point 9","HH","MM","SS","Ext. Device"], + ["26","1","RW","TS1 Time point 9 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["27","1","RW","TS1 Time point 10","HH","MM","SS","Ext. Device"], + ["28","1","RW","TS1 Time point 10 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["29","1","RW","TS1 Time point 11","HH","MM","SS","Ext. Device"], + ["30","1","RW","TS1 Time point 11 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["31","1","RW","TS1 Time point 12","HH","MM","SS","Ext. Device"], + ["32","1","RW","TS1 Time point 12 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["33","1","RW","TS1 Time point 13","HH","MM","SS","Ext. Device"], + ["34","1","RW","TS1 Time point 13 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["35","1","RW","TS1 Time point 14","HH","MM","SS","Ext. Device"], + ["36","1","RW","TS1 Time point 14 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["37","1","RW","TS1 Time point 15","HH","MM","SS","Ext. Device"], + ["38","1","RW","TS1 Time point 15 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["39","1","RW","TS1 Time point 16","HH","MM","SS","Ext. Device"], + ["40","1","RW","TS1 Time point 16 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["41","1","RW","TS2 Time point 1","HH","MM","SS","Ext. Device"], + ["42","1","RW","TS2 Time point 1 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["43","1","RW","TS2 Time point 2","HH","MM","SS","Ext. Device"], + ["44","1","RW","TS2 Time point 2 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["45","1","RW","TS2 Time point 3","HH","MM","SS","Ext. Device"], + ["46","1","RW","TS2 Time point 3 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["47","1","RW","TS2 Time point 4","HH","MM","SS","Ext. Device"], + ["48","1","RW","TS2 Time point 4 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["49","1","RW","TS2 Time point 5","HH","MM","SS","Ext. Device"], + ["50","1","RW","TS2 Time point 5 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["51","1","RW","TS2 Time point 6","HH","MM","SS","Ext. Device"], + ["52","1","RW","TS2 Time point 6 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["53","1","RW","TS2 Time point 7","HH","MM","SS","Ext. Device"], + ["54","1","RW","TS2 Time point 7 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["55","1","RW","TS2 Time point 8","HH","MM","SS","Ext. Device"], + ["56","1","RW","TS2 Time point 8 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["57","1","RW","TS2 Time point 9","HH","MM","SS","Ext. Device"], + ["58","1","RW","TS2 Time point 9 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["59","1","RW","TS2 Time point 10","HH","MM","SS","Ext. Device"], + ["60","1","RW","TS2 Time point 10 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["61","1","RW","TS2 Time point 11","HH","MM","SS","Ext. Device"], + ["62","1","RW","TS2 Time point 11 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["63","1","RW","TS2 Time point 12","HH","MM","SS","Ext. Device"], + ["64","1","RW","TS2 Time point 12 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["65","1","RW","TS2 Time point 13","HH","MM","SS","Ext. Device"], + ["66","1","RW","TS2 Time point 13 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["67","1","RW","TS2 Time point 14","HH","MM","SS","Ext. Device"], + ["68","1","RW","TS2 Time point 14 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["69","1","RW","TS2 Time point 15","HH","MM","SS","Ext. Device"], + ["70","1","RW","TS2 Time point 15 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["71","1","RW","TS2 Time point 16","HH","MM","SS","Ext. Device"], + ["72","1","RW","TS2 Time point 16 Levels","R-Channel","G-Channel","B-Channel","W - Channel"], + ["73","1","RW","Power meter status","TBD","","",""], + ["74","1","R","Input Voltage","","","",""], + ["75","1","R","Input Current","","","",""], + ["76","1","R","Input Power","","","",""], + ["77","1","R","Cos phi","","","",""], + ["78","1","R","Frequency","","","",""], + ["79","1","RW","Energy","","","",""], + ["80","1","RW","Lifetime","","","",""], + ["81","1","RW","Power on cycles (input)","","","",""], + ["82","1","RW","Power on cycles (relay)","","","",""], + ["83","1","R","Time since last power on","","","",""], + ["84","1","R","Accelerometer data","","","",""], + ["85","1","RW","GPS latitude","pos/neg","deg","min ","sec"], + ["86","1","RW","GPS longitude","pos/neg","deg","min ","sec"], + ["87","1","RW","Actual time","HH","MM","SS","RES."], + ["88","1","RW","Actual date","Day of week","Day","Month","Year"], + ["89","1","R","Production data 1","","","",""], + ["90","1","R","Production data 2","","","",""], + ["91","1","RW","Network ID","NID3","NID2","NID1","NID0"], + ["95","1","RW","Actual Lux level from cabinet","RES.","RES.","HB","LB"], + ["96","1","RW","Threshold lux level","Dusk HB","Dusk LB","Dawn HB","Dawn LB"], + ["97","1","RW","Adjust period","Dusk HB","Dusk LB","Dawn HB","Dawn LB"], + ["98","1","RW","Offset","RES.","RES.","Dusk","Dawn"], + ["99","1","RW","CCT min/max range","max-H","max-L","min-H","min-L"], + ["100","1","RW","DALI interface","Cmd ID","Add","Cmd","Resp"], + ["101","1","RW","Module FW ver","v1","v2","v3","v4"], + ["102","1","RW","Module MAC-H","unused","unused","M1","M2"], + ["103","1","RW","Module MAC-L","M3","M4","M5","M6"], + ["104","1","RW","Sensor timeout","","","Timeout H","Timeout L"], + ["122","1","R","FW update status/control register","Byte3","Byte2","Byte1","Byte0"], + ["123","1","R","FW update - data index","Byte3","Byte2","Byte1","Byte0"], + ["124","1","R","FW update - data","Byte3","Byte2","Byte1","Byte0"], + ["0","0","R","Status","","","",""], + ["1","0","RW","Control register","RES.","RES.","RES.","init mode enable"], + ["2","0","R","Device types","","","",""], + ["3","0","R","Serial number (MAC)","","","",""], + ["4","0","R","Production data 1","","","",""], + ["5","0","R","Production data 2","","","",""], + ["6","0","RW","Network ID","NID3","NID2","NID1","NID0"], + ["7","0","RW","RS232 settings","param.","param.","Baudrate H","Baudrate L"], + ["8","0","R","Accelerometer data","","","",""], + ["9","0","RW","Module FW ver","v1","v2","v3","v4"], + ["10","0","RW","Module MAC-H","unused","unused","M1","M2"], + ["11","0","RW","Module MAC-L","M3","M4","M5","M6"], + ["32","0","RW","FW update status/control register","Byte3","Byte2","Byte1","Byte0"], + ["33","0","RW","FW update - data index","Byte3","Byte2","Byte1","Byte0"], + ["34","0","RW","FW update - data","Byte3","Byte2","Byte1","Byte0"], + ["125","0","RW","Debug Register","Byte3","Byte2","Byte1","Byte0"], + ["126","0","RW","Network Control Register","Byte3","Byte2","Byte1","Byte0"], + ["127","0","R","Network Status Register","Byte3","Byte2","Byte1","Byte0"], + ["128","0","RW","Node XX Serial Number Register","SER3","SER2","SER1","SER0"], + ["256","0","R","Node XX Network Status Register","","","",""] +]; \ No newline at end of file diff --git a/src/controls.js b/src/controls.js new file mode 100644 index 0000000..88eb04d --- /dev/null +++ b/src/controls.js @@ -0,0 +1,770 @@ +import React, {Component} from 'react' + +import Grid from '@material-ui/core/Grid'; +import Select from '@material-ui/core/Select'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; +import TextField from '@material-ui/core/TextField'; + +import IconButton from '@material-ui/core/IconButton'; +import Menu from '@material-ui/core/Menu'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +import axios from 'axios'; + + +import { crc8, crc16, crc32 } from 'easy-crc'; + +import {connect} from 'react-redux' +import * as actionTypes from './store/actions'; + +import * as data from './config/config'; + +//axios.defaults.baseURL = 'http://localhost:5000/'; + +function getParams() +{ + let params = {}; + + //core rpc values + params.address = 0;//if(recipient === 0) address = 0; + params.byte1 = 0; + params.byte2 = 0; + params.byte3 = 0; + params.byte4 = 0; + params.recipient = 0;//0: Master, 1: Slave, 2: Broadcast + params.register = 0;//register number + params.rw = 0;//0: read, 1: write + + //other values + //params.type = "rpc"; + //params.tbname = tbname; + //params.timestamp = 0;//execution time + //params.addMinutesToTimestamp = 0;//repeat if > 0, + //params.debug = ""; + + return params; +} + +class Controls extends Component { + constructor(props) { + super(props); + + + //default values + let tasks = this.buildTasks("R", 0); + let registerNumber = tasks[0].registerNumber; + + this.state = { + rw: "R", + recipient: 0, + registerNumber: registerNumber, + index: 0, + + anchorEl: null, + + requestToHost: "", + responsefromHost: "", + tasks: tasks, + showAddress: false, + address: "", + byte1Text: "", + byte2Text: "", + byte3Text: "", + byte4Text: "", + byte1: "", + byte2: "", + byte3: "", + byte4: "" + } + } + + checkForm = () =>{ + + let errors = []; + if(this.state.showAddress) + { + if(this.state.address === "") + { + errors.push("Address can not be empty"); + } + else if (!Number.isInteger( parseInt(this.state.address) )) { + errors.push("Address is not integer"); + } + } + + if(errors.length > 0) + { + alert(errors.join("\n")); + return false; + } + + return true; + } + + //test + handleResponse = (bytes) => + { + //local add = bit.bor(bit.lshift(bytes[1], 24), bit.lshift(bytes[2], 16), bit.lshift(bytes[3], 8), bytes[4]); + + //https://www.w3schools.com/js/js_bitwise.asp + + let type = "RESPONSE"; + if(bytes[4] === 0) type = "RESPONSE"; + else if(bytes[4] === 1) type = "ERROR"; + else if(bytes[4] === 2) type = "EVENT"; + else type = "UNKNOWN"; + + let crc = crc16('ARC', bytes.slice(0, 9)); + let c1 = (crc >> 8) & 0xFF; + let c2 = crc & 0xFF; + + + console.log("crc", bytes, c1, c2); + + //let add = (bytes[1] >> 24) + } + + //test + com_generic = (adresa, rec, rw, register, name, byte1, byte2, byte3, byte4) =>{ + let resp = []; + + let cmd = register; + + if (rw === 0) + { + cmd = cmd + 0x8000; + } + + if (rec === 3) + { + resp.push(0xFF); + resp.push(0xFF); + resp.push(0xFF); + resp.push(0xFF); + resp.push( adresa & 0xFF );//band + } + else + { + resp.push( (adresa >> 24) & 0xFF);//rshift + resp.push( (adresa >> 16) & 0xFF); + resp.push( (adresa >> 8) & 0xFF); + resp.push( adresa & 0xFF ); + + if (rec === 2) + { + resp.push(0xFF); + } + else resp.push(0); + } + + resp.push( (cmd >> 8) & 0xFF);//rshift + resp.push( cmd & 0xFF );//band + resp.push( byte1 & 0xFF );//band + resp.push( byte2 & 0xFF );//band + resp.push( byte3 & 0xFF );//band + resp.push( byte4 & 0xFF );//band + + //let data = '12345'; + let crc = crc16('ARC', resp); + let c1 = (crc >> 8) & 0xFF; + let c2 = crc & 0xFF; + + resp.push(c1); + resp.push(c2); + + + console.log("checksum", crc); + console.log("resp", resp); + + return resp; + } + + //send DATA + handleClick = event => { + + //this.com_generic(100, 1, 80, 'Control register', 0, 0, 0, 0);return; + + //let bytes = [0,0,0,0,0,128,1,0,0,0,0]; + //this.handleResponse(bytes); + //return; + + if(!this.checkForm()) + { + return; + } + + const found = this.state.tasks.find(element => element.registerNumber === this.state.registerNumber); + if(found === undefined) + { + alert("Invalid register number: " + this.state.registerNumber); + return; + } + + let address = parseInt(this.state.address); + if (isNaN(address)) address = 0; + + let recipient = parseInt(this.state.recipient); + if (isNaN(recipient)) recipient = 0; + + //master + if(recipient === 0) address = 0; + + if(recipient === 2) + { + address = 0xffffffff;//Broadcast + } + + let byte1 = parseInt(this.state.byte1); + if (isNaN(byte1)) byte1 = 0; + + let byte2 = parseInt(this.state.byte2); + if (isNaN(byte2)) byte2 = 0; + + let byte3 = parseInt(this.state.byte3); + if (isNaN(byte3)) byte3 = 0; + + let byte4 = parseInt(this.state.byte4); + if (isNaN(byte4)) byte4 = 0; + + let register = parseInt(this.state.registerNumber); + + //Zadaný index potom prirátaš k registru 128. Takže napríklad pri indexe 1 sa vyčítava z registra 129. Pri indexe 20, by sa vyčítalo z registra 148.. + if(this.state.rw === "R" && this.state.registerNumber === "128") + { + register = 128 + parseInt(this.state.index); + } + + let rw = 0;//read + if(this.state.rw === "W") rw = 1; + + let cmd = register; + if (rw === 0) + { + //cmd = cmd + 0x8000; + } + + let paramsToSend = { + command: { + register: cmd, + name: found.registerName, + recipient: recipient, + rw: rw, + address: address, + byte1: byte1, + byte2: byte2, + byte3: byte3, + byte4: byte4, + }, + username: this.props.username, + password: this.props.password, + }; + + //adresa, rec, cmd, name, byte3, byte2, byte1, byte0 + console.log(paramsToSend.command); + + //com generic is modifying cmd!!! use only for debug / info + let result = this.com_generic( + paramsToSend.command.address, + paramsToSend.command.recipient, + paramsToSend.command.rw, + paramsToSend.command.register, + 'Control register', + byte1, + byte2, + byte3, + byte4 + ); + + + let registerNames = [];// = + " from " ;// + " (WRITE: " + result.join(", ") + ")"; + registerNames.push(found.registerName); + if (recipient === 0) + { + registerNames.push("from"); + registerNames.push("Master"); + } + + if (recipient === 1) + { + registerNames.push("from"); + //let slave = 2*256 + result[3]; + let slave = address; + + registerNames.push( slave ); + } + + registerNames.push( "(WRITE: " + result.join(", ") + ")" ); + + let registerName = registerNames.join(" "); + + let params = { + command: { + //registerNumber: this.state.registerNumber, + //registerName: found.registerName + " (WRITE: " + result.join(", ") + ")", + registerName: registerName, + direction: "in", + type: "command", + date: new Date() + } + } + + //co zapisujeme? + this.props.addCommand(params); + + let url = '/cmd'; + //if(window.location.hostname === "localhost") url = "http://localhost:5000/cmd"; + if(window.location.hostname === "localhost") + { + //url = "http://localhost:5000/cmd"; + url = "http://10.0.0.5:5000/cmd"; + } + + this.setState({ + requestToHost: url + }); + + console.log(url); + + //make http request + axios(url, + { + method: 'POST', + mode: 'no-cors', + data: paramsToSend, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + //"Content-Type": "text/html;charset=UTF-8" + }, + withCredentials: false, + credentials: 'same-origin', + }) + .then(response => { + //HANDLE response + console.log("RESPONSE", response.data.messages); + //alert(response.data.hostname); + + //multiple messages + if(Array.isArray(response.data.messages)) + { + + for(let i = 0; i < response.data.messages.length; i++) + { + let date = null; + let m = response.data.messages[i]; + let message; + let type; + + if(typeof m === 'object') + { + if ("date" in m) + { + date = new Date(m.date); + } + + message = m.message; + type = m.type; + + if(typeof message === 'object') + { + message = message.message; + type = message.type; + } + + + } + else + { + message = m; + } + + let params = { + command: { + responsefromHost: response.data.hostname, + direction: "out", + type: type, + message: message, + date: date + } + } + + this.props.addCommand(params); + + let hostInfo = response.data.hostname; + if(hostInfo === "localhost") hostInfo = {hostInfo} + else if(hostInfo !== "localhost") hostInfo = {hostInfo} + + this.setState({ + responsefromHost: hostInfo + }); + } + + return; + } + + + let params = { + command: { + direction: "out", + type: response.data.type, + message: response.data.message, + date: new Date() + } + } + + this.props.addCommand(params); + + }) + .catch(error => { + + //http request err + const message = error + ""; + + let params = { + command: { + direction: "out", + type: "error", + message: message, + date: new Date() + } + } + + this.props.addCommand(params); + + }); + }; + + handleClear = event => { + this.props.clearState(); + }; + + buildTasks = (rw, recipient) =>{ + + let tasks = []; + for (let i = 0; i < data.config.length; i++) { + + let registerNumber = data.config[i][0]; + let masterSlaveFlag = data.config[i][1]; + let rwFlag = data.config[i][2]; + let registerName = data.config[i][3]; + + /* + if (rwFlag.includes(rw)) { + if (recipient === masterSlaveFlag){ + tasks.push({registerNumber: registerNumber, registerName: registerName}); + } + } + */ + + + if (rwFlag.includes(rw)) { + //master + if (recipient === 0){ + if (masterSlaveFlag === "0"){ + tasks.push({registerNumber: registerNumber, registerName: registerName}); + } + } + else{ + //slave / Broadcast 1, 2 + if (masterSlaveFlag === "1"){ + tasks.push({registerNumber: registerNumber, registerName: registerName}); + } + } + } + + } + + return tasks; + } + + + menuItemSelected = item =>{ + this.props.handleCommand(item); + this.handleMenuClose(); + } + + handleMenuClose = event =>{ + this.setState({ + anchorEl: null + }); + } + + handleMenuClick = event =>{ + //this.anchorEl = event.currentTarget; + + this.setState({ + anchorEl: event.currentTarget + }); + } + + handleTextFieldChange = event =>{ + this.setState({ + [event.target.name]: event.target.value + }); + } + + handleChange = event => { + + let name = event.target.name; + let value = event.target.value; + + let rw = this.state.rw; + let recipient = this.state.recipient; + let registerNumber = this.state.registerNumber; + + let obj = { + ...this.state, + [name]: value, + } + + if(name === "rw") rw = value; + if(name === "registerNumber") registerNumber = value; + if(name === "recipient") + { + //slave + if(value === 1) obj.showAddress = true; + else obj.showAddress = false; + + recipient = value; + } + + let tasks = this.buildTasks(rw, recipient); + + if(name !== "registerNumber") + { + //tasks was rebuil, set first element + console.log(tasks); + + if(tasks.length > 0) + { + registerNumber = tasks[0].registerNumber; + obj.registerNumber = registerNumber; + } + + } + + //clear bit values and set labels + if(this.state.registerNumber !== registerNumber) + { + + for(let i = 0; i < 4; i++) + { + let key = "byte" + (i + 1); + + obj[key] = ""; + obj[key + "Text"] = ""; + } + + const found = data.config.find(element => element[0] === registerNumber); + if(found && rw === "W") + { + var bytes = found.slice(4, 8); + + for(let i = 0; i < bytes.length; i++) + { + let key = "byte" + (i + 1); + let value = bytes[i]; + + if(value === "RES.") value = ""; + + obj[key] = ""; + obj[key + "Text"] = value; + } + } + } + + //console.log(obj); + + obj.tasks = tasks; + + this.setState(obj); + }; + + render() { + + let MenuItemTasks = []; + for (var i = 0; i < this.state.tasks.length; i++) { + MenuItemTasks.push( {this.state.tasks[i].registerName} ); + } + + let address; + if(this.state.showAddress) + { + address = ; + } + + let bytes = []; + const found = this.state.tasks.find(element => element.registerNumber === this.state.registerNumber); + if(found !== undefined && this.state.rw === "W") + { + for(i = 0; i < 4; i++) + { + let key = "byte" + (i + 1); + + //byte1 MSB = data3, byte2 = data2, byte3 = data1, byte4 = data0 LSB + + let byteDescription = "byte" + (i + 1); + if(i === 0) byteDescription = byteDescription + " (MSB)"; + if(i === 3) byteDescription = byteDescription + " (LSB)"; + + let str = this.state[key + "Text"] + ": " + byteDescription; + + if(str !== "") + { + let byte = ; + bytes.push({byte}); + } + + } + } + + let index; + //Node XX Serial Number Register + //console.log(this.state.registerNumber); + if(this.state.rw === "R" && this.state.registerNumber === "128") + { + let key = "index"; + let str = "Index"; + //console.log(this.state.registerNumber); + index = ; + index = {index} + } + + return ( +
{ this.divElement = divElement } }> + + + + + + + + + + this.menuItemSelected('settings')}>Nastavenia + Logout + + + + + {this.props.title} + + + + + + + + <> + + + Read/Write + + + + Recip. + + + + Task + + + + + Request to: {this.state.requestToHost}
response from: {this.state.responsefromHost} +
+ + {bytes} + + {index} + + + {address} + +
+ +
+ + + + + +
+
+ ); + } +} + +const mapDispatchToProps = dispatch => { + return { + addCommand: (params) => dispatch ({ + type: actionTypes.ADD_COMMAND, + payload: { + command: params.command + } + }), + clearState: (params) => dispatch ({ + type: actionTypes.CLEAR_STATE, + }) + } + } + + + export default connect(null, mapDispatchToProps)(Controls); \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..7bc4e03 --- /dev/null +++ b/src/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +import { createStore } from 'redux'; +import { Provider } from 'react-redux' +import reducer from './store/reducer' +const store = createStore(reducer); + +ReactDOM.render( + + + , + document.getElementById('root') +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/output.js b/src/output.js new file mode 100644 index 0000000..130054e --- /dev/null +++ b/src/output.js @@ -0,0 +1,108 @@ +import React, {Component} from 'react' +import {connect} from 'react-redux' +import { AnimatedList } from 'react-animated-list'; +import * as actionTypes from './store/actions' + +import moment from 'moment'; + +class Output extends Component { + + constructor(props) { + super(props); + this.state = { commands: [] } + + this.messagesEndRef = React.createRef(); + } + + componentDidMount () { + this.scrollToBottom() + } + componentDidUpdate () { + this.scrollToBottom() + } + scrollToBottom = () => { + this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + + render() { + + let index = 0; + let responsefromHostWasShow = true;//obsolete + const items = this.props.commands.map(item => { + + let dateString = ""; + if("date" in item) + { + if(item.date != null) dateString = "[" + moment(item.date).format('DD.MM.YYYY hh.mm.ss') + "] "; + } + + if(item.direction === "in") + { + const container =
[{dateString}] COMMAND: {item.registerName}
; + + index++; + + return container; + } + else if (item.direction === "out") + { + //RESPONSE, ERROR, EVENT, UNKNOWN + + let color = '#95FE35';//RESPONSE + if(item.type === "ERROR") color = 'red'; + + let responsefromHost; + if(!responsefromHostWasShow) + { + responsefromHost = (
RESPONSE FROM HOST: {item.responsefromHost}
); + responsefromHostWasShow = true; + } + + const container = ( + <> + {responsefromHost} +
+ {dateString} + RESPONSE: {item.message} +
+ + ); + + index++; + + return container; + } + }) + + return ( + <> + + {items} + +
+ + ); + } +} + +const mapDispatchToProps = dispatch => { + return { + addCommand: (params) => dispatch ({ + type: actionTypes.ADD_COMMAND, + payload: { + command: params.command + } + }), + clearState: (params) => dispatch ({ + type: actionTypes.CLEAR_STATE, + }) + } + } + +const mapStateToProps = state => { + return{ + commands: state.commands, + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Output); \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/src/store/actions.js b/src/store/actions.js new file mode 100644 index 0000000..1703e55 --- /dev/null +++ b/src/store/actions.js @@ -0,0 +1,2 @@ +export const ADD_COMMAND = 'ADD_COMMAND'; +export const CLEAR_STATE = 'CLEAR_STATE'; \ No newline at end of file diff --git a/src/store/reducer.js b/src/store/reducer.js new file mode 100644 index 0000000..1560140 --- /dev/null +++ b/src/store/reducer.js @@ -0,0 +1,23 @@ +import * as actionTypes from './actions'; + +const initialState = { + commands: [], +} + +const reducer = (state = initialState, action) =>{ + //console.log(action.type, action.payload ); + if(action.type === actionTypes.ADD_COMMAND){ + return { + ...state, + commands: state.commands.concat(action.payload.command) + } + } + + if(action.type === actionTypes.CLEAR_STATE){ + return {commands: []}; + } + + return state; +}; + +export default reducer; \ No newline at end of file diff --git a/src/util/db/SqlQueryBuilder.js b/src/util/db/SqlQueryBuilder.js new file mode 100644 index 0000000..88e4748 --- /dev/null +++ b/src/util/db/SqlQueryBuilder.js @@ -0,0 +1,248 @@ + +export const LogicOperator = { + UndefOperator: '', + And: "AND", + Or: "OR" +} + +export const Function = { + UndefFn: 'UndefFn', + Min: "Min", + Max: "Max", + Avg: "Avg" +} + +class SqlQueryBuilder{ + + className = "SqlQueryBuilder"; + type = "SELECT"; + + queryData = { + columns: [], + tables: [], + wheres: [], + variables: {} + }; + + select(column, alias, fn) + { + /* + SqlQueryData::Column col; + col.column = column; + col.alias = alias; + col.func = fn; + + queryData.columns.append(col); + return this; + */ + + if(Array.isArray(column)) + { + for(let i = 0; i < column.length; i++) + { + let c = column[i]; + + let col; + let alias; + let fn; + + if(typeof c == "string") col = c; + if(col === undefined) continue; + + this.select(col, alias, fn); + } + + return; + } + + const SqlQueryData_Column = { + column: column, + alias: alias, + func: fn + }; + + this.queryData.columns.push(SqlQueryData_Column); + + return this; + + } + + from(table, alias) + { + + const SqlQueryData_Table = { + table: table, + alias: alias + }; + + this.queryData.tables.push(SqlQueryData_Table); + + return this; + } + + where(cond, op = LogicOperator.And) + { + //queryBuilderTmp->where("`date` >= :date1", SqlQueryBuilder::And); + if(LogicOperator === undefined) op = LogicOperator.And; + + const SqlQueryData_Where = { + cond: cond, + operation: op + }; + + this.queryData.wheres.push(SqlQueryData_Where); + } + + setParameter(key, value) + { + this.queryData.variables[key] = value; + } + + getSql() + { + let sql = this.type + " "; + + if(this.queryData.tables.length === 0) + { + throw "SqlQueryBuilder::queryData.tables.length === 0"; + } + + // blok stlpcov + if(this.type === "SELECT") + { + if(this.queryData.columns.length === 0) sql = sql + "*"; + } + + let params = []; + for(let i = 0; i < this.queryData.columns.length; i++) + { + let col = this.queryData.columns[i]; + + let column = col.column; + + params.push("`" + column + "`"); + } + + sql = sql + params.join(','); + + sql = sql + " FROM "; + + params = []; + for(let i = 0; i < this.queryData.tables.length; i++) + { + let table = this.queryData.tables[i]; + + params.push("`" + table.table + "`"); + } + + sql = sql + params.join(','); + + //WHERE + if(this.queryData.wheres.length > 0) + { + + let size = this.queryData.wheres.length; + + sql = sql + " WHERE "; + + for(let i = 0; i < size; i++) + { + let where = this.queryData.wheres[i]; + sql = sql + where.cond; + + if (where.operation !== LogicOperator.UndefOperator) + { + if(i < (size - 1)) + { + if (where.operation === LogicOperator.And) + { + sql = sql + " AND "; + } + else if (where.operation === LogicOperator.Or) + { + sql = sql + " OR "; + } + } + } + + } + } + + return sql; + } + + serializeToJson() + { + return JSON.stringify({ + className: "SqlQueryBuilder", + type: this.type, + queryData: this.queryData + }); + } + + //SqlQueryBuilder* select(const QString &column, const QString &alias = QString(), Function fn = UndefFn); + //SqlQueryBuilder* from(const QString &table, const QString &alias = QString()); + //SqlQueryBuilder* where(const QString &cond, LogicOperator op = UndefOperator); + //SqlQueryBuilder* join(const QString &joinWhat, const QString &cond = QString(), JoinType type = LeftJoin); + //SqlQueryBuilder* orderBy(const QString &name); + //SqlQueryBuilder* groupBy(const QString &name); + //SqlQueryBuilder* limit(int count); + //SqlQueryBuilder* limit(int from, int to); + //SqlQueryBuilder* ascend(Ascend asc = Ascending); + //SqlQueryBuilder* setParameters(const QMap ¶ms); + //void clearParameters(); + //QMap getParameters(); + //SqlQueryBuilder* setParameter(const QString &key, const QVariant &value); + //SqlQueryBuilder* removeParameter(const QString &key); + //void clear(); + //void clear(SqlPart); + //QString getSql(bool showError = true) const; + + /** + * \brief Query data storage, + */ + + /* + struct SqlQueryData + { + SqlQueryData() = default; + SqlQueryData(const SqlQueryData& other) + { + hasLimit = other.hasLimit; + asc = other.asc; + columns = other.columns; + tables = other.tables; + wheres = other.wheres; + joins = other.joins; + orderByes = other.orderByes; + groupByes = other.groupByes; + connectionName = other.connectionName; + variables = other.variables; + } + + struct Column { QString column, alias; SqlQueryBuilder::Function func; }; + struct Table { QString table, alias; }; + struct Join { SqlQueryBuilder::JoinType type; QString cond, what; }; + struct Where { QString cond; SqlQueryBuilder::LogicOperator operation; }; + + struct Limit { bool range; int from, to, count; } limit; + bool hasLimit = false; + + SqlQueryBuilder::Ascend asc; + + QList columns; + QList tables; + QList wheres; + QList joins; + QStringList orderByes; + QStringList groupByes; + + QString connectionName = ""; + + QMap variables; + } queryData; + */ + +} + +export default SqlQueryBuilder; \ No newline at end of file diff --git a/src/util/db/db.js b/src/util/db/db.js new file mode 100644 index 0000000..cbb66fe --- /dev/null +++ b/src/util/db/db.js @@ -0,0 +1,300 @@ +import axios from 'axios' +//import moment from 'moment' + +class DB{ + static token = ""; + static company = ""; + static connectionName = ""; + static db_structure = ""; + static backendUrl = '/backend/db_connector.php'; + + data = []; + transaction = false; + + updateJsonTypes(arr, table) + { + let table_structure; + if(DB.db_structure !== undefined) + { + table_structure = DB.db_structure[table]; + } + + let result = []; + let index = 0; + for(let i = 0; i < arr.length; ++i) + { + let record = arr[i]; + + + if(table_structure !== undefined) + { + let keys = Object.keys(record); + for(let ii = 0; ii < keys.length; ii++) + { + let key = keys[ii]; + if(table_structure[key] === undefined) + { + console.log("updateJsonTypes - undefined key", table, key); + continue; + } + + if(table_structure[key]["type"] === "uint" || table_structure[key]["type"] === "int") + { + record[key] = parseInt(record[key]); + } + else if(table_structure[key]["type"] === "double") + { + record[key] = parseFloat(record[key]); + } + else if(table_structure[key]["type"] === "QDateTime") + { + + } + else if(table_structure[key]["type"] === "char") + { + if(table_structure[key]["length"] === 1) + { + //boolean + let value = parseInt(record[key]); + //TODO use translation + if(value === 0) record[key] = "nie"; + else record[key] = "áno"; + } + } + else if(table_structure[key]["type"] === "QDate") + { + let d = new Date(record[key]); + record["_" + key] = record[key];//to preserve original value + //record[key] = moment(d).format('DD.MM.YYYY'); + } + + } + } + else + { + console.log("no table structure provided"); + } + + record._index = index; + result.push(record); + + index++ + } + + return arr; + } + + dbSelect(data, exec = false, error_message = "") + { + // + //let obj = {type: "select", error_message: error_message}; + //this.data.push(obj); + + //data can be string, or object SqlQueryBuilder!!! + //TODO push + + //or sql as string + if((typeof data) == "object") + { + if(data.className === "SqlQueryBuilder") + { + //if(error_message === undefined) error_message = "update pre tabulku " + table + " zlyhal"; + + //run later + if(exec === false) + { + let obj = {type: "select", className: "SqlQueryBuilder", obj: data, error_message: error_message}; + this.data.push(obj); + + return; + } + } + } + + if((typeof data) == "string") + { + //alert("str"); + } + + return new Promise((resolve, reject) => { + let obj = { + token: DB.token, + company: DB.company, + connectionName: DB.connectionName, + type: "select", + data: data, + error_message: error_message + } + + let json = JSON.stringify(obj); + + if(DB.backendUrl === undefined || DB.backendUrl === "") + { + reject("Nie je definovaný endpoint"); + } + + axios(DB.backendUrl, + { + method: 'POST', + mode: 'no-cors', + data: json, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }, + withCredentials: false, + credentials: 'same-origin', + }) + .then(response => { + + console.log(response); + + let success = response.data.success; + + if(!success) + { + //console.log(response.data); + let message = response.data.message; + if(message === undefined) message = "nedefinovaná chyba"; + + throw message; + } + + if(success) + { + resolve( response.data.result ); + } + + }) + .catch(error => { + const message = error + ""; + + reject(message); + }); + + }) + + } + + dbStartTransaction() + { + let obj = {type: "START TRANSACTION"}; + this.data.push(obj); + } + + dbCommitTransaction() + { + let obj = {type: "COMMIT"}; + this.data.push(obj); + } + + dbDelete(table, conditions, error_message) + { + + if(Number.isInteger(conditions)) + { + conditions = {"id": conditions}; + } + + if(error_message === undefined) error_message = "zlyhalo zmazanie z tabulky " + table; + let obj = {type: "delete", table: table, conditions: conditions, error_message: error_message}; + this.data.push(obj); + } + + dbInsert(data, table, foreign_keys, error_message) { + //console.log(data, table, condition); + +//foreign key: table: lastInsertedId +//foreign_keys: {"subject_id": {"subject": "id"}} + if(foreign_keys === undefined) foreign_keys = {}; + + //token + if(error_message === undefined) error_message = "insert pre tabulku " + table + " zlyhal"; + let obj = {type: "insert", foreign_keys: foreign_keys, table: table, params: {...data}, error_message: error_message}; + this.data.push(obj); + } + + dbUpdate(data, table, condition, error_message) { + //console.log(data, table, condition); + + //token + if(error_message === undefined) error_message = "update pre tabulku " + table + " zlyhal"; + let obj = {type: "update", table: table, params: {...data}, condition: condition, error_message: error_message}; + this.data.push(obj); + } + + exec() + { + //build json and send to server + + return new Promise((resolve, reject) => { + + let obj = { + token: DB.token, + company: DB.company, + connectionName: DB.connectionName, + data: this.data + } + + let json = JSON.stringify(obj); + + if(DB.backendUrl === undefined) + { + reject("Nie je definovaný endpoint"); + } + + axios(DB.backendUrl, + { + method: 'POST', + mode: 'no-cors', + data: json, + headers: { + 'Access-Control-Allow-Origin': '*', + "Content-Type": "text/html;charset=UTF-8" + }, + withCredentials: false, + credentials: 'same-origin', + }) + .then(response => { + + //console.log(response); + + if(!response.data.sucess) + { + //alert(response.data.error_message); + console.log(response); + + let errors = []; + if("data" in response) + { + if(Array.isArray(response.data.result)) + { + for(let i = 0; i < response.data.result.length; ++i) + { + if(!response.data.result[i].sucess) errors.push(response.data.result[i].error_message); + } + } + } + + throw errors; + } + + if(response.data.sucess) + { + resolve(response.data.result); + } + + }) + .catch(error => { + const message = error + ""; + + console.log("catsh", message, error); + + reject(message); + }); + + }) + } +} + +export default DB; \ No newline at end of file diff --git a/src/util/findGetParameterInUrl.js b/src/util/findGetParameterInUrl.js new file mode 100644 index 0000000..d900f7f --- /dev/null +++ b/src/util/findGetParameterInUrl.js @@ -0,0 +1,14 @@ +function findGetParameterInUrl(parameterName) { + var result = null, + tmp = []; + window.location.search + .substr(1) + .split("&") + .forEach(function (item) { + tmp = item.split("="); + if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]); + }); + return result; + } + +export default findGetParameterInUrl \ No newline at end of file diff --git a/src/util/isLocalhost.js b/src/util/isLocalhost.js new file mode 100644 index 0000000..e891e4d --- /dev/null +++ b/src/util/isLocalhost.js @@ -0,0 +1,11 @@ +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) + ); + +export default isLocalhost; \ No newline at end of file diff --git a/src/util/isMobile.js b/src/util/isMobile.js new file mode 100644 index 0000000..19a283a --- /dev/null +++ b/src/util/isMobile.js @@ -0,0 +1,15 @@ +function isMobile() +{ + var isMobile = false; //initiate as false + // device detection + if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) + || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) { + isMobile = true; + + return isMobile; + } + + return isMobile; +} + +export default isMobile; \ No newline at end of file