diff --git a/README.md b/README.md index 16b40bbf28344c5d2250a18237ec93bc06530467..788708185e5bfb8b33fb67956cd79dc8a936fc14 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,18 @@ # README # -NRP web-frontend 2.0 using React +NRP web-frontend 4.0 using React +### Prerequisites + +- "nvm install 14" + +### Install + +- "nvm use 14" +- "npm install" ### Commands +- "nvm use 14" - "npm start" (dev server) - "npm run build" (build for production) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e9b37333452f49bd17ee3b46dec3ecabf28d71f4..4c67903c555a9eafa5ae8f01b0cd24e5db610256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1264,6 +1264,87 @@ "minimist": "^1.2.0" } }, + "@codemirror/autocomplete": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.1.0.tgz", + "integrity": "sha512-wtO4O5WDyXhhCd4q4utDIDZxnQfmJ++3dGBCG9LMtI79+92OcA1DVk/n7BEupKmjIr8AzvptDz7YQ9ud6OkU+A==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.0.tgz", + "integrity": "sha512-qCj2YqmbBjj0P1iumnlL5lBqZvJPzT+t2UvgjcaXErp5ZvMqFRVgQyrEfdXX6SX5UcvcHKBjXqno+MkUp0aYvQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.2.1.tgz", + "integrity": "sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.0.0.tgz", + "integrity": "sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.1.0.tgz", + "integrity": "sha512-ye6m0jFHSgQ4qnfWVwArvm7XrCMNppMYnL5f4M0WdBScslnckomf5eVacYCw8P0UBWeq72lCSXA0/eo1piZxLA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.1.tgz", + "integrity": "sha512-2s+aXsxmAwnR3Rd+JDHPG/1lw0YsA9PEwl7Re88gHJHGfxyfEzKBmsN4rr53RyPIR4lzbbhJX0DCq0WlqlBIRw==" + }, + "@codemirror/theme-one-dark": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.0.0.tgz", + "integrity": "sha512-jTCfi1I8QT++3m21Ui6sU8qwu3F/hLv161KLxfvkV1cYWSBwyUanmQFs89ChobQjBHi2x7s2k71wF9WYvE8fdw==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@codemirror/view": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.2.0.tgz", + "integrity": "sha512-3emW1symh+GoteFMBPsltjmF790U/trouLILATh3JodbF/z98HvcQh2g3+H6dfNIHx16uNonsAF4mNzVr1TJNA==", + "requires": { + "@codemirror/state": "^6.0.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, "@csstools/convert-colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", @@ -1883,6 +1964,27 @@ } } }, + "@lezer/common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz", + "integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==" + }, + "@lezer/highlight": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz", + "integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.3.tgz", + "integrity": "sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@material-ui/core": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz", @@ -2980,6 +3082,41 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@uiw/codemirror-extensions-basic-setup": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.11.5.tgz", + "integrity": "sha512-aHtdF1JEzHmBVuWXemr8OH7SQP/LbXXZdiOo/4tcxjFpyTuVGzPteBdfQU0xPOk0m+5Oc1LPqM+HaNPXNzX6aA==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "@uiw/react-codemirror": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.11.5.tgz", + "integrity": "sha512-Bf8l3nVV4ekHbv4U0VrzUibl8+ucAY3UV0gk0xckbFnV1AlUxHcrYFiXSgy/rkyWBD7enHQENtM888B/3qBiwg==", + "requires": { + "@babel/runtime": "^7.18.6", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.11.5", + "codemirror": "^6.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -4650,6 +4787,20 @@ "q": "^1.1.2" } }, + "codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -4984,6 +5135,11 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "cross-fetch": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz", @@ -7548,6 +7704,11 @@ "slash": "^3.0.0" } }, + "google-protobuf": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.0.tgz", + "integrity": "sha512-byR7MBTK4tZ5PZEb+u5ZTzpt4SfrTxv5682MjPlHN16XeqgZE2/8HOIWeiXe8JKnT9OVbtBGhbq8mtvkK8cd5g==" + }, "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", @@ -15684,6 +15845,11 @@ "schema-utils": "^2.7.0" } }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "stylehacks": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", @@ -16575,6 +16741,11 @@ "browser-process-hrtime": "^1.0.0" } }, + "w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + }, "w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", diff --git a/package.json b/package.json index c992410e816558c807d4fb1748a5f1f9c65d4960..459c389fb63fafc700fc14f18e5056eb120b3a31 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "bootstrap": "4.5", "flexlayout-react": "0.5.5", + "google-protobuf": "3.21.0", "jquery": "3.6.0", "jszip": "3.2.0", "mqtt": "4.3.5", @@ -24,6 +25,7 @@ "protobufjs": "6.11.2", "react": "^17.0.1", "react-bootstrap": "1.4.0", + "@uiw/react-codemirror": "4.11.5", "react-dom": "^17.0.1", "react-icons": "4.1.0", "react-router-dom": "5.2.0", diff --git a/src/components/entry-page/entry-page.js b/src/components/entry-page/entry-page.js index 038e1177f83dbc27e5350d94e3ed4837b2158f74..435685932b60b4e86da4b4aa7c0aacd9c83836d1 100644 --- a/src/components/entry-page/entry-page.js +++ b/src/components/entry-page/entry-page.js @@ -3,8 +3,8 @@ import React from 'react'; import NrpHeader from '../nrp-header/nrp-header.js'; import './entry-page.css'; -import PlaceholderImage from '../../assets/images/Artificial_Intelligence_2.jpg'; import NrpCoreDashboard from '../nrp-core-dashboard/nrp-core-dashboard.js'; +//import TransceiverFunctionEditor from '../tf-editor/tf-editor'; export default class EntryPage extends React.Component { render() { @@ -33,6 +33,7 @@ export default class EntryPage extends React.Component { <div><b>!!! NRP Core testing !!!</b></div> </div> <NrpCoreDashboard /> + {/*<TransceiverFunctionEditor experimentId='mqtt_simple_1'/>*/} </div> ); } diff --git a/src/components/experiment-overview/experiment-overview.js b/src/components/experiment-overview/experiment-overview.js index 9f367bf2fca7a9148be051e61e2fe3392154b4c3..b43fb7a9d504792832640f57a7b35acfa5de78c6 100644 --- a/src/components/experiment-overview/experiment-overview.js +++ b/src/components/experiment-overview/experiment-overview.js @@ -106,7 +106,7 @@ export default class ExperimentOverview extends React.Component { onUpdatePublicExperiments(publicExperiments) { this.setState({ - publicExperiments: publicExperiments.filter(exp => exp.configuration.maturity === 'production') + publicExperiments: publicExperiments //.filter(exp => exp.configuration.maturity === 'production') }); } diff --git a/src/components/nrp-core-dashboard/nrp-core-dashboard.js b/src/components/nrp-core-dashboard/nrp-core-dashboard.js index 49aa07eed0ffc9a74bbbc956207eecd3b2d4a9cc..88489d1d385635993ec40d279564071663286227 100644 --- a/src/components/nrp-core-dashboard/nrp-core-dashboard.js +++ b/src/components/nrp-core-dashboard/nrp-core-dashboard.js @@ -15,8 +15,8 @@ export default class NrpCoreDashboard extends React.Component { MqttClientService.instance.connect(this.mqttBrokerUrl); } - onMqttClientConnected(mqttClient) { - mqttClient.subscribe('#', (err) => { + onMqttClientConnected(MqttClient) { + MqttClient.subscribe('#', (err) => { if (err) { console.error(err); } diff --git a/src/components/tf-editor/tf-editor.css b/src/components/tf-editor/tf-editor.css new file mode 100644 index 0000000000000000000000000000000000000000..8f9261144e0954c79323e82a01cd828764a2abe6 --- /dev/null +++ b/src/components/tf-editor/tf-editor.css @@ -0,0 +1,44 @@ +.tf-editor-header { + display: flex; + flex-direction: row; + align-items: center; +} + +.tf-editor-icon { + width: 40px; + height: 40px; + background-color: yellow; + text-align: center; + border: 1px solid black; + margin: 5px; + font-size: 1.5em; +} + +.tf-editor-file-ui { + padding: 10px; + display: flex; + flex-direction: row; + align-items: center; +} + +.tf-editor-file-ui-item { + margin: 5px; +} + +.tf-editor-ui-save { + display: flex; + flex-direction: row; + align-items: center; +} + +.tf-editor-text-saved { + color: green; +} + +.tf-editor-text-unsaved { + color: orange; +} + +.tf-editor-codemirror-container { + overflow: scroll; +} \ No newline at end of file diff --git a/src/components/tf-editor/tf-editor.js b/src/components/tf-editor/tf-editor.js new file mode 100644 index 0000000000000000000000000000000000000000..126ba788a2c147eba4dda1f982b59ad003d50462 --- /dev/null +++ b/src/components/tf-editor/tf-editor.js @@ -0,0 +1,143 @@ +import React from 'react'; +import CodeMirror from '@uiw/react-codemirror'; +import { Modal, Button } from 'react-bootstrap'; + +import ExperimentStorageService from '../../services/experiments/files/experiment-storage-service'; + + +import './tf-editor.css'; + +export default class TransceiverFunctionEditor extends React.Component { + + constructor(props) { + super(props); + + this.testListTfFiles = ['cg_mqtt.py', 'cg_mqtt_2.py', 'cg_mqtt_3.py']; + this.state = { + selectedFilename: this.testListTfFiles[0], + code: '', + textChanges: '', + showDialogUnsavedChanges: false + }; + } + + async componentDidMount() { + this.loadFileContent(this.state.selectedFilename); + } + + onChangeSelectedFile(event) { + let filename = event.target.value; + if (this.hasUnsavedChanges) { + this.pendingFileChange = { + newFilename: filename, + oldFilename: this.state.selectedFilename + }; + this.setState({showDialogUnsavedChanges: true}); + } + else { + this.loadFileContent(filename); + } + } + + onUnsavedChangesDiscard() { + this.loadFileContent(this.pendingFileChange.newFilename); + } + + async onUnsavedChangesSave() { + let success = await this.saveTF(); + if (success) { + this.loadFileContent(this.pendingFileChange.newFilename); + } + } + + async loadFileContent(filename) { + let fileContent = await ExperimentStorageService.instance.getFileText(this.props.experimentId, filename); + this.fileLoading = true; + this.setState({selectedFilename: filename, code: fileContent, showDialogUnsavedChanges: false}); + } + + onChangeCodemirror(change, viewUpdate) { + this.setState({code: change}); + this.hasUnsavedChanges = !this.fileLoading; + this.fileLoading = false; + if (this.hasUnsavedChanges) { + this.setState({textChanges: 'unsaved changes'}); + } + } + + async saveTF() { + let response = await ExperimentStorageService.instance.setFile( + this.props.experimentId, this.state.selectedFilename, this.state.code); + if (response.ok) { + this.hasUnsavedChanges = false; + this.setState({textChanges: 'saved'}); + setTimeout(() => { + this.setState({textChanges: ''}); + }, 3000); + return true; + } + else { + console.error('Error trying to save TF!'); + console.error(response); + return false; + } + } + + render() { + return ( + <div className='tf-editor-container'> + <div className='tf-editor-header'> + <div className='tf-editor-icon'>TF</div> + <div className='tf-editor-file-ui'> + <select + className='tf-editor-file-ui-item' + name="selectTFFile" + value={this.state.selectedFilename} + onChange={(event) => this.onChangeSelectedFile(event)}> + {this.testListTfFiles.map(file => { + return (<option key={file} value={file}>{file}</option>); + })} + </select> + <button className='tf-editor-file-ui-item' onClick={() => this.saveTF()}>Save</button> + <div className={this.hasUnsavedChanges ? + 'tf-editor-text-unsaved' : 'tf-editor-text-saved'}> + {this.state.textChanges} + </div> + </div> + </div> + + <div className='tf-editor-codemirror-container'> + <CodeMirror + value={this.state.code} + onChange={(change, viewUpdate) => this.onChangeCodemirror(change, viewUpdate)}/> + </div> + + {this.state.showDialogUnsavedChanges ? + <div> + <Modal show={this.state.showDialogUnsavedChanges} + onHide={() => this.setState({showDialogUnsavedChanges: false})}> + <Modal.Header> + <Modal.Title>Unsaved Changes</Modal.Title> + </Modal.Header> + <Modal.Body>You have unsaved changes for "{this.pendingFileChange.oldFilename}". + What would you like to do?</Modal.Body> + <Modal.Footer> + <div> + <Button variant="danger" onClick={() => this.setState({showDialogUnsavedChanges: false})}> + Cancel + </Button> + <Button variant="danger" onClick={() => this.onUnsavedChangesDiscard()}> + Discard changes + </Button> + <Button variant="light" onClick={() => this.onUnsavedChangesSave()}> + Save + </Button> + </div> + </Modal.Footer> + </Modal> + </div> + : null} + </div> + ); + } +} diff --git a/src/services/__tests__/mqtt-client-service.test.js b/src/services/__tests__/mqtt-client-service.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3f384cdd630949251c30d98e6a799b51229e2516 --- /dev/null +++ b/src/services/__tests__/mqtt-client-service.test.js @@ -0,0 +1,72 @@ +/** + * @jest-environment jsdom +*/ +import '@testing-library/jest-dom'; + +import MqttClientService from '../mqtt-client-service'; + +let subscribeTopicAndValidate = (topic, callback) => { + let token = MqttClientService.instance.subscribeToTopic(topic, callback); + expect(token).toBeDefined(); + expect(token.topic).toBe(topic); + expect(token.callback).toBe(callback); + expect(MqttClientService.instance.subTokensMap.get(topic).includes(token)).toBeTruthy(); + + return token; +}; + +let unsubscribeAndValidate = (token) => { + MqttClientService.instance.unsubscribe(token); + expect(MqttClientService.instance.subTokensMap.get(token.topic).includes(token)).toBeFalsy(); +}; + +test('sub/unsub', async () => { + let topicA = 'topic/A'; + let topicB = 'topic/B'; + + let sub1Callback = jest.fn(); + let sub1Token = subscribeTopicAndValidate(topicA, sub1Callback); + let sub2Callback = jest.fn(); + let sub2Token = subscribeTopicAndValidate(topicA, sub2Callback); + let sub3Callback = jest.fn(); + let sub3Token = subscribeTopicAndValidate(topicB, sub3Callback); + + expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(2); + expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(1); + + MqttClientService.instance.onMessage(topicA, {}); + MqttClientService.instance.onMessage(topicB, {}); + expect(sub1Token.callback).toHaveBeenCalledTimes(1); + expect(sub2Token.callback).toHaveBeenCalledTimes(1); + expect(sub3Token.callback).toHaveBeenCalledTimes(1); + + unsubscribeAndValidate(sub1Token); + expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(1); + expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(1); + + MqttClientService.instance.onMessage(topicA, {}); + MqttClientService.instance.onMessage(topicB, {}); + expect(sub1Token.callback).toHaveBeenCalledTimes(1); + expect(sub2Token.callback).toHaveBeenCalledTimes(2); + expect(sub3Token.callback).toHaveBeenCalledTimes(2); + + unsubscribeAndValidate(sub2Token); + expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(0); + expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(1); + + MqttClientService.instance.onMessage(topicA, {}); + MqttClientService.instance.onMessage(topicB, {}); + expect(sub1Token.callback).toHaveBeenCalledTimes(1); + expect(sub2Token.callback).toHaveBeenCalledTimes(2); + expect(sub3Token.callback).toHaveBeenCalledTimes(3); + + unsubscribeAndValidate(sub3Token); + expect(MqttClientService.instance.subTokensMap.get(topicA).length).toBe(0); + expect(MqttClientService.instance.subTokensMap.get(topicB).length).toBe(0); + + MqttClientService.instance.onMessage(topicA, {}); + MqttClientService.instance.onMessage(topicB, {}); + expect(sub1Token.callback).toHaveBeenCalledTimes(1); + expect(sub2Token.callback).toHaveBeenCalledTimes(2); + expect(sub3Token.callback).toHaveBeenCalledTimes(3); +}); diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js index baa4d79656644eb5abde013a80d36f8f9f5e69c2..36942d6c996b583b2dbb7d4d59b2cfd4aaea494f 100644 --- a/src/services/experiments/files/experiment-storage-service.js +++ b/src/services/experiments/files/experiment-storage-service.js @@ -191,6 +191,19 @@ class ExperimentStorageService extends HttpService { } + /** + * Gets a file from the storage as text. + * @param {string} experimentName - name of the experiment + * @param {string} filename - name of the file + * @param {Boolean} byName - whether to check for the file by name or not (default TRUE) + * + * @returns {Blob} the contents of the file as text + */ + async getFileText(experimentName, filename, byName = true) { + return await (await this.getBlob(experimentName, filename, byName)).text(); + } + + /** * Deletes an experiment entity (folder or file) from the storage. * Called by other functions, not to be called independently. diff --git a/src/services/experiments/files/public-experiments-service.js b/src/services/experiments/files/public-experiments-service.js index 86b3eb167f7fccb3825c69d05853a338d32dc5e1..88e10eaaa525decfdb7b6bdb0039d54bd51d2dc2 100644 --- a/src/services/experiments/files/public-experiments-service.js +++ b/src/services/experiments/files/public-experiments-service.js @@ -86,8 +86,8 @@ class PublicExperimentsService extends HttpService { sortExperiments(experimentList) { experimentList = experimentList.sort( (a, b) => { - let nameA = a.configuration.name.toLowerCase(); - let nameB = b.configuration.name.toLowerCase(); + let nameA = a.configuration.SimulationName.toLowerCase(); + let nameB = b.configuration.SimulationName.toLowerCase(); if (nameA < nameB) { return -1; } diff --git a/src/services/mqtt-client-service.js b/src/services/mqtt-client-service.js index ce09aa079385307634bda4ffd27c9d99c588cdaf..06b2193c3180dc803e3180d2c36bb18865db867c 100644 --- a/src/services/mqtt-client-service.js +++ b/src/services/mqtt-client-service.js @@ -1,7 +1,8 @@ import mqtt from 'mqtt'; import { EventEmitter } from 'events'; -//import * as proto from 'nrp-jsproto/engine_grpc_pb'; +//import { DataPackMessage } from 'nrp-jsproto/engine_grpc_pb'; +import jspb from '../../node_modules/google-protobuf/google-protobuf'; let _instance = null; const SINGLETON_ENFORCER = Symbol(); @@ -16,7 +17,7 @@ export default class MqttClientService extends EventEmitter { throw new Error('Use ' + this.constructor.name + '.instance'); } - //console.info(proto); + this.subTokensMap = new Map(); } static get instance() { @@ -36,7 +37,9 @@ export default class MqttClientService extends EventEmitter { this.emit(MqttClientService.EVENTS.CONNECTED, this.client); }); this.client.on('error', this.onError); - this.client.on('message', this.onMessage); + this.client.on('message', (params) => { + this.onMessage(params); + }); } onError(error) { @@ -44,13 +47,90 @@ export default class MqttClientService extends EventEmitter { } onMessage(topic, payload, packet) { - console.info('MQTT message: [topic, payload, packet]'); - console.info([topic, payload, packet]); + if (typeof payload === 'undefined') { + return; + } + + //console.info('MQTT message: [topic, payload, packet]'); + //console.info([topic, payload, packet]); + //Now we see which callbacks have been assigned for a topic + let subTokens = this.subTokensMap.get(topic); + if (typeof subTokens !== 'undefined') { + for (var token of subTokens) { + //Deserializatin of Data must happen here + token.callback(payload); + }; + }; + + /*try { + if (topic.endsWith('/type')) { + let msg = String(payload); + console.info('"' + topic + '" message format = ' + msg); + } + else { + let msg = DataPackMessage.deserializeBinary(payload); + console.info('DataPackMessage'); + console.info(msg); + } + } + catch (error) { + console.error(error); + }*/ + } + + //callback should have args topic, payload + subscribeToTopic(topic, callback) { + if (typeof callback !== 'function') { + console.error('trying to subscribe to topic "' + topic + '", but no callback function given!'); + return; + } + + const token = { + topic: topic, + callback: callback + }; + if (this.subTokensMap.has(token.topic)){ + this.subTokensMap.get(token.topic).push(token); + } + else{ + this.subTokensMap.set( + token.topic, + [token] + ); + } + //console.info('You have been subscribed to topic ' + topic); + //console.info(this.subTokensMap); + return token; + } - // step 0: deserialize the payload => messageData - // step 1: pick subs based on topic => subs - // step 2: foreach(sub) {sub.callback(messageData, topic);} + unsubscribe(unsubToken) { + if (this.subTokensMap.has(unsubToken.topic)){ + let tokens = this.subTokensMap.get(unsubToken.topic); + let index = tokens.indexOf(unsubToken); + if (index !== -1) { + tokens.splice(index, 1); + //console.info('You have been unsubscribed from topic ' + unsubToken.topic); + } + else { + console.warn('Your provided token could not be found in the subscription list'); + } + } + else{ + console.warn('The topic ' + unsubToken.topic + ' was not found'); + } } + + static getProtoOneofData(protoMsg, oneofCaseNumber) { + return jspb.Message.getField(protoMsg, oneofCaseNumber); + } + + /*static getDataPackMessageOneofCaseString(protoMsg) { + for (let dataCase in DataPackMessage.DataCase) { + if (DataPackMessage.DataCase[dataCase] === protoMsg.getDataCase()) { + return dataCase; + } + } + }*/ } MqttClientService.EVENTS = Object.freeze({