├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── _config.yml ├── deviceModel.json ├── package-lock.json ├── package.json ├── src ├── client │ ├── app.scss │ ├── app.tsx │ ├── const.scss │ ├── context │ │ ├── appContext.tsx │ │ ├── controlContext.tsx │ │ ├── deviceContext.tsx │ │ ├── endpoint.tsx │ │ └── statsContext.tsx │ ├── dashboard │ │ ├── dashboard.scss │ │ ├── dashboard.tsx │ │ ├── grid.tsx │ │ └── stats.tsx │ ├── device │ │ ├── device.scss │ │ └── device.tsx │ ├── deviceCommands │ │ ├── deviceCommands.scss │ │ └── deviceCommands.tsx │ ├── deviceEdge │ │ ├── deviceEdge.scss │ │ ├── deviceEdge.tsx │ │ └── deviceEdgeChildren.tsx │ ├── deviceFields │ │ ├── deviceFieldC2D.tsx │ │ ├── deviceFieldD2C.tsx │ │ ├── deviceFieldMethod.tsx │ │ └── deviceFields.scss │ ├── deviceOptions │ │ ├── deviceOptions.scss │ │ └── deviceOptions.tsx │ ├── devicePlan │ │ ├── devicePlan.scss │ │ └── devicePlan.tsx │ ├── devicePower │ │ ├── devicePower.scss │ │ └── devicePower.tsx │ ├── deviceTitle │ │ ├── deviceTitle.scss │ │ └── deviceTitle.tsx │ ├── devices │ │ ├── devices.scss │ │ └── devices.tsx │ ├── modals │ │ ├── addDevice.scss │ │ ├── addDevice.tsx │ │ ├── bulk.scss │ │ ├── bulk.tsx │ │ ├── connect.scss │ │ ├── connect.tsx │ │ ├── consoleInspector.scss │ │ ├── consoleInspector.tsx │ │ ├── edgeModule.scss │ │ ├── edgeModule.tsx │ │ ├── edit.scss │ │ ├── edit.tsx │ │ ├── help.scss │ │ ├── help.tsx │ │ ├── leafDevice.scss │ │ ├── leafDevice.tsx │ │ ├── modal.scss │ │ ├── modal.tsx │ │ ├── modalConfirm.scss │ │ ├── modalConfirm.tsx │ │ ├── reapply.scss │ │ ├── reapply.tsx │ │ ├── simulation.scss │ │ ├── simulation.tsx │ │ ├── ux.scss │ │ └── ux.tsx │ ├── nav │ │ ├── nav.scss │ │ └── nav.tsx │ ├── selector │ │ ├── selector.scss │ │ ├── selector.tsx │ │ ├── selectorCard.scss │ │ └── selectorCard.tsx │ ├── shell │ │ ├── console.scss │ │ ├── console.tsx │ │ ├── shell.scss │ │ └── shell.tsx │ ├── strings.ts │ └── ui │ │ ├── controls.tsx │ │ └── utilities.tsx └── server │ ├── api │ ├── bulk.ts │ ├── connect.ts │ ├── device.ts │ ├── devices.ts │ ├── dtdl.ts │ ├── plugins.ts │ ├── root.ts │ ├── sensors.ts │ ├── server.ts │ ├── simulation.ts │ ├── state.ts │ └── template.ts │ ├── config.ts │ ├── core │ ├── iotHub.ts │ ├── messageService.ts │ ├── mockDevice.ts │ ├── templates.ts │ └── utils.ts │ ├── framework │ └── AssociativeStore.ts │ ├── interfaces │ ├── device.ts │ ├── payload.ts │ └── plugin.ts │ ├── plugins │ ├── devicemove.ts │ ├── increment.ts │ ├── index.ts │ └── location.ts │ ├── start.ts │ └── store │ ├── deviceStore.ts │ ├── dtdlStore.ts │ ├── sensorStore.ts │ └── simulationStore.ts ├── static ├── HELP.md ├── favicon.ico ├── index.html └── vendor │ └── fontawesome-free-5.8.2-web │ ├── LICENSE.txt │ ├── css │ ├── all.css │ ├── all.min.css │ ├── brands.css │ ├── brands.min.css │ ├── fontawesome.css │ ├── fontawesome.min.css │ ├── regular.css │ ├── regular.min.css │ ├── solid.css │ ├── solid.min.css │ ├── svg-with-js.css │ ├── svg-with-js.min.css │ ├── v4-shims.css │ └── v4-shims.min.css │ ├── js │ ├── all.js │ ├── all.min.js │ ├── brands.js │ ├── brands.min.js │ ├── fontawesome.js │ ├── fontawesome.min.js │ ├── regular.js │ ├── regular.min.js │ ├── solid.js │ ├── solid.min.js │ ├── v4-shims.js │ └── v4-shims.min.js │ └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── tsconfig.client.json ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /_dist -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Electron Main", 11 | "cwd": "${workspaceFolder}", 12 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 13 | "windows": { 14 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd", 15 | }, 16 | "args": [ 17 | "." 18 | ], 19 | "console": "internalConsole", 20 | "outputCapture": "std", 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | WORKDIR /usr/src/app 4 | COPY package*.json ./ 5 | RUN npm i typescript -g && npm i webpack@4.32.2 -g && npm ci 6 | COPY . ./ 7 | RUN npm run build 8 | CMD [ "node", "./_dist/server/start.js" ] 9 | EXPOSE 9000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 codetunez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mock-devices v10 (Desktop Edition) 2 | mock-devices is a simulation engine that manages and runs simulated devices that connect to an Azure Iot Hub and modules and leaf devices for Azure Iot Edge. When hosted in the Azure IoT Edge runtime, the engine will simulate Edge modules too. The simulated devices and modules implement D2C/C2D scenarios i.e telemetry, twin and commands as supported by the Azure IoT Device SDK 3 | 4 | Each configured device/module acts independently of other devices/modules running within the engine. Each has its own model (capabilities), configuration and connection details. Devices/modules running on the same simulation engine can be a mix of connection strings, DPS, SaS, Edge modules. The engine has additional scenarios like cloning, bulk, templates and acknowledgements. See internal help 5 | 6 | This desktop edition of mock-devices is an Electron app for Windows/Linux/OSX and provides a UX + engine single install application experience. It is part of a suite of mock-devices tools 7 | 8 | **Other editions** 9 | - The [mock-devices-de](http://github.com/codetunez/mock-devices-de) edition is a Docker container of the running simulation engine. It exposes a REST endpoint for control and data plane operations. Use this edition to run the Edge modules configured in a mock-devices state file (and deploy as an Edge module) It is also useful where a headless simulation experience per state file is required 10 | 11 | - The [mock-devices-edge](http://github.com/codetunez/mock-devices-edge) edition is a Docker container configured as an Edge module that can be used to manage basic operations for running instances of mock-devices-de within the same Edge runtime. Clients can interact with the simulation engine using Twin Desired and Direct Commands making it an alternative to doing REST 12 | 13 | - The [mdux](https://hub.docker.com/r/codetunez/mdux) edition is a Docker container build of the desktop edition. It is a fully functional UX + simulation engine mock-devices instance and is useful for dynamic module scenarios. It is feature limited to run inside containers with no access to file system. It can also be used as a "terminal" UX experience to see other running mock-devices engines connecting via IP or DNS 14 | 15 | ## State file 16 | The state file is the current state of the simulation engine including the list of devices/modules, their capabilities and value set ups. Its also used as the load/save file for the mock-devices desktop tool. Both editions of mock-devices can create and/or utilize a state file created in ether edition with matching version numbers. It is recommended to use the desktop edition to create/manage state files 17 | 18 | # Getting started 19 | 20 | #### First time running the app - One Time Install and Build 21 | From a command prompt navigate to the folder where the repo was sync'd. Perform the following command. 22 | 23 | ``` 24 | npm ci && npm run build 25 | ``` 26 | 27 | Do this every time the code is sync'd from the repo i.e. getting a new version of the app. If you are experiencing issues with this step see the _Pre-Reqs and build issues_ step below 28 | 29 | ### Launching app (everyday use) 30 | From a command prompt navigate to the folder where the repo was sync'd and perform the following command 31 | 32 | ``` 33 | npm run app 34 | ```` 35 | 36 | ### Usage Instructions 37 | 38 | Basic help is available inside the application 39 | 40 | --- 41 | 42 | ### v10 Update 43 | - Azure IoT Edge Transparent Gateway and Identity Protocol support. The simulation runs on the mock-devices engine and therefore virtual machines or docker are not required to simulate these two Edge scenarios. The following is supported... 44 | 45 | 1. Simulate an Edge device and have it send and receive telemetry/twin/commands 46 | 2. Simulate leaf devices that can auto attach to the Edge parent device 47 | 2. Simulate any number of Modules for the Edge device and use Plugins for inter module communication 48 | 4. Simulate multiple Edge devices (and their children) at the same time 49 | 5. Use single or group DPS enrollment credentials for the Edge device and modules 50 | 51 | - The simulation engine has been cranked up to support __3,500__ devices simultaneously (the count of all Edges/Leafs/Modules/Devices configured in the engine) 52 | 53 | ### v9 Updates 54 | - Plugins - Provide app level state machines written in JavaScript that can be used at the device or capability level to provide values (Sample plugin provided) 55 | - Multiple GEOs - Each device can configure it’s own Geo radius 56 | - Override loop values - Use the simulation config to override loop duration for a capability 57 | - Reduced min/max loop times - Default times are now within the seconds and minute ranges 58 | - Connect dialog - Select a template and provision a device in IoT Central using an API token 59 | - Send values on StartUp - Capabilities can now opt in to be sent on Power Up. Assembled into single payload 60 | - Bulk update - Update specific common properties in a capability across a selection of devices 61 | 62 | #### Features 63 | - Sample device - Generate a sample mock device based from a pre-defined configuration 64 | - Supports 1,000 mock devices/modules 65 | - Various connection options; DPS single/group enrollment support with SaS. Sas and/or Connection String 66 | - Bulk/Clone/Templated device create operations 67 | - Auto gen DTDL Complex Types; Objects/Maps/Arrays with random values using the DCM 68 | - Simulated versions of common device operations such as Reboot, Shutdown, Firmware 69 | - "Pretend" sensors like battery, heaters, fans, +1 and -1 70 | - Plan mode - Create a timed series of events 71 | - Support for C2D command using cloud messages 72 | - Support for Azure IoT Edge modules 73 | - Dashboard statistics mode 74 | - Connect to any mock-devices engine using IP or DNS 75 | - Auto gen from DTDLv2 models/interfaces and DTDLv1 models (DCMs) 76 | 77 | #### Macro support for value payloads 78 | Use auto generated values to send as a device value when running in loops 79 | 80 | - AUTO_STRING - Use a random word from 'random-words' library 81 | - AUTO_BOOLEAN - Use a random true or false 82 | - AUTO_INTEGER - Use a random number 83 | - AUTO_LONG - Use a random number 84 | - AUTO_DOUBLE - Use a random number with precision 85 | - AUTO_FLOAT - Use a random number with precision 86 | - AUTO_DATE - Use now() ISO 8601 format 87 | - AUTO_DATETIME - Use now() ISO 8601 format 88 | - AUTO_TIME - Use now() ISO 8601 format 89 | - AUTO_DURATION - Use now() ISO 8601 format 90 | - AUTO_GEOPOINT - Use a random lat/long object 91 | - AUTO_VECTOR - Use a random x,y,z 92 | - AUTO_MAP - Use empty HashMap 93 | - AUTO_ENUM/* - Use a random Enum value (See below) 94 | - AUTO_VALUE - Use the last user supplied or mock sensor value. Honors String setting. 95 | 96 | --- 97 | ## Advanced, troubleshooting, Pre-Reqs and build issues 98 | Ensure you have the following configured for your environment 99 | 100 | *Pre-reqs* 101 | - Node LTS 102 | - git 103 | 104 | *If you are experiencing multiple build issues, try the following steps* 105 | 106 | Clear NPM cache 107 | ``` 108 | npm cache clean --force 109 | ``` 110 | 111 | Do the install step separately 112 | ``` 113 | npm ci 114 | ``` 115 | 116 | Rebuild node-sass 117 | ``` 118 | rebuild node-sass --force 119 | ``` 120 | 121 | Do the build step separately 122 | ``` 123 | npm run build 124 | ``` 125 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock-devices", 3 | "version": "10.3.0", 4 | "description": "mock-devices desktop edition", 5 | "main": "./_dist/server/start.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/codetunez/mock-devices" 9 | }, 10 | "scripts": { 11 | "build": "tsc && webpack", 12 | "watchux": "webpack --progress --colors --watch", 13 | "watchsrv": "tsc -watch --pretty", 14 | "app": "electron ./_dist/server/start.js" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@types/electron": "^1.6.10", 20 | "@types/lodash": "^4.14.157", 21 | "@types/node": "^12.0.2", 22 | "@types/react": "^17.0.0", 23 | "@types/react-dom": "^16.9.1", 24 | "@types/react-router-dom": "^5.1.5", 25 | "axios": "^0.21.1", 26 | "azure-iot-common": "^1.12.6", 27 | "azure-iot-device": "^1.17.2", 28 | "azure-iot-device-mqtt": "^1.15.2", 29 | "azure-iot-provisioning-device": "^1.8.6", 30 | "azure-iot-provisioning-device-mqtt": "^1.7.6", 31 | "azure-iot-security-symmetric-key": "^1.7.6", 32 | "azure-iothub": "^1.9.9", 33 | "body-parser": "^1.19.0", 34 | "bootstrap": "^4.3.1", 35 | "classnames": "^2.2.6", 36 | "cors": "^2.8.5", 37 | "css-loader": "^2.1.1", 38 | "electron": "^10.2.0", 39 | "express": "^4.17.0", 40 | "file-loader": "^6.0.0", 41 | "is-docker": "^2.0.0", 42 | "jimp": "^0.16.1", 43 | "jsoneditor": "^9.0.0", 44 | "lodash": "^4.17.21", 45 | "markdown-loader": "^6.0.0", 46 | "mini-css-extract-plugin": "^0.6.0", 47 | "morgan": "^1.9.1", 48 | "node-sass": "^4.12.0", 49 | "qrcode-reader": "^1.0.4", 50 | "random-location": "^1.1.2", 51 | "random-words": "^1.1.0", 52 | "react": "^17.0.0", 53 | "react-dom": "^17.0.0", 54 | "react-markdown": "^4.3.1", 55 | "react-router-dom": "^5.2.0", 56 | "react-spinners": "^0.9.0", 57 | "react-splitter-layout": "^4.0.0", 58 | "react-toggle": "^4.1.1", 59 | "sass-loader": "^7.1.0", 60 | "ts-loader": "^6.2.0", 61 | "typescript": "^3.6.4", 62 | "uuid": "^3.3.2", 63 | "webpack": "^4.32.2", 64 | "webpack-cli": "^3.3.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/client/app.scss: -------------------------------------------------------------------------------- 1 | @import "./const.scss"; 2 | html { 3 | scroll-behavior: smooth; 4 | } 5 | body { 6 | font-family: "Roboto Mono"; 7 | z-index: $order-base; 8 | } 9 | 10 | .section-title { 11 | text-align: center; 12 | font-size: 0.8rem; 13 | color: white; 14 | padding: $md-gutter $xs-gutter; 15 | text-transform: uppercase; 16 | font-weight: 700; 17 | } 18 | 19 | .section-title-nav { 20 | color: $app-clr-blue-1; 21 | } 22 | 23 | .section-title-header-active { 24 | background-color: $app-clr-1; 25 | } 26 | 27 | .btn-bar { 28 | display: flex; 29 | flex-flow: nowrap; 30 | button:not(:last-child) { 31 | margin-right: 5px; 32 | } 33 | } 34 | 35 | .inline-label-field { 36 | display: flex; 37 | justify-content: center; 38 | flex-flow: nowrap; 39 | > div, 40 | label { 41 | margin: 0 !important; 42 | margin-right: $sm-gutter !important; 43 | } 44 | } 45 | 46 | input[type="text"], 47 | input[type="number"], 48 | input[type="text"]:focus, 49 | input[type="number"]:focus, 50 | input[type="text"]:read-only, 51 | input[type="number"]:read-only, 52 | textarea:read-only { 53 | background-color: $app-clr-2 !important; 54 | color: $app-white !important; 55 | } 56 | 57 | input[type="text"]:disabled, 58 | input[type="number"]:disabled { 59 | cursor: not-allowed; 60 | color: $app-clr-4 !important; 61 | } 62 | 63 | .custom-textarea, 64 | .custom-textarea:focus { 65 | background-color: $app-clr-2; 66 | color: $app-white; 67 | resize: none; 68 | } 69 | 70 | .custom-textarea:disabled { 71 | cursor: not-allowed; 72 | } 73 | 74 | .custom-textarea-sm { 75 | height: calc(1.5em + 0.5rem + 2px); 76 | padding: 0.25rem 0.5rem; 77 | font-size: 0.875rem; 78 | line-height: 1.5; 79 | border-radius: 0.2rem; 80 | } 81 | 82 | .custom-combo-sm { 83 | margin-top: -2px; 84 | height: calc(1.5em + 0.5rem + 2px); 85 | padding: 0.25rem 0.5rem; 86 | padding-right: 1.6rem; 87 | font-size: 0.875rem; 88 | } 89 | 90 | .custom-select, 91 | .custom-select:focus { 92 | background-color: $app-clr-2; 93 | color: $app-white; 94 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='white' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"); 95 | } 96 | 97 | .custom-checkbox { 98 | display: block; 99 | position: relative; 100 | padding-left: 25px; 101 | cursor: pointer; 102 | font-size: 22px; 103 | -webkit-user-select: none; 104 | -moz-user-select: none; 105 | -ms-user-select: none; 106 | user-select: none; 107 | } 108 | 109 | .custom-checkbox input { 110 | position: absolute; 111 | opacity: 0; 112 | cursor: pointer; 113 | height: 0; 114 | width: 0; 115 | } 116 | 117 | .checkmark { 118 | position: absolute; 119 | top: 0; 120 | left: 0; 121 | height: 16px; 122 | width: 16px; 123 | background-color: #eee; 124 | margin-top: 6px; 125 | } 126 | 127 | .custom-checkbox:hover input ~ .checkmark { 128 | background-color: #ccc; 129 | } 130 | 131 | .custom-checkbox input:checked ~ .checkmark { 132 | background-color: #2196f3; 133 | } 134 | 135 | .checkmark:after { 136 | content: ""; 137 | position: absolute; 138 | display: none; 139 | } 140 | 141 | .custom-checkbox input:checked ~ .checkmark:after { 142 | display: block; 143 | } 144 | 145 | .custom-checkbox .checkmark:after { 146 | left: 5px; 147 | top: 1px; 148 | width: 6px; 149 | height: 11px; 150 | border: solid white; 151 | border-width: 0 3px 3px 0; 152 | -webkit-transform: rotate(45deg); 153 | -ms-transform: rotate(45deg); 154 | transform: rotate(45deg); 155 | } 156 | 157 | button:disabled { 158 | cursor: not-allowed; 159 | } 160 | 161 | .snippets { 162 | font-size: 0.7rem; 163 | display: flex; 164 | width: 410px; 165 | color: $app-clr-4; 166 | 167 | > div:first-child { 168 | color: $app-clr-yellow-1; 169 | white-space: nowrap; 170 | } 171 | 172 | * { 173 | margin-right: $sm-gutter; 174 | } 175 | 176 | .snippet-links { 177 | display: flex; 178 | flex-wrap: wrap; 179 | 180 | div { 181 | cursor: pointer; 182 | } 183 | 184 | div:hover { 185 | color: $app-white; 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /src/client/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import { Route, BrowserRouter as Router } from 'react-router-dom' 4 | 5 | import { Shell } from './shell/shell' 6 | import { DeviceProvider } from './context/deviceContext' 7 | import { AppProvider } from './context/appContext'; 8 | import { StatsProvider } from './context/statsContext'; 9 | import { ControlProvider } from './context/controlContext'; 10 | 11 | const routing = ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | ReactDOM.render(routing, document.getElementById('app')) -------------------------------------------------------------------------------- /src/client/const.scss: -------------------------------------------------------------------------------- 1 | /* margin */ 2 | $xs-gutter: 0.3rem; 3 | $sm-gutter: 0.5rem; 4 | $md-gutter: 1rem; 5 | $lg-gutter: 1.5rem; 6 | $xl-gutter: 2rem; 7 | 8 | /* sizes */ 9 | $width-nav: 75px; 10 | $width-selector: 240px; 11 | $width-content: 100%; 12 | $border-radius: 0.2rem; 13 | $border-thicker: 2px; 14 | $title-height: 48px; 15 | 16 | /* ordering */ 17 | $order-base: 0; 18 | $order-level-1: 100; 19 | $order-level-1: 200; 20 | $order-floating: 500; 21 | $order-blastshield: 750; 22 | $order-dialog: 1000; 23 | 24 | /* color */ 25 | $app-black: #000; 26 | $app-white: #fff; 27 | $app-shield: #111; 28 | $app-shadow: rgba(0, 0, 0, 0.75); 29 | 30 | $app-clr-0: #1e1e1e; 31 | $app-clr-1: #252526; 32 | $app-clr-2: #333333; 33 | $app-clr-3: #37373d; 34 | $app-clr-4: #888888; 35 | 36 | $app-clr-blue-1: #0275d8; 37 | $app-clr-blue-2: #6392c4; 38 | $app-clr-blue-3: #162c35; 39 | $app-clr-blue-4: #525c6a; 40 | $app-clr-blue-5: #199dab; 41 | $app-clr-yellow-1: #daba89; 42 | $app-clr-yellow-2: #ffc107; 43 | $app-clr-green-1: #106b18; 44 | $app-clr-green-2: #19ab27; 45 | $app-clr-orange-1: orange; 46 | $app-clr-red-1: red; 47 | $app-clr-purple-1: #c15bb3; 48 | $app-clr-purple-2: #9a89da; 49 | $app-clr-purple-3: #4324b3; 50 | -------------------------------------------------------------------------------- /src/client/context/appContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import axios from 'axios'; 3 | import { Endpoint } from './endpoint'; 4 | 5 | export const AppContext = React.createContext({}); 6 | 7 | export class AppProvider extends React.PureComponent { 8 | 9 | private dirty: string = ''; 10 | 11 | setExpand = (id: any) => { 12 | const newProp = this.state.property; 13 | newProp[id] = !newProp[id] ? true : !newProp[id] 14 | this.setState({ ...this.state, property: newProp }); 15 | } 16 | 17 | setSelectorExpand = (flag: boolean) => { 18 | this.setState({ ...this.state, selectorExpand: flag }); 19 | } 20 | 21 | setDirty = (id: string) => { 22 | if (this.dirty !== id) { 23 | this.dirty = id; 24 | localStorage.setItem('dirty', this.dirty); 25 | } 26 | } 27 | 28 | getDirty = () => { 29 | return localStorage.getItem('dirty'); 30 | } 31 | 32 | clearDirty = () => { 33 | this.dirty = ''; 34 | localStorage.setItem('dirty', this.dirty); 35 | } 36 | 37 | state: any = { 38 | property: {}, 39 | dirtyProperty: '', 40 | selectorExpand: true, 41 | setExpand: this.setExpand, 42 | setSelectorExpand: this.setSelectorExpand, 43 | setDirty: this.setDirty, 44 | getDirty: this.getDirty, 45 | clearDirty: this.clearDirty 46 | } 47 | 48 | render() { 49 | return ( 50 | 51 | {this.props.children} 52 | 53 | ) 54 | } 55 | } -------------------------------------------------------------------------------- /src/client/context/controlContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Endpoint } from './endpoint'; 3 | 4 | export const ControlContext = React.createContext({}); 5 | 6 | export class ControlProvider extends React.PureComponent { 7 | 8 | private eventSource = null; 9 | 10 | constructor() { 11 | super(null); 12 | this.init(); 13 | } 14 | 15 | init = () => { 16 | setInterval(() => { 17 | if (!this.eventSource) { 18 | this.eventSource = new EventSource(`${Endpoint.getEndpoint()}api/events/control`); 19 | this.eventSource.onmessage = ((e) => { 20 | this.setControlMessages(JSON.parse(e.data)); 21 | }); 22 | } 23 | }, 250); 24 | } 25 | 26 | killConnection = () => { 27 | if (this.eventSource != null) { 28 | this.eventSource.close() 29 | this.eventSource = null; 30 | } 31 | } 32 | 33 | setControlMessages(data: any) { 34 | this.setState({ control: data }); 35 | } 36 | 37 | state: any = { 38 | control: {}, 39 | killConnection: this.killConnection 40 | } 41 | 42 | render() { 43 | return ( 44 | 45 | {this.props.children} 46 | 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /src/client/context/endpoint.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import axios from 'axios'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | const VARS = { 6 | sessionId: '', 7 | } 8 | 9 | export class Endpoint { 10 | 11 | static setSession(id) { 12 | VARS.sessionId = id; 13 | } 14 | 15 | static getEndpoint() { 16 | //REFACTOR: The safest way to re-init all URLs is to reload the UX with the new Endpoint 17 | // defined. For the reload to come back to the same UX, we store temporarily the 18 | // session its needs to come back to. This is a high risk approach and only works 19 | // because the expectation is not many UXs will launch at the same time. 20 | if (localStorage.getItem('lastSession')) { 21 | VARS.sessionId = localStorage.getItem('lastSession'); 22 | localStorage.removeItem('lastSession'); 23 | } 24 | return localStorage.getItem(`engEndpoint-${VARS.sessionId}`) || '/'; 25 | } 26 | 27 | static setEndpoint = ({ serverEndpoint, serverMode }) => { 28 | serverEndpoint = serverEndpoint += serverEndpoint.slice(-1) === '/' ? '' : '/' 29 | const mode = !serverMode || serverMode && serverMode === '' ? 'ux' : serverMode; 30 | axios.post(`${Endpoint.getEndpoint()}api/setmode/${mode}`, { serverEndpoint, serverMode: mode }) 31 | .then((response: any) => { 32 | localStorage.setItem('lastSession', VARS.sessionId) 33 | localStorage.setItem(`engEndpoint-${VARS.sessionId}`, serverEndpoint); 34 | window.location.reload(); 35 | }) 36 | } 37 | 38 | static resetEndpoint() { 39 | localStorage.setItem('lastSession', VARS.sessionId) 40 | localStorage.setItem(`engEndpoint-${VARS.sessionId}`, '/'); 41 | window.location.reload(); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/client/context/statsContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Endpoint } from './endpoint'; 3 | 4 | export const StatsContext = React.createContext({}); 5 | 6 | export class StatsProvider extends React.PureComponent { 7 | 8 | private eventSource = null; 9 | 10 | constructor() { 11 | super(null); 12 | this.init(); 13 | } 14 | 15 | init = () => { 16 | setInterval(() => { 17 | if (!this.eventSource) { 18 | this.eventSource = new EventSource(`${Endpoint.getEndpoint()}api/events/stats`); 19 | this.eventSource.onmessage = ((e) => { 20 | this.setStats(JSON.parse(e.data)); 21 | }); 22 | } 23 | }, 250); 24 | } 25 | 26 | killConnection = () => { 27 | if (this.eventSource != null) { 28 | this.eventSource.close() 29 | this.eventSource = null; 30 | } 31 | } 32 | 33 | setStats(data: any) { 34 | this.setState({ stats: data }); 35 | } 36 | 37 | state: any = { 38 | stats: {}, 39 | killConnection: this.killConnection 40 | } 41 | 42 | render() { 43 | return ( 44 | 45 | {this.props.children} 46 | 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /src/client/dashboard/dashboard.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dashboard { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .dashboard-toolbar { 11 | background-color: $app-clr-1; 12 | padding: 1rem; 13 | width: 100%; 14 | flex-shrink: 0; 15 | } 16 | 17 | .dashboard-content { 18 | background-color: $app-clr-1; 19 | display: flex; 20 | width: 100%; 21 | height: 100%; 22 | flex-flow: column; 23 | flex-grow: 1; 24 | overflow: auto; 25 | padding: 0 1rem; 26 | } 27 | 28 | .tile-host { 29 | width: 100%; 30 | height: 100%; 31 | display: flex; 32 | flex-flow: wrap; 33 | } 34 | 35 | .tile { 36 | background-color: $app-clr-2; 37 | border: solid 1px $app-clr-3; 38 | border-radius: 2px; 39 | margin: 0 1rem 1rem 0; 40 | min-width: 26.3rem; 41 | height: fit-content; 42 | box-shadow: 0 0 0.5rem 0.1rem rgba(0, 0, 0, 0.2); 43 | 44 | .header { 45 | padding: $md-gutter; 46 | border-bottom: 1px solid $app-clr-4; 47 | color: $app-clr-yellow-1; 48 | font-weight: 500; 49 | 50 | a { 51 | text-decoration: none; 52 | } 53 | } 54 | 55 | .body { 56 | text-align: center; 57 | padding: $md-gutter; 58 | font-size: 0.8rem; 59 | } 60 | 61 | .tile-field { 62 | display: flex; 63 | justify-content: space-between; 64 | div:first-child { 65 | font-weight: 900; 66 | } 67 | margin-bottom: $sm-gutter; 68 | } 69 | } 70 | 71 | .waiting { 72 | height: 100%; 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | } 77 | 78 | .power-icons { 79 | display: flex; 80 | flex-wrap: wrap; 81 | a:hover { 82 | text-decoration: none; 83 | } 84 | } 85 | 86 | .power-icon { 87 | margin-top: 8px; 88 | display: flex; 89 | flex-direction: column; 90 | align-items: center; 91 | justify-content: center; 92 | background-color: transparent; 93 | border: 2px solid $app-clr-2; 94 | border-radius: $border-radius; 95 | padding: 0.8rem 0 0.5rem 0; 96 | width: 5rem; 97 | margin: 0 6px 6px 0; 98 | font-size: 0.8rem; 99 | } 100 | 101 | .power-icon:hover { 102 | cursor: pointer; 103 | background-color: $app-clr-blue-1; 104 | color: $app-white; 105 | } 106 | 107 | /* - DUPED -- */ 108 | 109 | .control { 110 | font-size: 0.8rem; 111 | padding: 0 $xs-gutter; 112 | } 113 | .control-OFF { 114 | color: $app-clr-4; 115 | } 116 | .control-DELAY { 117 | color: $app-clr-purple-1; 118 | } 119 | .control-ON { 120 | color: $app-clr-blue-1; 121 | } 122 | .control-TRYING { 123 | color: $app-clr-orange-1; 124 | } 125 | .control-SUCCESS { 126 | color: $app-clr-green-1; 127 | } 128 | .control-INIT { 129 | color: $app-clr-blue-5; 130 | } 131 | .control-CONNECTED { 132 | color: $app-clr-green-2; 133 | } 134 | .control-ERROR { 135 | color: $app-clr-red-1; 136 | } 137 | -------------------------------------------------------------------------------- /src/client/dashboard/dashboard.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./dashboard.scss')); 3 | import * as React from 'react'; 4 | import { RESX } from '../strings'; 5 | import { Stats } from './stats'; 6 | import { Grid } from './grid'; 7 | 8 | export const Dashboard: React.FunctionComponent = () => { 9 | 10 | const [type, setType] = React.useState(0); 11 | 12 | return
13 |
14 |
15 | 19 | 23 |
24 |
25 |
26 | {type === 0 ? : null} 27 | {type === 1 ? : null} 28 |
29 |
30 | } -------------------------------------------------------------------------------- /src/client/dashboard/grid.tsx: -------------------------------------------------------------------------------- 1 | 2 | var classNames = require('classnames'); 3 | const cx = classNames.bind(require('./dashboard.scss')); 4 | import { ControlContext } from '../context/controlContext'; 5 | import { DeviceContext } from '../context/deviceContext'; 6 | 7 | import * as React from 'react'; 8 | import { RESX } from '../strings'; 9 | import { Link } from 'react-router-dom' 10 | 11 | export function Grid() { 12 | 13 | const buildBoxes = (control: any) => { 14 | const dom = []; 15 | for (const key in control) { 16 | if (key === '__clear') { continue; } 17 | dom.push( 18 | 19 |
20 | 21 | {control[key][2]} 22 |
23 | 24 | ) 25 | } 26 | return
{dom}
27 | } 28 | 29 | return 30 | {(state: any) =>
31 | {buildBoxes(state.control)} 32 |
} 33 |
34 | 35 | } -------------------------------------------------------------------------------- /src/client/dashboard/stats.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./dashboard.scss')); 3 | import { StatsContext } from '../context/statsContext'; 4 | import * as React from 'react'; 5 | import { RESX } from '../strings'; 6 | import { Link } from 'react-router-dom' 7 | 8 | export function Stats() { 9 | function dom(stats) { 10 | const dom = [] 11 | for (const item in stats) { 12 | const dom2 = []; 13 | for (const item2 in stats[item]) { 14 | const lbl = RESX.UI.STATS[item2] || item2 15 | dom2.push(
16 |
{lbl}
17 |
{stats[item][item2] || RESX.dashboard.nodata}
18 |
); 19 | } 20 | dom.push(
21 |
{item}
22 |
{dom2}
23 |
); 24 | } 25 | return dom; 26 | } 27 | 28 | return 29 | {(state: any) => (
30 | {state.stats && Object.keys(state.stats).length === 0 ?
{RESX.dashboard.waiting}
: 31 |
{dom(state.stats)}
32 | } 33 |
)} 34 |
35 | } -------------------------------------------------------------------------------- /src/client/device/device.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .device { 4 | min-width: 670px; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | flex-wrap: nowrap; 9 | width: 100%; 10 | } 11 | 12 | .device-title { 13 | flex-shrink: 0; 14 | border-bottom: 1px solid $app-clr-3; 15 | padding: 1rem; 16 | } 17 | 18 | .device-title-active { 19 | background-color: $app-clr-1; 20 | } 21 | 22 | .device-commands { 23 | flex-shrink: 0; 24 | border-bottom: 1px solid $app-clr-3; 25 | box-shadow: 0px 2px 3px 0px $app-shadow; 26 | } 27 | 28 | .device-fields { 29 | flex-grow: 1; 30 | min-height: 0; 31 | height: 100%; 32 | overflow-y: auto; 33 | } 34 | 35 | .device-capabilities, 36 | .device-plan, 37 | .device-edge { 38 | margin: $md-gutter 0 0 $md-gutter; 39 | display: flex; 40 | flex-wrap: wrap; 41 | overflow: hidden; 42 | } 43 | 44 | .device-capabilities-empty { 45 | padding-right: $sm-gutter; 46 | } 47 | -------------------------------------------------------------------------------- /src/client/device/device.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./device.scss')); 3 | 4 | import * as React from 'react'; 5 | import { useParams } from 'react-router-dom' 6 | import { DeviceTitle } from '../deviceTitle/deviceTitle'; 7 | import { DeviceCommands } from '../deviceCommands/deviceCommands'; 8 | import { DevicePower } from '../devicePower/devicePower'; 9 | import { DeviceFieldD2C } from '../deviceFields/deviceFieldD2C'; 10 | import { DeviceFieldC2D } from '../deviceFields/deviceFieldC2D'; 11 | import { DeviceFieldMethod } from '../deviceFields/deviceFieldMethod'; 12 | import { DevicePlan } from '../devicePlan/devicePlan'; 13 | import { DeviceEdge } from '../deviceEdge/deviceEdge'; 14 | import { DeviceOptions } from '../deviceOptions/deviceOptions'; 15 | import { RESX } from '../strings'; 16 | 17 | import { DeviceContext } from '../context/deviceContext'; 18 | import { AppContext } from '../context/appContext'; 19 | import { ControlContext } from '../context/controlContext'; 20 | 21 | export function Device() { 22 | 23 | const { id } = useParams(); 24 | 25 | const [edgeSelect, setEdgeSelect] = React.useState(true); 26 | const deviceContext: any = React.useContext(DeviceContext); 27 | const appContext: any = React.useContext(AppContext); 28 | 29 | appContext.clearDirty(); 30 | 31 | React.useEffect(() => { 32 | appContext.clearDirty(); 33 | deviceContext.getDevice(id); 34 | }, [id]); 35 | 36 | if (!deviceContext.device.configuration) { return null; } 37 | 38 | const kind = deviceContext.device.configuration._kind; 39 | const modules = deviceContext.device.configuration.modules || []; 40 | const modulesDocker = deviceContext.device.configuration.modulesDocker || {}; 41 | const leafDevices = deviceContext.device.configuration.leafDevices || []; 42 | const content = deviceContext.device.configuration.planMode ? 'plan' : kind === 'edge' ? 'edge' : 'caps'; 43 | const plugIn = deviceContext.device.configuration.plugIn && deviceContext.device.configuration.plugIn !== '' ? true : false; 44 | const showOptions = kind === 'edge' || kind === 'module' || kind === 'moduleHosted' || kind === 'leafDevice'; 45 | 46 | return <>{Object.keys(deviceContext.device).length > 0 ? 47 |
48 |
49 | 50 | {(state: any) => ( 51 | 52 | )} 53 | 54 |
55 |
56 |
57 | {showOptions ?
: null} 58 | 59 |
60 | {content === 'plan' ?
: null} 61 | {content === 'edge' ?
62 | 63 | {(state: any) => ( 64 | <> 65 | {edgeSelect ? 66 | 67 | : 68 | deviceContext.device && deviceContext.device.comms && deviceContext.device.comms.map((capability: any) => { 69 | const expand = appContext.property[capability._id] || false; 70 | return <> 71 | {capability.type && capability.type.direction === 'd2c' ? : null} 72 | {capability.type && capability.type.direction === 'c2d' ? : null} 73 | {capability._type === 'method' ? : null} 74 | 75 | }) 76 | } 77 | 78 | )} 79 | 80 |
: null} 81 | {content === 'caps' ?
82 | {deviceContext.device && deviceContext.device.comms && deviceContext.device.comms.map((capability: any) => { 83 | const expand = appContext.property[capability._id] || false; 84 | return <> 85 | {capability.type && capability.type.direction === 'd2c' ? : null} 86 | {capability.type && capability.type.direction === 'c2d' ? : null} 87 | {capability._type === 'method' ? : null} 88 | 89 | })} 90 |
91 | {deviceContext.device.comms && deviceContext.device.comms.length === 0 ? RESX.device.empty : ''} 92 |
93 |
: null} 94 |
95 |
: null} 96 | 97 | } -------------------------------------------------------------------------------- /src/client/deviceCommands/deviceCommands.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .device-commands-container { 4 | display: flex; 5 | flex-flow: nowrap; 6 | justify-content: space-between; 7 | padding: $sm-gutter $md-gutter; 8 | } 9 | -------------------------------------------------------------------------------- /src/client/deviceCommands/deviceCommands.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./deviceCommands.scss')); 3 | 4 | import * as React from 'react'; 5 | import { Modal } from '../modals/modal'; 6 | import { EdgeModule } from '../modals/edgeModule'; 7 | import { LeafDevice } from '../modals/leafDevice'; 8 | import { Edit } from '../modals/edit'; 9 | import { RESX } from '../strings'; 10 | import { decodeModuleKey } from '../ui/utilities'; 11 | import { DeviceContext } from '../context/deviceContext'; 12 | import { AppContext } from '../context/appContext'; 13 | import { ModalConfirm } from '../modals/modalConfirm'; 14 | 15 | export function DeviceCommands() { 16 | 17 | const deviceContext: any = React.useContext(DeviceContext); 18 | const appContext: any = React.useContext(AppContext); 19 | 20 | const [showEdit, toggleEdit] = React.useState(false); 21 | const [showEdgeModule, toggleEdgeModule] = React.useState(false); 22 | const [showEdgeDevice, toggleEdgeDevice] = React.useState(false); 23 | const [showDelete, toggleDelete] = React.useState(false); 24 | const [showDirty, toggleDirty] = React.useState(false); 25 | 26 | const kind = deviceContext.device.configuration._kind; 27 | const index = deviceContext.devices.findIndex((x) => x._id == deviceContext.device._id); 28 | 29 | const deleteDialogAction = (result) => { 30 | if (result === "Yes") { 31 | //TODO: refactor 32 | appContext.clearDirty(); 33 | deviceContext.deleteDevice(); 34 | } 35 | toggleDelete(false); 36 | } 37 | 38 | const deleteModalConfig = { 39 | title: RESX.modal.delete_title, 40 | message: kind === 'edge' ? RESX.modal.delete_edge : kind === 'template' ? RESX.modal.delete_template : RESX.modal.delete_device, 41 | options: { 42 | buttons: [RESX.modal.YES, RESX.modal.NO], 43 | handler: deleteDialogAction, 44 | close: () => toggleDelete(false) 45 | } 46 | } 47 | 48 | const dirtyModalConfig = { 49 | title: RESX.modal.device.add_capability_title, 50 | message: RESX.modal.save_first, 51 | options: { 52 | buttons: [RESX.modal.OK], 53 | handler: () => toggleDirty(false), 54 | close: () => toggleDirty(false) 55 | } 56 | } 57 | 58 | const checkDirty = (type: string, direction?: string) => { 59 | if (appContext.getDirty() != '') { 60 | toggleDirty(true); 61 | } else { 62 | deviceContext.createCapability(type, direction); 63 | } 64 | } 65 | 66 | return
67 |
68 | {deviceContext.device.configuration.planMode ? 69 | <> 70 | : 71 | <>{kind != 'edge' ? <> 72 | 73 | 74 | 75 | 76 | : 77 | <> 78 | 79 | 80 | 81 | } 82 | } 83 |
84 |
85 | 86 | 87 |
88 | {showEdit ?
: null} 89 | {showEdgeModule ?
: null} 90 | {showEdgeDevice ?
: null} 91 | {showDelete ?
: null} 92 | {showDirty ?
: null} 93 |
94 | } -------------------------------------------------------------------------------- /src/client/deviceEdge/deviceEdge.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .device-edge-empty { 4 | padding-right: $sm-gutter; 5 | } 6 | 7 | .device-edge-modules { 8 | .title { 9 | padding-bottom: $md-gutter; 10 | } 11 | .list { 12 | display: flex; 13 | flex-direction: row; 14 | flex-wrap: wrap; 15 | } 16 | 17 | .edge-module { 18 | max-width: 280px; 19 | } 20 | 21 | .edge-module:not(:last-child) { 22 | margin: 0 1rem 1rem 0; 23 | } 24 | 25 | h4 { 26 | text-overflow: ellipsis; 27 | overflow: hidden; 28 | white-space: nowrap; 29 | } 30 | 31 | .expander { 32 | padding-left: $xs-gutter; 33 | display: flex; 34 | justify-content: space-between; 35 | align-items: center; 36 | height: 2.6rem; 37 | cursor: default; 38 | font-size: 1rem; 39 | 40 | .btn-bar { 41 | align-items: center; 42 | div { 43 | margin-right: 0.5rem; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/deviceEdge/deviceEdge.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx2 = classNames.bind(require('../selector/selectorCard.scss')); 3 | const cx = classNames.bind(require('./deviceEdge.scss')); 4 | 5 | import * as React from 'react'; 6 | import { RESX } from '../strings'; 7 | import { controlEvents } from '../ui/utilities'; 8 | import { DeviceEdgeChildren } from './deviceEdgeChildren'; 9 | import { ControlContext } from '../context/controlContext'; 10 | 11 | export function DeviceEdge({ gatewayId, modules, modulesDocker, leafDevices, control }) { 12 | 13 | return <>{modules.length > 0 || leafDevices.length > 0 ? 14 |
15 | 16 | {(state: any) => ( 17 | 18 |
19 | {modules.map((element, index) => { 20 | const runningEvent = control && control[element] ? control[element][2] : controlEvents.OFF; 21 | return 22 | })} 23 | {leafDevices.map((element, index) => { 24 | const runningEvent = control && control[element] ? control[element][2] : controlEvents.OFF; 25 | return 26 | })} 27 |
28 | )} 29 |
30 |
31 | : null} 32 |
33 | {modules.length === 0 || leafDevices.length === 0 ? <>

{RESX.edge.empty[0]}

{RESX.edge.empty[1]}

{RESX.edge.empty[2]}

{RESX.edge.empty[3]}

: null} 34 |
35 | 36 | } -------------------------------------------------------------------------------- /src/client/deviceEdge/deviceEdgeChildren.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx2 = classNames.bind(require('../selector/selectorCard.scss')); 3 | const cx = classNames.bind(require('./deviceEdge.scss')); 4 | 5 | import * as React from 'react'; 6 | import { DeviceContext } from '../context/deviceContext'; 7 | import { AppContext } from '../context/appContext'; 8 | import { RESX } from '../strings'; 9 | import { decodeModuleKey, controlEvents } from '../ui/utilities'; 10 | import { Modal } from '../modals/modal'; 11 | import { ModalConfirm } from '../modals/modalConfirm'; 12 | 13 | export function DeviceEdgeChildren({ control, gatewayId, index, compositeKey, running, type, docker }) { 14 | 15 | const deviceContext: any = React.useContext(DeviceContext); 16 | const appContext: any = React.useContext(AppContext); 17 | const [power, setPower] = React.useState({}); 18 | 19 | const [showDelete, toggleDelete] = React.useState(false); 20 | 21 | const deleteDialogAction = (result) => { 22 | if (result === "Yes") { 23 | //TODO: refactor 24 | appContext.clearDirty(); 25 | deviceContext.deleteModule(compositeKey); 26 | } 27 | toggleDelete(false); 28 | } 29 | 30 | const deleteModalConfig = { 31 | title: RESX.modal.delete_title, 32 | message: RESX.modal.delete_module, 33 | options: { 34 | buttons: [RESX.modal.YES, RESX.modal.NO], 35 | handler: deleteDialogAction, 36 | close: () => toggleDelete(false) 37 | } 38 | } 39 | 40 | let moduleId, deviceId, title: any = ''; 41 | if (type === 'module') { 42 | const decoded = decodeModuleKey(compositeKey); 43 | moduleId = decoded.moduleId; 44 | deviceId = decoded.deviceId; 45 | title = RESX.edge.card.title_module; 46 | } else { 47 | moduleId = `(${compositeKey})` 48 | deviceId = gatewayId; 49 | title = RESX.edge.card.title_device; 50 | } 51 | 52 | React.useEffect(() => { 53 | const on = control && control[compositeKey] ? control[compositeKey][2] != controlEvents.OFF : false; 54 | setPower({ 55 | label: on ? RESX.device.power.powerOff_label : RESX.device.power.powerOn_label, 56 | title: on ? RESX.device.power.powerOff_title : RESX.device.power.powerOn_title, 57 | style: on ? "btn-success" : "btn-outline-secondary", 58 | handler: on ? (() => deviceContext.stopDevice(compositeKey)) : (() => deviceContext.startDevice(compositeKey)) 59 | }) 60 | }, [deviceContext.device, control[compositeKey]]) 61 | 62 | return <> 63 |
64 |
65 |
{title} {index + 1}
66 |
67 |
{docker && docker[compositeKey] ? : null}
68 | 69 | 70 |
71 |
72 | 80 |
81 | { showDelete ?
: null} 82 | 83 | } -------------------------------------------------------------------------------- /src/client/deviceFields/deviceFields.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | $device-field-width: 40rem; 4 | $device-field-width-1st-col: 9rem; 5 | $device-card-field: 6rem; 6 | $device-card-input: 6rem; 7 | $device-card-minimized: 9rem; 8 | $device-card-button-size: 2.5rem; 9 | $device-card-field-label-height: 1.7rem; 10 | 11 | $device-card-title-width: 500px; 12 | $device-card-title-width-text: 500px; 13 | 14 | .device-field-card { 15 | padding: $sm-gutter $sm-gutter 0 $sm-gutter; 16 | overflow-wrap: break-word; 17 | background-color: $app-clr-2; 18 | color: $app-white; 19 | border-radius: $border-radius; 20 | min-width: $device-field-width; 21 | max-width: $device-field-width; 22 | overflow: hidden; 23 | margin-bottom: $md-gutter; 24 | margin-right: $md-gutter; 25 | } 26 | 27 | .device-field-card-dirty { 28 | box-shadow: inset 0px 0px 1px 1px $app-clr-yellow-2; 29 | } 30 | 31 | .device-field-card-small { 32 | height: $device-card-minimized; 33 | } 34 | 35 | .df-card-header { 36 | display: flex; 37 | justify-content: space-between; 38 | 39 | .df-card-menu { 40 | width: 0; 41 | } 42 | 43 | .df-card-title { 44 | display: flex; 45 | width: $device-card-title-width; 46 | 47 | .df-card-title-chevron { 48 | margin: $sm-gutter; 49 | border: none; 50 | border-radius: 2px; 51 | background-color: transparent; 52 | cursor: pointer; 53 | color: $app-white; 54 | margin: 0 0.3rem 0 -0.3rem; 55 | } 56 | 57 | .df-card-title-chevron:focus { 58 | outline: none; 59 | box-shadow: 0 0 2px $app-clr-4; 60 | } 61 | 62 | .df-card-title-chevron-spacer { 63 | width: $xl-gutter; 64 | } 65 | 66 | .df-card-title-text { 67 | * { 68 | width: $device-card-title-width-text; 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | white-space: nowrap; 72 | } 73 | 74 | div:first-child { 75 | color: $app-clr-4; 76 | font-size: 0.7rem; 77 | font-weight: 700; 78 | } 79 | div:nth-child(2) { 80 | color: $app-white; 81 | font-weight: 600; 82 | font-size: 0.95rem; 83 | } 84 | } 85 | } 86 | 87 | .df-card-value { 88 | text-align: center; 89 | div:first-child { 90 | color: $app-clr-4; 91 | font-size: 0.7rem; 92 | font-weight: 700; 93 | } 94 | div:nth-child(2) { 95 | font-size: 0.9rem; 96 | } 97 | } 98 | 99 | .df-card-cmd { 100 | button { 101 | width: $device-card-button-size; 102 | height: $device-card-button-size; 103 | } 104 | } 105 | } 106 | 107 | .df-card-row-wide { 108 | font-size: 0.7rem; 109 | text-align: justify; 110 | font-weight: 700; 111 | width: 99%; 112 | margin-top: $md-gutter; 113 | margin-bottom: $xl-gutter; 114 | padding-left: 1.6rem; 115 | color: $app-clr-blue-2; 116 | } 117 | 118 | .df-card-row { 119 | font-size: 0.8rem; 120 | font-weight: 700; 121 | display: flex; 122 | margin-top: $md-gutter; 123 | margin-bottom: $xl-gutter; 124 | padding-left: $xl-gutter; 125 | 126 | .card-field-label-height { 127 | min-height: $device-card-field-label-height; 128 | } 129 | 130 | input[type="text"], 131 | input[type="number"] { 132 | width: $device-card-input; 133 | margin-right: $xs-gutter; 134 | } 135 | 136 | .mock-field { 137 | width: 5.2rem !important; 138 | } 139 | 140 | .single-width { 141 | width: $device-card-field !important; 142 | margin-right: 0.3rem; 143 | } 144 | 145 | .double-width { 146 | width: ($device-card-field * 2) !important; 147 | margin-right: 0.3rem; 148 | } 149 | 150 | .third-width { 151 | width: ($device-card-field * 3) !important; 152 | margin-right: 0.3rem; 153 | } 154 | 155 | .full-width { 156 | width: ($device-card-field * 4) !important; 157 | margin-right: 0.3rem; 158 | } 159 | 160 | textarea:focus { 161 | margin-bottom: 0; 162 | } 163 | 164 | .single-item { 165 | align-self: flex-end; 166 | } 167 | 168 | .custom-textarea-xsm { 169 | width: ($device-card-field * 4); 170 | } 171 | } 172 | 173 | .df-card-row-nogap { 174 | margin-top: -20px; 175 | } 176 | 177 | .df-card-row > div:nth-child(1) { 178 | width: $device-field-width-1st-col; 179 | color: $app-clr-yellow-1; 180 | } 181 | -------------------------------------------------------------------------------- /src/client/deviceOptions/deviceOptions.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .device-options { 4 | display: flex; 5 | width: 100%; 6 | justify-content: space-between; 7 | padding: $sm-gutter $md-gutter; 8 | height: 3.5rem; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/deviceOptions/deviceOptions.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./deviceOptions.scss')); 3 | 4 | import * as React from 'react'; 5 | import { DeviceContext } from '../context/deviceContext'; 6 | import { decodeModuleKey } from '../ui/utilities'; 7 | import { RESX } from '../strings'; 8 | 9 | export function DeviceOptions({ selection, handler }) { 10 | const deviceContext: any = React.useContext(DeviceContext); 11 | const kind = deviceContext.device.configuration._kind; 12 | 13 | let leafDevice = decodeModuleKey(deviceContext.device._id); 14 | if (kind === 'leafDevice') { 15 | leafDevice = { 16 | deviceId: deviceContext.device.configuration.gatewayDeviceId 17 | } 18 | } 19 | 20 | return
21 | {kind === 'module' || kind === 'moduleHosted' || kind === 'leafDevice' ? 22 | 23 | : 24 |
25 | 29 | 33 |
34 | } 35 |
36 | } -------------------------------------------------------------------------------- /src/client/devicePlan/devicePlan.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .plan { 4 | width: 100%; 5 | height: 100%; 6 | overflow-y: auto; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .plan-title { 12 | max-width: 620px; 13 | margin-bottom: $md-gutter; 14 | text-align: center; 15 | } 16 | 17 | .plan-card { 18 | max-width: 620px; 19 | padding: $sm-gutter; 20 | background-color: $app-clr-2; 21 | color: $app-white; 22 | border-radius: $border-radius; 23 | margin: 0 $md-gutter $md-gutter 0; 24 | h5 { 25 | color: $app-clr-4; 26 | margin-bottom: $sm-gutter; 27 | } 28 | } 29 | 30 | .mini-grid { 31 | display: table; 32 | 33 | .mini-grid-row-header { 34 | display: table-row; 35 | .mini-grid-header { 36 | font-size: 0.9rem; 37 | display: table-cell; 38 | padding: 0 $xs-gutter $xs-gutter 0; 39 | } 40 | } 41 | 42 | .mini-grid-row { 43 | display: table-row; 44 | .mini-grid-cell { 45 | display: table-cell; 46 | padding: 0 $xs-gutter $xs-gutter 0; 47 | 48 | button { 49 | margin-top: -3px; 50 | } 51 | } 52 | .mini-grid-cell:not(:last-child) { 53 | margin-right: $sm-gutter; 54 | } 55 | .mini-grid-header { 56 | label : { 57 | color: $app-clr-yellow-1; 58 | } 59 | } 60 | } 61 | 62 | .mini-grid-row:not(:last-child) { 63 | margin-bottom: $sm-gutter; 64 | } 65 | 66 | .mini-grid-row-toolbar { 67 | margin-top: $sm-gutter; 68 | } 69 | } 70 | 71 | .cell-width-2 { 72 | width: 50%; 73 | } 74 | 75 | .cell-width-3 { 76 | width: 33%; 77 | } 78 | 79 | .cell-width-4 { 80 | width: 25%; 81 | } 82 | -------------------------------------------------------------------------------- /src/client/devicePower/devicePower.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .device-toolbar-container { 4 | padding: $md-gutter $md-gutter $sm-gutter $md-gutter; 5 | border-bottom: 1px solid $app-clr-3; 6 | box-shadow: 0px 2px 3px 0px $app-shadow; 7 | overflow: hidden; 8 | display: flex; 9 | flex-wrap: nowrap; 10 | justify-content: space-between; 11 | align-items: center; 12 | 13 | .type { 14 | display: flex; 15 | align-items: center; 16 | font-size: 1.1rem; 17 | font-weight: 100; 18 | color: $app-clr-4; 19 | } 20 | 21 | .plugin { 22 | margin-left: 0.5rem; 23 | padding: 0.25rem 1rem; 24 | font-size: 0.75rem; 25 | background-color: $app-clr-purple-3; 26 | color: $app-white; 27 | border-radius: 2px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/devicePower/devicePower.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./devicePower.scss')); 3 | 4 | import * as React from 'react'; 5 | import { DeviceContext } from '../context/deviceContext'; 6 | import { controlEvents } from '../ui/utilities'; 7 | import { Modal } from '../modals/modal'; 8 | import { Reapply } from '../modals/reapply'; 9 | import { RESX } from '../strings'; 10 | 11 | export function DevicePower({ control }) { 12 | 13 | const deviceContext: any = React.useContext(DeviceContext); 14 | const [power, setPower] = React.useState({}); 15 | const [showReapply, toggleReapply] = React.useState(false); 16 | 17 | const kind = deviceContext.device.configuration._kind; 18 | const plugIn = deviceContext.device.configuration.plugIn; 19 | 20 | React.useEffect(() => { 21 | const on = control && control[deviceContext.device._id] ? control[deviceContext.device._id][2] != controlEvents.OFF : false; 22 | setPower({ 23 | label: on ? RESX.device.power.powerOff_label : RESX.device.power.powerOn_label, 24 | title: on ? RESX.device.power.powerOff_title : RESX.device.power.powerOn_title, 25 | style: on ? "btn-success" : "btn-outline-secondary", 26 | handler: on ? deviceContext.stopDevice : deviceContext.startDevice 27 | }) 28 | }, [deviceContext.device, control[deviceContext.device._id]]) 29 | 30 | return
31 |
32 | {kind === 'template' ? 33 | 34 | : 35 | 36 | } 37 |
38 | 39 |
40 | {kind === 'template' ? RESX.device.power.kindTemplate : kind === 'edge' ? RESX.device.power.kindEdge : kind === 'module' ? RESX.device.power.kindModule : kind === 'leafDevice' ? RESX.device.power.kindEdgeDevice : RESX.device.power.kindReal} 41 | {plugIn ?
{plugIn} Plugin
: null} 42 |
43 | {showReapply ?
: null} 44 |
45 | } -------------------------------------------------------------------------------- /src/client/deviceTitle/deviceTitle.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .device-title-container { 4 | display: flex; 5 | width: 100%; 6 | justify-content: space-between; 7 | height: 2rem; 8 | 9 | div:first-child { 10 | font-weight: 900; 11 | font-size: 1.2rem; 12 | white-space: nowrap; 13 | } 14 | 15 | > div:first-child { 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | margin-right: 2rem; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/deviceTitle/deviceTitle.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./deviceTitle.scss')); 3 | 4 | import * as React from 'react'; 5 | import { DeviceContext } from '../context/deviceContext'; 6 | import { RESX } from '../strings'; 7 | 8 | export function DeviceTitle() { 9 | const deviceContext: any = React.useContext(DeviceContext); 10 | const kind = deviceContext.device.configuration._kind; 11 | const planMode = deviceContext.device.configuration.planMode; 12 | 13 | return
14 |
{deviceContext.device.configuration && deviceContext.device.configuration.mockDeviceName}
15 | {kind === 'edge' ? null : 16 | <> 17 |
18 | 22 | 27 |
28 | 29 | } 30 |
31 | } -------------------------------------------------------------------------------- /src/client/devices/devices.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; -------------------------------------------------------------------------------- /src/client/devices/devices.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | var classNames = require('classnames'); 5 | const cx = classNames.bind(require('./devices.scss')); 6 | const cx2 = classNames.bind(require('../shell/shell.scss')); 7 | 8 | import { Selector } from '../selector/selector'; 9 | import { Device } from '../device/device'; 10 | 11 | export const Devices: React.FunctionComponent = () => { 12 | return <> 13 |
14 |
15 | 16 | } -------------------------------------------------------------------------------- /src/client/modals/addDevice.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-add { 4 | height: 100%; 5 | 6 | .m-modal { 7 | min-height: $sm-gutter !important; 8 | 9 | .m-content { 10 | display: flex; 11 | } 12 | 13 | .m-close { 14 | min-height: 42px !important; 15 | padding-top: $md-gutter; 16 | padding-right: $md-gutter; 17 | } 18 | } 19 | 20 | .menu-vertical button { 21 | margin-bottom: $sm-gutter; 22 | } 23 | 24 | label { 25 | margin-left: 0 !important; 26 | color: $app-clr-yellow-1; 27 | } 28 | 29 | .error { 30 | color: $app-clr-red-1; 31 | } 32 | 33 | .container { 34 | font-size: 0.8rem; 35 | i { 36 | margin-top: $xs-gutter; 37 | } 38 | } 39 | 40 | .quick-url { 41 | color: $app-clr-blue-2; 42 | } 43 | 44 | .edge-modules-list { 45 | > span { 46 | font-size: 0.8rem; 47 | } 48 | > div { 49 | display: flex; 50 | } 51 | 52 | > div:not(:first-child) { 53 | margin-bottom: 0.5rem; 54 | } 55 | 56 | > div > *:nth-child(1) { 57 | width: 220px; 58 | margin-right: 0.5rem; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/client/modals/bulk.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-bulk { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | 8 | .split-pane { 9 | margin-top: 1rem; 10 | display: flex; 11 | height: 100%; 12 | } 13 | 14 | .split-pane > div { 15 | width: 40%; 16 | } 17 | 18 | .split-pane > div:first-child { 19 | margin-right: 1.5rem; 20 | } 21 | 22 | .split-pane > div:nth-child(2) { 23 | width: 60%; 24 | overflow-y: auto; 25 | height: 500px; 26 | padding-right: 1rem; 27 | } 28 | 29 | .split-pane .form-group .df-card-row { 30 | padding: 0; 31 | 32 | > div { 33 | margin-right: 0.5rem; 34 | } 35 | } 36 | 37 | .inline-form { 38 | display: flex; 39 | align-items: center; 40 | 41 | input:first-child { 42 | margin-right: 0.75rem; 43 | margin-bottom: 0.5rem; 44 | } 45 | } 46 | 47 | select { 48 | width: 100%; 49 | } 50 | 51 | label { 52 | margin-left: 0 !important; 53 | color: $app-clr-yellow-1; 54 | } 55 | 56 | select:disabled { 57 | cursor: not-allowed; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/client/modals/connect.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-central { 4 | height: 100%; 5 | 6 | .m-modal { 7 | padding: $md-gutter; 8 | 9 | .m-content { 10 | padding: 0 $md-gutter; 11 | 12 | .progress-container { 13 | height: 2rem; 14 | display: flex; 15 | align-items: center; 16 | .p-bar { 17 | margin: 0 0 $sm-gutter $sm-gutter; 18 | height: 1rem; 19 | } 20 | } 21 | 22 | .form-inline { 23 | display: flex; 24 | height: auto; 25 | align-items: flex-end; 26 | width: 100%; 27 | h3 { 28 | margin-bottom: $md-gutter; 29 | } 30 | .form-group:not(:last-child) { 31 | padding-right: $md-gutter; 32 | } 33 | margin-bottom: $lg-gutter; 34 | } 35 | 36 | .form-group { 37 | display: flex; 38 | align-items: flex-start; 39 | flex-direction: column; 40 | } 41 | 42 | input { 43 | width: 280px; 44 | } 45 | 46 | label { 47 | margin-left: 0 !important; 48 | color: $app-clr-yellow-1; 49 | } 50 | 51 | .btn-bar { 52 | button { 53 | color: $app-clr-4; 54 | } 55 | 56 | .active { 57 | color: white; 58 | text-decoration: underline; 59 | } 60 | } 61 | 62 | .error { 63 | color: $app-clr-red-1; 64 | } 65 | } 66 | } 67 | } 68 | 69 | .button-spinner { 70 | display: flex; 71 | align-items: center; 72 | > div { 73 | margin-left: $sm-gutter; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/client/modals/consoleInspector.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-inspect { 4 | height: 100%; 5 | .m-modal { 6 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 7 | } 8 | 9 | .console-buttons { 10 | display: flex; 11 | padding: 0 1rem 1rem 0; 12 | a { 13 | cursor: pointer; 14 | margin-right: 0.6rem; 15 | } 16 | } 17 | 18 | .console-window { 19 | height: calc(100% - 3rem); 20 | } 21 | 22 | .console-message { 23 | background-color: $app-black; 24 | border: solid 1px $app-clr-4; 25 | overflow-y: scroll; 26 | overflow-x: hidden; 27 | overflow-wrap: break-word; 28 | height: 100%; 29 | 30 | div { 31 | background-color: $app-black; 32 | padding: 1.2rem; 33 | position: fixed; 34 | font-size: 0.9rem; 35 | overflow: hidden; 36 | width: calc(100% - 60px); 37 | } 38 | 39 | pre { 40 | padding-top: $lg-gutter * 3.5; 41 | padding-left: $lg-gutter; 42 | color: $app-white; 43 | font-size: 1.4rem; 44 | white-space: pre-wrap; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/modals/consoleInspector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | var classNames = require('classnames'); 4 | const cx = classNames.bind(require('./consoleInspector.scss')); 5 | const cxM = classNames.bind(require('./modal.scss')); 6 | import { RESX } from '../strings'; 7 | 8 | export const ConsoleInspector: React.FunctionComponent = ({ lines, index, handler }) => { 9 | 10 | const [messages, setMessages] = React.useState({ lines, index }); 11 | 12 | React.useEffect(() => { 13 | setMessages({ lines, index }); 14 | }, [index, lines]); 15 | 16 | const display = () => { 17 | const regex = /(.*?)([^\]]*)$/ 18 | const match = messages.lines[messages.index].match(regex); 19 | 20 | let title = ''; 21 | let preContent = ''; 22 | if (match) { 23 | title = match[1]; 24 | try { 25 | preContent = JSON.stringify(JSON.parse(match[2]), null, 2); 26 | } catch (err) { 27 | preContent = match[2].trim(); 28 | } 29 | } 30 | 31 | return <> 32 |
{title}
33 |
{preContent}
34 | 35 | } 36 | 37 | const prev = () => { 38 | setMessages({ lines, index: messages.index > 0 ? messages.index - 1 : 0 }); 39 | } 40 | const next = () => { 41 | setMessages({ lines, index: messages.index < lines.length - 1 ? messages.index + 1 : lines.length - 1 }); 42 | } 43 | 44 | return
45 |
46 |
handler(false)}>
47 |
48 |
49 | next()}> 50 | prev()}> 51 |
{RESX.modal.console.text1[0]} {lines.length - messages.index} {RESX.modal.console.text1[1]} {messages.lines.length}
52 |
53 |
54 |
{display()}
55 |
56 |
57 |
58 |
59 | } -------------------------------------------------------------------------------- /src/client/modals/edgeModule.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-module { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | 8 | label { 9 | margin-left: 0 !important; 10 | color: $app-clr-yellow-1; 11 | } 12 | } -------------------------------------------------------------------------------- /src/client/modals/edgeModule.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./edgeModule.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | import "react-toggle/style.css" 5 | 6 | import * as React from 'react'; 7 | import axios from 'axios'; 8 | import { DeviceContext } from '../context/deviceContext'; 9 | import { RESX } from '../strings'; 10 | import { Combo, Json } from '../ui/controls'; 11 | import { Endpoint } from '../context/endpoint'; 12 | import Toggle from 'react-toggle'; 13 | 14 | export const EdgeModule: React.FunctionComponent = ({ handler, deviceId, scopeId, sasKey }) => { 15 | 16 | const deviceContext: any = React.useContext(DeviceContext); 17 | const [moduleList, setModuleList] = React.useState([]); 18 | const [state, setPayload] = React.useState({ 19 | _kind: 'module', 20 | _deviceList: [], 21 | _plugIns: [], 22 | mockDeviceCloneId: '', 23 | moduleId: '', 24 | deviceId, 25 | scopeId, 26 | sasKey, 27 | plugIn: '' 28 | }); 29 | 30 | React.useEffect(() => { 31 | let list = []; 32 | let plugIns = []; 33 | axios.get(`${Endpoint.getEndpoint()}api/devices`) 34 | .then((response: any) => { 35 | list.push({ name: RESX.modal.module.select1, value: null }); 36 | response.data.map((ele: any) => { 37 | list.push({ name: ele.configuration.mockDeviceName, value: ele._id }); 38 | }); 39 | return axios.get(`${Endpoint.getEndpoint()}api/plugins`); 40 | }) 41 | .then((response: any) => { 42 | 43 | plugIns.push({ name: RESX.modal.module.select2, value: null }); 44 | response.data.map((ele: any) => { 45 | plugIns.push({ name: ele, value: ele }); 46 | }); 47 | 48 | setPayload({ 49 | ...state, 50 | _deviceList: list, 51 | _plugIns: plugIns 52 | }) 53 | }) 54 | }, []); 55 | 56 | const updateField = (e: any) => { 57 | setPayload({ 58 | ...state, 59 | [e.target.name]: e.target.value 60 | }) 61 | } 62 | 63 | const toggleEnvironment = () => { 64 | setPayload({ 65 | ...state, 66 | _kind: state._kind === 'module' ? 'moduleHosted' : 'module' 67 | }); 68 | } 69 | 70 | const save = () => { 71 | deviceContext.updateDeviceModules(state, 'module'); 72 | handler(); 73 | } 74 | 75 | const loadManifest = () => { 76 | axios.get(`${Endpoint.getEndpoint()}api/openDialog`) 77 | .then(response => { 78 | if (response.data) { 79 | const modules = response.data?.modulesContent?.['$edgeAgent']?.['properties.desired']?.['modules'] || {}; 80 | const combo = [{ name: RESX.modal.module.select3, value: null }]; 81 | for (const module in modules) { 82 | combo.push({ name: module, value: module }) 83 | } 84 | setModuleList(combo); 85 | }; 86 | }) 87 | } 88 | 89 | return
90 |
91 |
handler(false)}>
92 |
93 |

{RESX.modal.module.title}

94 |
95 |
96 | 97 |
98 | 99 |
100 |
101 | 102 |
103 | 104 |
105 |
106 | 107 |
108 | 109 |
110 | {moduleList && moduleList.length > 0 ? 111 | 112 | : 113 | 114 | } 115 |
116 | 117 |
118 |
119 |
{ toggleEnvironment() }} />
120 |
121 | 122 |
123 |
124 |
125 | 126 |
127 |
128 |
129 |
130 | } -------------------------------------------------------------------------------- /src/client/modals/edit.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-edit { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | } 8 | 9 | .editor { 10 | height: 280px; 11 | } 12 | -------------------------------------------------------------------------------- /src/client/modals/edit.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./edit.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | 5 | import * as React from 'react'; 6 | import { DeviceContext } from '../context/deviceContext'; 7 | import { RESX } from '../strings'; 8 | import { Combo, Json } from '../ui/controls'; 9 | 10 | export const Edit: React.FunctionComponent = ({ handler, index }) => { 11 | 12 | const deviceContext: any = React.useContext(DeviceContext); 13 | const [json, setJson] = React.useState({ simulation: {} }); 14 | const [error, setError] = React.useState(''); 15 | const [position, setPosition] = React.useState(index + 1); 16 | 17 | React.useEffect(() => { 18 | deviceContext.getDevice(deviceContext.device.configuration.deviceId); 19 | }, []) 20 | 21 | const updateDeviceConfiguration = () => { 22 | deviceContext.updateDeviceConfiguration(json); 23 | handler(); 24 | } 25 | 26 | const updateDevicePosition = () => { 27 | deviceContext.reorderDevicePosition({ current: index, next: position - 1 }); 28 | handler(); 29 | } 30 | 31 | const updateField = (text: any) => { 32 | setJson(text); 33 | setError(''); 34 | } 35 | 36 | const updatePosition = (e: any) => { 37 | setPosition(parseInt(e.target.value)); 38 | } 39 | 40 | const indexes = deviceContext.devices.map((ele, index) => { 41 | return { name: index + 1, value: index + 1 } 42 | }); 43 | 44 | return
45 |
46 |
handler(false)}>
47 |
48 |
49 |

{RESX.modal.edit.title1}

50 |

{RESX.modal.edit.text1}

51 |
52 | { updateField(text) }} err={() => setError(RESX.modal.error_json)} /> 53 |
54 |
55 | 56 |
{error}
57 |
58 |
59 |
60 | 61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | } -------------------------------------------------------------------------------- /src/client/modals/help.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .help { 4 | width: 100%; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | flex-wrap: nowrap; 9 | } 10 | 11 | .help-close { 12 | margin: 0.5rem 1rem 0 0; 13 | height: 1.4rem; 14 | flex-shrink: 0; 15 | align-self: flex-end; 16 | cursor: pointer; 17 | } 18 | 19 | .help-content { 20 | flex-grow: 1; 21 | overflow-x: hidden; 22 | overflow-y: auto; 23 | padding: 0 $md-gutter; 24 | 25 | h2, 26 | h3 { 27 | color: $app-clr-yellow-1; 28 | } 29 | h4, 30 | h5 { 31 | color: $app-clr-blue-2; 32 | } 33 | li { 34 | margin-bottom: 0.75rem; 35 | } 36 | pre { 37 | color: $app-white; 38 | background-color: $app-black; 39 | padding: 1rem; 40 | } 41 | hr { 42 | margin: 2rem 4rem; 43 | border: 1px solid white; 44 | } 45 | } -------------------------------------------------------------------------------- /src/client/modals/help.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./help.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | 5 | import * as React from 'react'; 6 | import * as ReactMarkdown from 'react-markdown'; 7 | import axios from 'axios'; 8 | import { Endpoint } from '../context/endpoint'; 9 | 10 | export const Help: React.FunctionComponent = ({ handler }) => { 11 | 12 | const [text, setText] = React.useState(''); 13 | 14 | React.useEffect(() => { 15 | axios.get(`${Endpoint.getEndpoint()}api/help`) 16 | .then((response: any) => { 17 | setText(response.data); 18 | }) 19 | }, []); 20 | 21 | return
22 |
handler(false)}>
23 |
24 | 25 |
26 |
27 | } -------------------------------------------------------------------------------- /src/client/modals/leafDevice.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-module { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | 8 | label { 9 | margin-left: 0 !important; 10 | color: $app-clr-yellow-1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/modals/leafDevice.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./edgeModule.scss')); 3 | const cxM = classNames.bind(require('./leafDevice.scss')); 4 | 5 | import Toggle from 'react-toggle'; 6 | import "react-toggle/style.css" 7 | 8 | import * as React from 'react'; 9 | import axios from 'axios'; 10 | import { DeviceContext } from '../context/deviceContext'; 11 | import { RESX } from '../strings'; 12 | import { Combo, Json } from '../ui/controls'; 13 | import { Endpoint } from '../context/endpoint'; 14 | 15 | export const LeafDevice: React.FunctionComponent = ({ handler, gatewayDeviceId, capabilityUrn }) => { 16 | 17 | const deviceContext: any = React.useContext(DeviceContext); 18 | const [state, setPayload] = React.useState({ 19 | _kind: 'leafDevice', 20 | _deviceList: [], 21 | _plugIns: [], 22 | deviceId: '', 23 | mockDeviceName: '', 24 | mockDeviceCount: 1, 25 | mockDeviceCountMax: 1, 26 | mockDeviceCloneId: '', 27 | connectionString: '', 28 | scopeId: '', 29 | dpsPayload: { "iotcGateway": { "iotcGatewayId": gatewayDeviceId } }, 30 | sasKey: '', 31 | isMasterKey: false, 32 | capabilityModel: '', 33 | capabilityUrn: capabilityUrn, 34 | machineState: '', 35 | machineStateClipboard: '', 36 | plugIn: '', 37 | gatewayDeviceId: gatewayDeviceId 38 | }); 39 | 40 | React.useEffect(() => { 41 | let list = []; 42 | let plugIns = []; 43 | axios.get(`${Endpoint.getEndpoint()}api/devices`) 44 | .then((response: any) => { 45 | list.push({ name: RESX.modal.leafDevice.select1, value: null }); 46 | response.data.map((ele: any) => { 47 | list.push({ name: ele.configuration.mockDeviceName, value: ele._id }); 48 | }); 49 | return axios.get(`${Endpoint.getEndpoint()}api/plugins`); 50 | }) 51 | .then((response: any) => { 52 | 53 | plugIns.push({ name: RESX.modal.leafDevice.select2, value: null }); 54 | response.data.map((ele: any) => { 55 | plugIns.push({ name: ele, value: ele }); 56 | }); 57 | 58 | setPayload({ 59 | ...state, 60 | _deviceList: list, 61 | _plugIns: plugIns 62 | }) 63 | }) 64 | }, []); 65 | 66 | const updateField = (e: any) => { 67 | setPayload({ 68 | ...state, 69 | [e.target.name]: e.target.value 70 | }) 71 | } 72 | 73 | const toggleMasterKey = () => { 74 | setPayload({ 75 | ...state, 76 | isMasterKey: !state.isMasterKey 77 | }); 78 | } 79 | 80 | const getTemplate = (id: string) => { 81 | axios.get(`${Endpoint.getEndpoint()}api/device/${id}`) 82 | .then(response => { 83 | const json = response.data; 84 | 85 | let payload: any = {} 86 | payload.scopeId = json.configuration.scopeId 87 | payload.capabilityUrn = json.configuration.capabilityUrn 88 | payload.mockDeviceCloneId = id; 89 | payload.dpsPayload = Object.assign({}, state.dpsPayload, { "iotcModelId": json.configuration.capabilityUrn }); 90 | 91 | if (json.configuration.isMasterKey) { 92 | payload.sasKey = json.configuration.sasKey; 93 | payload.isMasterKey = true; 94 | } 95 | 96 | setPayload(Object.assign({}, state, payload)); 97 | document.getElementById('device-id').focus(); 98 | }) 99 | } 100 | 101 | const clickAddDevice = () => { 102 | const newState = Object.assign({}, state); 103 | delete newState._deviceList; 104 | delete newState._plugIns; 105 | deviceContext.updateDeviceModules(state, 'leafDevice'); 106 | handler(); 107 | } 108 | 109 | return
110 |
111 |
handler(false)}>
112 |
113 |

{RESX.modal.leafDevice.title}

114 |
115 |
116 | getTemplate(e.target.value)} value={state.mockDeviceCloneId || ''} /> 117 |
118 | 119 |
120 |
121 | 122 |
123 | 124 |
125 |
126 | 127 |
128 | 129 |
130 | 131 | 132 |
133 |
134 | 135 | 136 |
137 |
138 | 139 |
{ toggleMasterKey() }} />
140 |
141 | 142 |
143 |
144 |
145 | 146 |
147 |
148 |
149 | 150 |
151 |
152 | 153 |
154 | 155 | 156 |
157 | 158 |
159 |
160 |
161 | 162 |
163 |
164 |
165 |
166 | } -------------------------------------------------------------------------------- /src/client/modals/modal.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | /* shield */ 4 | .blast-shield { 5 | z-index: $order-blastshield; 6 | position: absolute; 7 | left: 0; 8 | top: 0; 9 | height: 100vh; 10 | width: 100vw; 11 | background-color: $app-shield; 12 | opacity: 0.92; 13 | } 14 | 15 | /* dialog types */ 16 | .app-modal { 17 | z-index: $order-dialog; 18 | position: absolute; 19 | background-color: $app-clr-2; 20 | color: $app-white; 21 | overflow: hidden; 22 | } 23 | 24 | .context-modal { 25 | top: 0; 26 | right: 0; 27 | width: calc(100vw - 40%); 28 | height: 100%; 29 | overflow-y: auto; 30 | } 31 | 32 | .context-modal-wide { 33 | width: calc(100vw - 20%); 34 | } 35 | 36 | .center-modal { 37 | top: 50%; 38 | left: 50%; 39 | transform: translate(-50%, -50%); 40 | width: 864px; 41 | height: 688px; 42 | border-radius: $xs-gutter; 43 | } 44 | 45 | .height-modal { 46 | height: auto; 47 | } 48 | 49 | .min-modal { 50 | width: 528px; 51 | height: auto; 52 | } 53 | 54 | /* dialog ui */ 55 | 56 | .m-modal { 57 | height: 100%; 58 | width: 100%; 59 | display: flex; 60 | flex-direction: column; 61 | } 62 | 63 | .m-close { 64 | text-align: right; 65 | i { 66 | cursor: pointer; 67 | } 68 | } 69 | 70 | .m-content { 71 | flex-grow: 1; 72 | width: 100%; 73 | overflow: auto; 74 | h4 { 75 | margin-bottom: $md-gutter; 76 | } 77 | } 78 | 79 | .m-footer { 80 | margin-top: $lg-gutter; 81 | flex-shrink: 0; 82 | .form-group { 83 | margin-bottom: 0; 84 | justify-content: flex-end; 85 | } 86 | } 87 | 88 | .m-commands { 89 | align-self: flex-end; 90 | } 91 | 92 | .m-tabbed-nav { 93 | height: calc(100% - 25px); 94 | width: 250px; 95 | text-align: center; 96 | padding: 0 $lg-gutter; 97 | border-right: 1px solid $app-clr-4; 98 | label { 99 | font-weight: 700; 100 | text-transform: uppercase; 101 | font-size: 0.9rem; 102 | margin: 0 $sm-gutter $sm-gutter $sm-gutter; 103 | } 104 | label:not(:first-child) { 105 | margin-top: $sm-gutter; 106 | } 107 | button { 108 | width: 100%; 109 | margin-bottom: $xs-gutter; 110 | font-size: 0.7rem; 111 | } 112 | } 113 | 114 | .m-tabbed-panel { 115 | height: calc(100% - 25px); 116 | width: calc(100% - 250px); 117 | padding: 0 0 $md-gutter $lg-gutter; 118 | 119 | display: flex; 120 | flex-direction: column; 121 | 122 | .m-tabbed-panel-form { 123 | flex-grow: 1; 124 | overflow: auto; 125 | padding-right: $lg-gutter; 126 | label { 127 | font-weight: 700; 128 | font-size: 0.9rem; 129 | margin: 0 $xs-gutter $sm-gutter $sm-gutter; 130 | } 131 | .form-group { 132 | width: 100%; 133 | margin-bottom: 0.8rem; 134 | 135 | h4 { 136 | font-size: 1.3rem; 137 | margin-bottom: 0.9rem; 138 | } 139 | } 140 | .input-wide { 141 | padding-right: 0; 142 | } 143 | } 144 | 145 | .m-tabbed-panel-footer { 146 | padding-top: $sm-gutter; 147 | flex-shrink: 0; 148 | height: 2rem; 149 | align-self: flex-end; 150 | width: 100%; 151 | } 152 | } 153 | 154 | .ace_content { 155 | height: 400px; 156 | } -------------------------------------------------------------------------------- /src/client/modals/modal.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | 5 | export class Modal extends React.Component { 6 | render() { 7 | return ReactDOM.createPortal( 8 | this.props.children, 9 | document.querySelector("#modal-root") 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/modals/modalConfirm.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-confirm { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/client/modals/modalConfirm.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./modalConfirm.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | 5 | import * as React from 'react'; 6 | 7 | export const ModalConfirm: React.FunctionComponent = ({ config }) => { 8 | return
9 |
10 |
config.options.close()}>
11 |
12 |

{config.title}

13 |
{config.message}
14 |
15 |
16 |
17 | {config && config.options.buttons.map((ele: any, index: number) => { 18 | return 19 | })} 20 |
21 |
22 |
23 |
24 | } -------------------------------------------------------------------------------- /src/client/modals/reapply.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-reapply { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | 8 | select { 9 | width: 100%; 10 | } 11 | 12 | select:disabled { 13 | cursor: not-allowed; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/modals/reapply.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./reapply.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | 5 | import * as React from 'react'; 6 | import { DeviceContext } from '../context/deviceContext'; 7 | import { RESX } from '../strings'; 8 | 9 | export const Reapply: React.FunctionComponent = ({ handler }) => { 10 | 11 | const deviceContext: any = React.useContext(DeviceContext); 12 | const [all, setAll] = React.useState(false); 13 | 14 | const apply = () => { 15 | const selectedList = []; 16 | if (!all) { 17 | const list: any = document.getElementById('devices'); 18 | for (const item of list) { if (item.selected) { selectedList.push(item.value) } } 19 | } 20 | deviceContext.reapplyTemplate({ templateId: deviceContext.device._id, devices: selectedList, all: all }) 21 | handler(); 22 | } 23 | 24 | const indexes = deviceContext.devices.map((ele) => { 25 | return { name: ele.configuration.mockDeviceName, value: ele.configuration.deviceId } 26 | }); 27 | 28 | return
29 |
30 |
handler(false)}>
31 |
32 |
33 |

{RESX.modal.reapply.title1}

34 | {RESX.modal.reapply.title2} 35 |

36 | setAll(!all)} /> {RESX.modal.reapply.selectAll} 37 | 42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 | } -------------------------------------------------------------------------------- /src/client/modals/simulation.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .simulation { 4 | width: 100%; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | flex-wrap: nowrap; 9 | } 10 | 11 | .simulation-close { 12 | margin: 0.5rem 1rem 0 0; 13 | height: 1.4rem; 14 | flex-shrink: 0; 15 | align-self: flex-end; 16 | cursor: pointer; 17 | } 18 | 19 | .simulation-content { 20 | flex-grow: 1; 21 | padding: 0 $md-gutter 0 $md-gutter; 22 | 23 | .editor { 24 | height: calc(100vh - 316px); 25 | margin-bottom: 1rem; 26 | } 27 | } 28 | .simulation-footer { 29 | flex-shrink: 0; 30 | padding: 0 $md-gutter $md-gutter $md-gutter; 31 | display: flex; 32 | 33 | > div { 34 | margin-left: $sm-gutter; 35 | margin-top: $xs-gutter; 36 | } 37 | .error { 38 | color: $app-clr-red-1; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/modals/simulation.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./simulation.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | 5 | import axios from 'axios'; 6 | import * as React from 'react'; 7 | 8 | import { RESX } from '../strings'; 9 | import { Json } from '../ui/controls'; 10 | import { Endpoint } from '../context/endpoint'; 11 | 12 | export const Simulation: React.FunctionComponent = ({ handler }) => { 13 | 14 | const [updatePayload, setPayload] = React.useState({ simulation: {} }); 15 | const [json, setJson] = React.useState({}); 16 | const [error, setError] = React.useState(''); 17 | 18 | React.useEffect(() => { 19 | axios.get(`${Endpoint.getEndpoint()}api/simulation`) 20 | .then((response: any) => { 21 | setPayload(response.data); 22 | }) 23 | .catch((err) => { 24 | setError(RESX.modal.simulation.error_load); 25 | }); 26 | }, []); 27 | 28 | const updateField = (obj: any) => { 29 | setJson(obj); 30 | setError(''); 31 | } 32 | 33 | const reset = () => { 34 | if (error != '') { return; } 35 | axios.post(`${Endpoint.getEndpoint()}api/simulation`, { simulation: json }) 36 | .then((res) => { 37 | // this is a temp hack to workaround a routing issue 38 | window.location.href = "/"; 39 | handler(false); 40 | }) 41 | .catch((err) => { 42 | setError(RESX.modal.simulation.error_save); 43 | }) 44 | } 45 | 46 | return
47 |
handler(false)}>
48 |
49 |

{RESX.modal.simulation.title}

50 |

{RESX.modal.simulation.text1}

51 |
52 | { updateField(obj) }} err={() => setError(RESX.modal.error_json)} /> 53 |
54 |

{RESX.modal.simulation.text2}

55 |
56 |
57 | 58 |
{error}
59 |
60 |
61 | } -------------------------------------------------------------------------------- /src/client/modals/ux.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .dialog-ux { 4 | .m-modal { 5 | padding: $sm-gutter $md-gutter $md-gutter $md-gutter; 6 | } 7 | } -------------------------------------------------------------------------------- /src/client/modals/ux.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./ux.scss')); 3 | const cxM = classNames.bind(require('./modal.scss')); 4 | 5 | import * as React from 'react'; 6 | import { Combo } from '../ui/controls'; 7 | import { RESX } from '../strings'; 8 | import { Endpoint } from '../context/endpoint'; 9 | import { DeviceContext } from '../context/deviceContext'; 10 | 11 | export const Ux: React.FunctionComponent = ({ handler, index }) => { 12 | 13 | const deviceContext: any = React.useContext(DeviceContext); 14 | const [state, setPayload] = React.useState({ serverEndpoint: Endpoint.getEndpoint(), serverMode: '' }); 15 | 16 | React.useEffect(() => { 17 | var ele = document.getElementById('serverEndpoint') as HTMLInputElement; 18 | ele.focus(); 19 | ele.select(); 20 | }, []) 21 | 22 | const updateField = (e: any) => { 23 | setPayload({ 24 | ...state, 25 | [e.target.name]: e.target.value 26 | }) 27 | } 28 | 29 | const save = (state?) => { 30 | if (state) { Endpoint.setEndpoint(state) } else { Endpoint.resetEndpoint() }; 31 | handler(false); 32 | } 33 | 34 | const combo = [{ name: '--Select to change', value: null }, { name: 'ux', value: 'ux' }, { name: 'server', value: 'server' }, { name: 'mixed', value: 'mixed' },] 35 | 36 | return
37 |
38 |
handler(false)}>
39 |
40 |

{RESX.modal.ux.title}

41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 |
50 | {RESX.modal.ux.warning} 51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 | } -------------------------------------------------------------------------------- /src/client/nav/nav.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .nav { 4 | padding-top: 1rem; 5 | display: flex; 6 | flex-wrap: nowrap; 7 | flex-direction: column; 8 | align-content: center; 9 | background-color: $app-clr-2; 10 | height: 100%; 11 | width: $width-nav + 10px; 12 | overflow: hidden; 13 | min-height: 720px; 14 | button { 15 | margin-bottom: $sm-gutter; 16 | width: 100%; 17 | } 18 | 19 | hr { 20 | margin: 0 0 9px 0; 21 | border-bottom: 1px solid $app-clr-blue-1; 22 | width: 100%; 23 | } 24 | 25 | .nav-links { 26 | height: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: space-between; 30 | align-items: center; 31 | padding-bottom: $xs-gutter; 32 | } 33 | 34 | .nav-items { 35 | display: flex; 36 | flex-direction: column; 37 | a { 38 | text-decoration: none; 39 | } 40 | width: 100%; 41 | padding: 0 10px; 42 | } 43 | 44 | .nav-items button div { 45 | font-size: 0.5rem; 46 | } 47 | 48 | .container { 49 | text-align: center; 50 | padding: 0; 51 | margin-bottom: $sm-gutter; 52 | color: $app-clr-blue-4; 53 | } 54 | } 55 | 56 | .nav-active { 57 | background-color: $app-clr-1; 58 | } 59 | -------------------------------------------------------------------------------- /src/client/nav/nav.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./nav.scss')); 3 | 4 | import * as React from 'react'; 5 | import { DeviceContext } from '../context/deviceContext'; 6 | import { RESX } from '../strings'; 7 | import { NavLink, useRouteMatch } from 'react-router-dom' 8 | 9 | export function Nav({ actions }) { 10 | 11 | const deviceContext: any = React.useContext(DeviceContext); 12 | let match = useRouteMatch("/:content"); 13 | 14 | return
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | {deviceContext.ui.container ?
: null} 36 | 37 |
38 |
39 |
40 | } -------------------------------------------------------------------------------- /src/client/selector/selector.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .selector-container { 4 | background-color: $app-clr-1; 5 | width: $width-selector; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | align-content: center; 10 | } 11 | 12 | .selector-container-header { 13 | flex-shrink: 0; 14 | display: flex; 15 | align-content: center; 16 | justify-content: space-between; 17 | height: $title-height; 18 | margin: 0 0.5rem 0 1rem; 19 | } 20 | 21 | .selector-container-body { 22 | width: inherit; 23 | text-align: center; 24 | width: 100%; 25 | flex-grow: 1; 26 | min-height: 0; 27 | height: 100%; 28 | overflow-y: auto; 29 | padding: 0 $md-gutter $md-gutter $md-gutter; 30 | } 31 | 32 | .selector-toggle { 33 | margin: $sm-gutter 0; 34 | border: none; 35 | border-radius: 2px; 36 | background-color: transparent; 37 | cursor: pointer; 38 | color: $app-white; 39 | } 40 | 41 | .selector-toggle:focus { 42 | outline: none; 43 | box-shadow: 0 0 2px $app-clr-4; 44 | } 45 | -------------------------------------------------------------------------------- /src/client/selector/selector.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./selector.scss')); 3 | 4 | import * as React from 'react'; 5 | import { SelectorCard } from './selectorCard'; 6 | import { DeviceContext } from '../context/deviceContext'; 7 | import { AppContext } from '../context/appContext'; 8 | import { ControlContext } from '../context/controlContext'; 9 | import { RESX } from '../strings'; 10 | import { decodeModuleKey } from '../ui/utilities'; 11 | import { useRouteMatch } from 'react-router-dom' 12 | 13 | export const Selector: React.FunctionComponent = () => { 14 | 15 | const deviceContext: any = React.useContext(DeviceContext); 16 | const appContext: any = React.useContext(AppContext); 17 | const match: any = useRouteMatch("/devices/:routeDeviceId"); 18 | const routeDeviceId = match && match.params.routeDeviceId && match.params.routeDeviceId 19 | 20 | return
21 |
22 | {deviceContext.devices.length === 0 ? null : <> 23 |
{RESX.selector.title}
24 | 27 | 28 | } 29 |
30 |
31 | {deviceContext.devices.map((item: any, index: number) => { 32 | const decoded = decodeModuleKey(routeDeviceId || '') 33 | const active = routeDeviceId && routeDeviceId === item._id || item.configuration._kind === 'edge' && decoded && decoded.deviceId === item._id || false 34 | const expanded = item.configuration._kind === 'template' ? false : appContext.selectorExpand; 35 | return item.configuration._kind === 'module' || item.configuration._kind === 'leafDevice' ? null : 36 | 37 | {(state: any) => ( 38 | 39 | )} 40 | 41 | })} 42 | {deviceContext.devices.length === 0 ? <>{RESX.selector.empty[0]} {RESX.selector.empty[1]} : ''} 43 |
44 |
45 | } 46 | -------------------------------------------------------------------------------- /src/client/selector/selectorCard.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | $card-width: 205px; 4 | 5 | .selector-card-container { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: flex-start; 9 | a { 10 | text-decoration: none; 11 | } 12 | } 13 | 14 | .selector-card-container:not(:last-child) { 15 | margin-bottom: $sm-gutter; 16 | } 17 | 18 | .expander { 19 | cursor: pointer; 20 | background-color: $app-clr-2; 21 | border-left: 2px solid $app-clr-2; 22 | border-top: 2px solid $app-clr-2; 23 | border-right: 2px solid $app-clr-2; 24 | border-bottom: 0; 25 | color: $app-white; 26 | border-radius: $border-radius $border-radius 0 0; 27 | margin: 0; 28 | font-size: 0.4rem; 29 | padding: 0.1rem; 30 | height: 0.8rem; 31 | line-height: 0.3rem; 32 | width: $card-width; 33 | i { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | } 38 | 39 | .expander:focus { 40 | outline: none; 41 | box-shadow: 0 0 2px $app-clr-4; 42 | } 43 | 44 | /* --- */ 45 | 46 | .selector-card { 47 | color: $app-white; 48 | background-color: transparent; 49 | overflow-wrap: break-word; 50 | border: 2px solid $app-clr-2; 51 | border-radius: 0 0 $border-radius $border-radius; 52 | width: $card-width; 53 | h4, 54 | h5 { 55 | font-size: 1.2rem; 56 | color: $app-clr-yellow-1; 57 | } 58 | h5 { 59 | font-size: 1rem; 60 | } 61 | margin-top: -3px; 62 | } 63 | 64 | /* --- */ 65 | 66 | .selector-card-expanded { 67 | text-align: center; 68 | padding: $sm-gutter; 69 | overflow: hidden; 70 | 71 | strong { 72 | font-weight: 400; 73 | } 74 | 75 | .module-count { 76 | color: $app-clr-blue-2; 77 | font-size: 0.75rem; 78 | padding-bottom: 4px; 79 | } 80 | } 81 | 82 | .selector-card-expanded:focus { 83 | outline: none; 84 | box-shadow: 0 0 2px $app-clr-4; 85 | } 86 | 87 | /* --- */ 88 | 89 | .selector-card-mini { 90 | text-align: center; 91 | padding: $sm-gutter; 92 | overflow: hidden; 93 | 94 | display: flex; 95 | align-items: center; 96 | justify-content: space-between; 97 | 98 | div:first-child { 99 | display: flex; 100 | align-items: center; 101 | overflow: hidden; 102 | } 103 | 104 | h5 { 105 | margin: 0; 106 | margin-left: $sm-gutter; 107 | white-space: nowrap; 108 | overflow: hidden; 109 | text-overflow: ellipsis; 110 | } 111 | } 112 | 113 | .selector-card-mini:focus { 114 | outline: none; 115 | box-shadow: 0 0 2px $app-clr-4; 116 | } 117 | 118 | /* --- */ 119 | 120 | .selector-card-spinner { 121 | padding: $sm-gutter; 122 | height: 3.25rem; 123 | } 124 | 125 | .selector-card-spinner .leaf-msg { 126 | font-size: 0.9rem; 127 | font-weight: 1000; 128 | text-transform: uppercase; 129 | } 130 | 131 | .selector-card:hover { 132 | cursor: pointer; 133 | background-color: $app-clr-blue-1; 134 | color: $app-white; 135 | } 136 | 137 | .selector-card-active { 138 | background-color: $app-clr-0; 139 | } 140 | 141 | .selector-card-active-edge { 142 | background-color: $app-clr-0; 143 | } 144 | 145 | /* --- */ 146 | 147 | .control { 148 | font-size: 0.8rem; 149 | padding: 0 $xs-gutter; 150 | } 151 | .control-OFF { 152 | color: $app-clr-4; 153 | } 154 | .control-DELAY { 155 | color: $app-clr-purple-1; 156 | } 157 | .control-ON { 158 | color: $app-clr-blue-1; 159 | } 160 | .control-TRYING { 161 | color: $app-clr-orange-1; 162 | } 163 | .control-SUCCESS { 164 | color: $app-clr-green-1; 165 | } 166 | .control-INIT { 167 | color: $app-clr-blue-5; 168 | } 169 | .control-CONNECTED { 170 | color: $app-clr-green-2; 171 | } 172 | .control-ERROR { 173 | color: $app-clr-red-1; 174 | } 175 | -------------------------------------------------------------------------------- /src/client/selector/selectorCard.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./selectorCard.scss')); 3 | 4 | import * as React from 'react'; 5 | import { DeviceContext } from '../context/deviceContext'; 6 | import { RESX } from '../strings'; 7 | import { controlEvents, decodeModuleKey } from '../ui/utilities'; 8 | import { Link } from 'react-router-dom' 9 | 10 | export const SelectorCard: React.FunctionComponent = ({ exp, index, active, device, control }) => { 11 | 12 | const deviceContext: any = React.useContext(DeviceContext); 13 | const [expanded, setExpanded] = React.useState(exp); 14 | 15 | React.useEffect(() => { 16 | setExpanded(exp); 17 | }, [exp]); 18 | 19 | let leafsRunning = false; 20 | const runningEvent = control && control[device._id] ? control[device._id][2] : controlEvents.OFF; 21 | const kind = device.configuration._kind 22 | const selected = kind === 'edge' ? 'selector-card-active-edge' : 'selector-card-active'; 23 | 24 | // if the edge device is switched off, but leaf devices are still running, change the selector card 25 | if (runningEvent === controlEvents.OFF && device.configuration.leafDevices) { 26 | for (const index in device.configuration.leafDevices) { 27 | const leafDeviceId = device.configuration.leafDevices[index]; 28 | if (control[leafDeviceId] && control[leafDeviceId][2] != controlEvents.OFF) { 29 | leafsRunning = true; 30 | break; 31 | } 32 | } 33 | } 34 | 35 | const childCountModules = device.configuration.modules && device.configuration.modules.length || 0 36 | const childCountDevices = device.configuration.leafDevices && device.configuration.leafDevices.length || 0 37 | 38 | return
39 | 40 | {expanded ? 41 | 42 | 75 | 76 | : 77 | 78 | 87 | 88 | } 89 |
90 | } -------------------------------------------------------------------------------- /src/client/shell/console.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .shell-console { 4 | background-color: $app-black; 5 | color: limegreen; 6 | height: 100%; 7 | font-size: 0.8rem; 8 | display: flex; 9 | padding-left: 1.7rem; 10 | overflow-y: auto; 11 | 12 | .console-pause { 13 | position: fixed; 14 | left: 0.6rem; 15 | cursor: pointer; 16 | } 17 | 18 | .console-erase { 19 | position: fixed; 20 | font-size: 1rem; 21 | left: 0.6rem; 22 | top: 1rem; 23 | cursor: pointer; 24 | } 25 | 26 | .console-line { 27 | cursor: pointer; 28 | } 29 | 30 | .console-line:hover { 31 | background-color: $app-clr-2; 32 | } 33 | 34 | .ellipsis { 35 | width: calc(100vw - 2.9rem); 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/shell/console.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./console.scss')); 3 | import { Endpoint } from '../context/endpoint'; 4 | 5 | import * as React from 'react'; 6 | 7 | import { Modal } from '../modals/modal'; 8 | import { ConsoleInspector } from '../modals/consoleInspector'; 9 | import { RESX } from '../strings'; 10 | 11 | interface State { 12 | dialog: any; 13 | data: any; 14 | paused: boolean 15 | } 16 | 17 | interface Action { 18 | type: string; 19 | payload: any; 20 | } 21 | 22 | const reducer = (state: State, action: Action) => { 23 | 24 | const item = action.type.split('-')[1] 25 | const newData = Object.assign({}, state.data); 26 | 27 | switch (action.type) { 28 | case 'lines-add': 29 | if (state.paused) { return state }; 30 | // reverse the incoming list and unshift the array with new messages 31 | const newList = action.payload.data.split('\n').reverse().concat(newData.lines); 32 | const trim = newList.length - 1000; 33 | for (let i = 0; i < trim; i++) { newList.pop(); }; 34 | return { ...state, data: { lines: newList } }; 35 | case 'lines-clear': 36 | return { ...state, data: { lines: [] } }; 37 | case 'toggle-pause': 38 | return { ...state, paused: !state.paused }; 39 | case 'dialog-show': 40 | // create a small window of events to scroll between 41 | const i = action.payload.index; 42 | const start = newData.lines.slice(i - 50, i); 43 | const newIndex = start.slice(0).length; 44 | const end = newData.lines.slice(i, i + 50); 45 | return { ...state, dialog: { showDialog: true, dialogIndex: newIndex, dialogLines: start.concat(end) } }; 46 | case 'dialog-close': 47 | return { ...state, dialog: { showDialog: false, dialogIndex: -1, dialogLines: [] } }; 48 | default: 49 | return state; 50 | } 51 | } 52 | 53 | export const Console: React.FunctionComponent = () => { 54 | 55 | const [state, dispatch] = React.useReducer(reducer, { data: { lines: [], dialogIndex: -1, dialogLines: [] }, dialog: { showDialog: false }, paused: false }); 56 | 57 | let eventSource = null; 58 | 59 | React.useEffect(() => { 60 | eventSource = new EventSource(`${Endpoint.getEndpoint()}api/events/message`) 61 | eventSource.onmessage = ((e) => { 62 | dispatch({ type: 'lines-add', payload: { data: e.data } }) 63 | }); 64 | }, []); 65 | 66 | const closeDialog = () => { dispatch({ type: 'dialog-close', payload: null }) } 67 | 68 | return <> 69 |
70 | { dispatch({ type: 'toggle-pause', payload: null }) }}> 71 | { dispatch({ type: 'lines-clear', payload: null }) }}> 72 |
73 | {state.data.lines.length > 0 && state.data.lines.map((element, index) => { 74 | const i = index; 75 | return
{ dispatch({ type: 'dialog-show', payload: { index: i } }) }}>{element}
76 | })} 77 |
78 |
79 | 80 | {state.dialog.showDialog ?
: null} 81 | 82 | } -------------------------------------------------------------------------------- /src/client/shell/shell.scss: -------------------------------------------------------------------------------- 1 | @import "../const.scss"; 2 | 3 | .shell { 4 | width: 100%; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | flex-wrap: nowrap; 9 | } 10 | 11 | .shell-content-container { 12 | height: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .shell-banner { 18 | flex-shrink: 0; 19 | border-bottom: 1px solid $app-clr-3; 20 | box-shadow: 0px 2px 3px 0px $app-shadow; 21 | background-color: $app-clr-blue-3; 22 | height: 1.6rem; 23 | width: 100%; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | overflow: hidden; 28 | font-size: 0.75rem; 29 | color: $app-white; 30 | display: flex; 31 | justify-content: space-between; 32 | padding: 0 $sm-gutter; 33 | div { 34 | overflow: hidden; 35 | white-space: nowrap; 36 | text-overflow: ellipsis; 37 | } 38 | div:not(:first-child) { 39 | margin-left: 2rem; 40 | } 41 | } 42 | 43 | .shell-content { 44 | flex-grow: 1; 45 | background-color: $app-clr-0; 46 | color: $app-white; 47 | width: 100%; 48 | height: 100%; 49 | overflow: hidden; 50 | 51 | display: flex; 52 | } 53 | 54 | .shell-content-selector { 55 | border-right: $border-thicker solid $app-clr-3; 56 | } 57 | 58 | .shell-content-root { 59 | margin-top: 2rem; 60 | padding: 1rem; 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: space-between; 64 | div:last-child { 65 | margin-top: 0.5rem; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/client/shell/shell.tsx: -------------------------------------------------------------------------------- 1 | var classNames = require('classnames'); 2 | const cx = classNames.bind(require('./shell.scss')); 3 | 4 | import * as React from 'react'; 5 | import { Route } from 'react-router-dom' 6 | import { Nav } from '../nav/nav' 7 | import { Devices } from '../devices/devices'; 8 | import { Dashboard } from '../dashboard/dashboard'; 9 | import { Console } from './console'; 10 | import { DeviceContext } from '../context/deviceContext'; 11 | import { RESX } from '../strings'; 12 | import { AppContext } from '../context/appContext'; 13 | import { Endpoint } from '../context/endpoint'; 14 | import { Modal } from '../modals/modal'; 15 | import { Help } from '../modals/help'; 16 | import { AddDevice } from '../modals/addDevice'; 17 | import { Simulation } from '../modals/simulation'; 18 | import { Ux } from '../modals/ux'; 19 | import { ModalConfirm } from '../modals/modalConfirm'; 20 | import { Connect } from '../modals/connect'; 21 | import { Bulk } from '../modals/bulk'; 22 | 23 | import SplitterLayout from 'react-splitter-layout'; 24 | import 'react-splitter-layout/lib/index.css'; 25 | 26 | const minSize = 64; 27 | const height = 28; 28 | 29 | export const Shell: React.FunctionComponent = () => { 30 | 31 | const deviceContext: any = React.useContext(DeviceContext); 32 | const appContext: any = React.useContext(AppContext); 33 | const ep = Endpoint.getEndpoint() === '/' ? RESX.banner.local : Endpoint.getEndpoint(); 34 | 35 | const [showAdd, toggleAdd] = React.useState(false); 36 | const [showSimulation, toggleSimulation] = React.useState(false); 37 | const [showHelp, toggleHelp] = React.useState(false); 38 | const [showUx, toggleUx] = React.useState(false); 39 | const [showReset, toggleReset] = React.useState(false); 40 | const [showCentral, toggleCentral] = React.useState(false); 41 | const [showBulk, toggleBulk] = React.useState(false); 42 | 43 | const menuAdd = () => { toggleAdd(!showAdd); } 44 | const menuHelp = () => { toggleHelp(!showHelp); } 45 | const menuSimulation = () => { toggleSimulation(!showSimulation); } 46 | const menuUx = () => { toggleUx(!showUx); } 47 | const menuStartAll = () => { deviceContext.startAllDevices(); } 48 | const menuStopAll = () => { deviceContext.stopAllDevices(); } 49 | const menuReset = () => { toggleReset(!showReset); } 50 | const menuCentral = () => { toggleCentral(!showCentral); } 51 | const menuBulk = () => { toggleBulk(!showBulk); } 52 | 53 | const deleteDialogAction = (result) => { 54 | if (result === "Yes") { 55 | //TODO: refactor 56 | appContext.clearDirty(); 57 | deviceContext.reset(); 58 | } 59 | toggleReset(false); 60 | } 61 | 62 | const deleteModalConfig = { 63 | title: RESX.modal.delete_title, 64 | message: RESX.modal.delete_all, 65 | options: { 66 | buttons: [RESX.modal.YES, RESX.modal.NO], 67 | handler: deleteDialogAction, 68 | close: menuReset 69 | } 70 | } 71 | 72 | const nav = { menuAdd, menuHelp, menuSimulation, menuUx, menuStartAll, menuStopAll, menuReset, menuCentral, menuBulk } 73 | 74 | return
75 | 76 | 77 |
78 |
79 |
{`${RESX.banner.connect} ${ep}`}
80 | {deviceContext.ui && deviceContext.ui.edge && deviceContext.ui.edge.deviceId && deviceContext.ui.edge.moduleId ? 81 |
{`${RESX.banner.edge[0]} ${RESX.banner.edge[1]} '${deviceContext.ui.edge.moduleId}' ${RESX.banner.edge[2]} '${deviceContext.ui.edge.deviceId}'`}
: null} 82 |
83 |
84 |
85 |
{RESX.shell.title}
86 | 87 | 88 |
89 | {showHelp ?
: null} 90 | {showAdd ?
: null} 91 | {showSimulation ?
: null} 92 | {showUx ?
: null} 93 | {showReset ?
: null} 94 | {showCentral ?
: null} 95 | {showBulk ?
: null} 96 |
97 |
98 |
99 | } -------------------------------------------------------------------------------- /src/client/ui/controls.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as JS from 'jsoneditor'; 3 | import 'jsoneditor/dist/jsoneditor.css'; 4 | 5 | export const Combo: React.FunctionComponent = ({ name, value, items, cls, onChange }) => { 6 | return 11 | } 12 | 13 | export const Json: React.FunctionComponent = ({ json, cb, err }) => { 14 | 15 | let editor = null; 16 | const [payload, setPayload] = React.useState(json || {}) 17 | 18 | const callback = () => { 19 | let text = ''; 20 | try { 21 | text = this.editor.get() 22 | cb(text); 23 | } catch { 24 | err(); 25 | } 26 | } 27 | 28 | const options = { 29 | mode: 'code', 30 | mainMenuBar: false, 31 | navigationBar: false, 32 | statusBar: false, 33 | onChange: callback 34 | } 35 | 36 | React.useEffect(() => { 37 | const container = document.getElementById("jsoneditor") 38 | this.editor = new JS(container, options) 39 | this.editor.set(payload) 40 | }, []) 41 | 42 | React.useEffect(() => { 43 | this.editor.set(json || {}) 44 | }, [json]) 45 | 46 | return
47 | } -------------------------------------------------------------------------------- /src/client/ui/utilities.tsx: -------------------------------------------------------------------------------- 1 | export function decodeModuleKey(key: string): any { 2 | const r = new RegExp(`\<(.*)\>(.*)?`) 3 | if (key === undefined || key === null) { return null; } 4 | const m = key.match(r); 5 | if (!m || m.length != 3) { return null; } 6 | return { deviceId: m[1], moduleId: m[2] }; 7 | } 8 | 9 | export const controlEvents = { 10 | ON: 'ON', 11 | OFF: 'OFF', 12 | INIT: 'INIT', 13 | SUCCESS: 'SUCCESS', 14 | CONNECTED: 'CONNECTED', 15 | TRYING: 'TRYING', 16 | ERROR: 'ERROR', 17 | DELAY: 'DELAY' 18 | } -------------------------------------------------------------------------------- /src/server/api/bulk.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { DeviceStore } from '../store/deviceStore' 3 | import { DCMtoMockDevice } from '../core/templates'; 4 | 5 | export default function (deviceStore: DeviceStore) { 6 | let api = Router(); 7 | 8 | api.post('/properties', function (req, res) { 9 | const caps = deviceStore.getCommonCapabilities(req.body.devices, req.body.allDevices); 10 | res.json({ devices: req.body.devices, capabilities: caps }); 11 | res.end(); 12 | }); 13 | 14 | api.post('/update', function (req, res) { 15 | const caps = deviceStore.setCommonCapabilities(req.body.payload); 16 | res.json({ devices: deviceStore.getListOfItems() }); 17 | res.end(); 18 | }); 19 | 20 | return api; 21 | } -------------------------------------------------------------------------------- /src/server/api/connect.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import axios from 'axios'; 3 | import { DeviceStore } from '../store/deviceStore'; 4 | import { ServerSideMessageService } from '../core/messageService'; 5 | import { Device } from '../interfaces/device'; 6 | import { DCMtoMockDevice } from '../core/templates'; 7 | import { Config } from '../config'; 8 | import { isArray } from 'lodash'; 9 | 10 | interface Cache { 11 | templates: []; 12 | } 13 | 14 | function scrubError(err) { 15 | if (!err.response) { return err.message; } 16 | const errors = err.response.data.error.message.match(/(.*?)\bYou can contact support.*\bGMT./); 17 | return isArray(errors) && errors.length > 0 ? errors[1] : err.message; 18 | } 19 | 20 | export default function (deviceStore: DeviceStore, ms: ServerSideMessageService) { 21 | let api = Router(); 22 | 23 | let cache: Cache = { templates: [] }; 24 | 25 | api.post('/templates', function (req, res, next) { 26 | const { appUrl, token } = req.body; 27 | 28 | if (Config.CACHE_CENTRAL_TEMPLATES && cache.templates.length > 0) { res.json(cache.templates); return; } 29 | 30 | axios.get(`https://${appUrl}/api/preview/deviceTemplates`, { 31 | headers: { 32 | Authorization: token 33 | } 34 | }) 35 | .then((res2) => { 36 | cache.templates = res2.data && res2.data.value || []; 37 | res.json(cache.templates); 38 | }) 39 | .catch((err) => { 40 | res.status(500).send(scrubError(err)); 41 | }) 42 | .finally(() => { 43 | res.end(); 44 | }) 45 | }); 46 | 47 | api.post('/create', async function (req, res, next) { 48 | const { appUrl, token, id, deviceId, deviceUnique } = req.body; 49 | 50 | const authHeader = { Authorization: token } 51 | const deviceHeader = deviceUnique ? Object.assign({}, authHeader, { "If-None-Match": "*" }) : authHeader 52 | 53 | let dcm = null; 54 | axios.put(`https://${appUrl}/api/preview/devices/${deviceId}`, { instanceOf: id }, { headers: deviceHeader }) 55 | .then(() => { 56 | return axios.get(`https://${appUrl}/api/preview/deviceTemplates/${id}`, { headers: deviceHeader }); 57 | }) 58 | .then((response) => { 59 | dcm = response.data.capabilityModel; 60 | return axios.get(`https://${appUrl}/api/preview/devices/${deviceId}/credentials`, { headers: deviceHeader }); 61 | }) 62 | .then((response) => { 63 | const deviceConfiguration: any = { 64 | "_kind": "dps", 65 | "deviceId": deviceId, 66 | "mockDeviceName": deviceId, 67 | "scopeId": response.data.idScope, 68 | "dpsPayload": { 69 | "iotcModelId": id 70 | }, 71 | "sasKey": response.data.symmetricKey.primaryKey, 72 | "isMasterKey": false, 73 | "capabilityModel": dcm, 74 | "capabilityUrn": id, 75 | "centralAdded": true 76 | } 77 | 78 | let d: Device = new Device(); 79 | d._id = deviceId; 80 | d.configuration = deviceConfiguration; 81 | d.configuration.deviceId = deviceId; 82 | deviceStore.addDevice(d); 83 | 84 | if (dcm) { DCMtoMockDevice(deviceStore, d); } 85 | 86 | ms.sendAsStateChange({ 'devices': 'loaded' }) 87 | 88 | deviceStore.startDevice(d); 89 | res.sendStatus(200); 90 | }) 91 | .catch((err) => { 92 | res.status(500).send(scrubError(err)); 93 | }) 94 | .finally(() => { 95 | res.end(); 96 | }); 97 | }); 98 | 99 | return api; 100 | } -------------------------------------------------------------------------------- /src/server/api/devices.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { DeviceStore } from '../store/deviceStore' 3 | 4 | export default function (deviceStore: DeviceStore) { 5 | let api = Router(); 6 | 7 | api.get('/', function (req, res, next) { 8 | res.json(deviceStore.getListOfItems()); 9 | }); 10 | 11 | //TODO: this needs push to trap errors 12 | api.get('/start', function (req, res, next) { 13 | deviceStore.startAll(); 14 | res.json(deviceStore.getListOfItems()); 15 | }); 16 | 17 | api.get('/stop', function (req, res, next) { 18 | deviceStore.stopAll(); 19 | res.json(deviceStore.getListOfItems()); 20 | }); 21 | 22 | api.get('/reset', function (req, res, next) { 23 | deviceStore.reset(); 24 | res.json(deviceStore.getListOfItems()); 25 | }); 26 | 27 | return api; 28 | } -------------------------------------------------------------------------------- /src/server/api/dtdl.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { DtdlStore } from '../store/dtdlStore' 3 | 4 | export default function (dtdlStore: DtdlStore) { 5 | let api = Router(); 6 | 7 | api.get('/:name', function (req, res, next) { 8 | res.json(dtdlStore.getDtdl(req.params.name)); 9 | res.end(); 10 | }); 11 | 12 | api.get('/', function (req, res, next) { 13 | res.json(dtdlStore.getListOfItems()); 14 | res.end(); 15 | }); 16 | 17 | return api; 18 | } -------------------------------------------------------------------------------- /src/server/api/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | export default function (plugIns: any) { 4 | let api = Router(); 5 | 6 | api.get('/', function (req, res, next) { 7 | const items = []; 8 | for (const p in plugIns) { 9 | items.push(p); 10 | } 11 | res.json(items); 12 | }); 13 | 14 | return api; 15 | } -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | const isDocker = require('is-docker'); 2 | import { Router } from 'express'; 3 | import * as uuidV4 from 'uuid/v4'; 4 | import * as fs from 'fs'; 5 | import { REPORTING_MODES } from '../config'; 6 | var path = require('path'); 7 | var QrCode = require('qrcode-reader'); 8 | var Jimp = require("jimp"); 9 | 10 | export default function (dialog, app, globalContext, ms) { 11 | let api = Router(); 12 | 13 | api.get('/ping', function (req, res) { 14 | res.status(200); 15 | }); 16 | 17 | api.get('/ui', function (req, res) { 18 | res.json({ 19 | container: isDocker(), 20 | edge: { 21 | deviceId: globalContext.IOTEDGE_DEVICEID, 22 | moduleId: globalContext.IOTEDGE_MODULEID 23 | }, 24 | latest: globalContext.LATEST_VERSION 25 | }); 26 | }); 27 | 28 | api.get('/id', function (req, res) { 29 | res.status(200).send(uuidV4()).end(); 30 | }); 31 | 32 | api.get('/help', function (req, res) { 33 | res.status(200).send(fs.readFileSync(path.resolve(__dirname + '/../../../static/HELP.md'), { encoding: 'utf-8' })).end(); 34 | }) 35 | 36 | api.post('/setmode/:mode', function (req, res) { 37 | const mode = req.params.mode; 38 | switch (mode) { 39 | case "ux": 40 | globalContext.OPERATION_MODE = REPORTING_MODES.UX; 41 | break; 42 | case "server": 43 | globalContext.OPERATION_MODE = REPORTING_MODES.SERVER; 44 | break; 45 | case "mixed": 46 | globalContext.OPERATION_MODE = REPORTING_MODES.MIXED; 47 | break; 48 | default: 49 | globalContext.OPERATION_MODE = REPORTING_MODES.UX; 50 | } 51 | ms.sendConsoleUpdate(`DATA BACKEND CHANGED TO [${globalContext.OPERATION_MODE}] ${req.body.serverEndpoint}`); 52 | res.status(200).send(globalContext.OPERATION_MODE.toString()).end(); 53 | }); 54 | 55 | api.get('/openDialog', function (req, res, next) { 56 | dialog.showOpenDialog({ 57 | properties: ['openFile'], 58 | filters: [ 59 | { name: 'JSON', extensions: ['json'] }, 60 | { name: 'All Files', extensions: ['*'] } 61 | ] 62 | }).then((result: any) => { 63 | if (result.canceled) { res.status(444).end(); return; } 64 | if (result.filePaths && result.filePaths.length > 0) { 65 | const fileNamePath = result.filePaths[0]; 66 | fs.readFile(fileNamePath, 'utf-8', (error: any, data: any) => { 67 | res.json(JSON.parse(data)).status(200).end(); 68 | }) 69 | } 70 | }).catch((error: any) => { 71 | console.error(error); 72 | res.status(500).end(); 73 | }); 74 | }); 75 | 76 | api.post('/saveDialog', function (req, res, next) { 77 | dialog.showSaveDialog() 78 | .then((path: any) => { 79 | if (path.canceled) { res.status(444).end(); return; } 80 | if (path.filePath === "") { res.status(204).end(); return; } 81 | if (!path.filePath.toLocaleLowerCase().endsWith('.json')) { path.filePath += '.json'; } 82 | 83 | fs.writeFile(path.filePath, JSON.stringify(req.body, null, 2), 'utf8', (error) => { 84 | if (error) { res.status(500).end(); } 85 | else { res.status(200).end(); } 86 | }) 87 | }).catch((error: any) => { 88 | console.error(error); 89 | res.status(500).end(); 90 | }); 91 | }); 92 | 93 | api.get('/qrcode', async function (req, res, next) { 94 | dialog.showOpenDialog({ 95 | properties: ['openFile'], 96 | filters: [ 97 | { name: 'PNG', extensions: ['png'] }, 98 | { name: 'All Files', extensions: ['*'] } 99 | ] 100 | }).then((result: any) => { 101 | if (result.canceled) { res.status(444).end(); return; } 102 | if (result.filePaths && result.filePaths.length > 0) { 103 | const fileNamePath = result.filePaths[0]; 104 | 105 | var buffer = fs.readFileSync(fileNamePath); 106 | Jimp.read(buffer, function (err, image) { 107 | if (err) { 108 | console.error(err); 109 | res.status(500).end(); 110 | return; 111 | } 112 | var qr = new QrCode(); 113 | qr.callback = function (err, value) { 114 | if (err) { 115 | console.error(err); 116 | res.status(500).end(); 117 | return; 118 | } 119 | res.json({ code: value.result }).status(200).end(); 120 | }; 121 | qr.decode(image.bitmap); 122 | }); 123 | res.end 124 | } 125 | }).catch((error: any) => { 126 | console.error(error); 127 | res.status(500).end(); 128 | }); 129 | }); 130 | 131 | return api; 132 | } -------------------------------------------------------------------------------- /src/server/api/sensors.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { SensorStore } from '../store/sensorStore' 3 | 4 | export default function (sensorStore: SensorStore) { 5 | let api = Router(); 6 | 7 | api.get('/:type', function (req, res, next) { 8 | var type = req.params.type; 9 | res.json(sensorStore.getNewSensor(type)); 10 | res.end(); 11 | }); 12 | 13 | api.get('/', function (req, res, next) { 14 | res.json(sensorStore.getListOfItems()); 15 | res.end(); 16 | }); 17 | 18 | return api; 19 | } -------------------------------------------------------------------------------- /src/server/api/server.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { DeviceStore } from '../store/deviceStore' 3 | import { IotHub } from '../core/iotHub'; 4 | 5 | export default function (deviceStore: DeviceStore) { 6 | let api = Router(); 7 | 8 | api.post('/list', function (req, res) { 9 | var body = req.body; 10 | 11 | IotHub.GetDevices(body.connectionString) 12 | .then((deviceList: any) => { 13 | res.json(deviceList); 14 | res.status(200).end(); 15 | }) 16 | .catch((err: any) => { 17 | res.status(500).end(err); 18 | }) 19 | }); 20 | 21 | api.post('/:id/delete', function (req, res) { 22 | var body = req.body; 23 | var deviceId = req.params.id; 24 | 25 | IotHub.DeleteDevice(body.connectionString, deviceId) 26 | .then((deviceList: any) => { 27 | res.json(deviceList); 28 | res.status(200).end(); 29 | }) 30 | .catch((err: any) => { 31 | res.status(500).end(err); 32 | }) 33 | }); 34 | 35 | api.post('/:id/twinRead', function (req, res) { 36 | var body = req.body; 37 | var deviceId = req.params.id; 38 | 39 | IotHub.GetTwin(body.connectionString, deviceId) 40 | .then((twin: any) => { 41 | res.json(twin); 42 | res.status(200).end(); 43 | }) 44 | .catch((err: any) => { 45 | res.status(500).end(err); 46 | }) 47 | }); 48 | 49 | api.post('/:id/twinWrite', function (req, res) { 50 | var body = req.body; 51 | var deviceId = req.params.id; 52 | 53 | IotHub.WriteTwin(body.connectionString, body.properties, deviceId) 54 | .then((twin: any) => { 55 | res.json(twin); 56 | res.status(200).end(); 57 | }) 58 | .catch((err: any) => { 59 | res.status(500).end(err); 60 | }) 61 | }); 62 | 63 | return api; 64 | } -------------------------------------------------------------------------------- /src/server/api/simulation.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as Utils from '../core/utils'; 3 | 4 | export default function (deviceStore, simulationStore) { 5 | let api = Router(); 6 | 7 | api.get('/:key', function (req, res) { 8 | res.send(simulationStore.get()[req.params.key || '']); 9 | res.end(); 10 | }); 11 | 12 | api.get('/', function (req, res) { 13 | res.send(simulationStore.get()); 14 | res.end(); 15 | }); 16 | 17 | api.post('/', function (req, res) { 18 | try { 19 | let payload = req.body; 20 | deviceStore.stopAll(); 21 | simulationStore.set(payload.simulation); 22 | let devices = Object.assign({}, deviceStore.getListOfItems()); 23 | deviceStore.createFromArray(devices); 24 | res.json(deviceStore.getListOfItems()); 25 | res.end(); 26 | } 27 | catch (err) { 28 | res.status(500).send({ "message": "Cannot import this simulation data" }) 29 | res.end(); 30 | } 31 | }); 32 | 33 | return api; 34 | } -------------------------------------------------------------------------------- /src/server/api/state.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as Utils from '../core/utils'; 3 | 4 | export default function (deviceStore, simulationStore, ms) { 5 | let api = Router(); 6 | 7 | api.get('/', function (req, res) { 8 | const payload = { 9 | devices: JSON.parse(JSON.stringify(deviceStore.getListOfItems())), 10 | simulation: simulationStore.get() 11 | } 12 | res.json(payload); 13 | }); 14 | 15 | api.post('/', function (req, res) { 16 | try { 17 | let payload = req.body; 18 | deviceStore.stopAll(); 19 | if (payload.simulation) { simulationStore.set(payload.simulation); } 20 | deviceStore.init(); 21 | deviceStore.createFromArray(payload.devices); 22 | ms.sendAsStateChange({ 'devices': 'loaded' }) 23 | res.json(deviceStore.getListOfItems()); 24 | } 25 | catch (err) { 26 | res.status(500).send({ "message": "DATA ERROR: " + err.message }) 27 | } 28 | }); 29 | 30 | api.post('/merge', function (req, res) { 31 | try { 32 | let payload = req.body; 33 | deviceStore.stopAll(); 34 | let currentDevices = JSON.parse(JSON.stringify(deviceStore.getListOfItems())); 35 | payload.devices = currentDevices.concat(payload.devices); 36 | deviceStore.init(); 37 | deviceStore.createFromArray(payload.devices); 38 | ms.sendAsStateChange({ 'devices': 'loaded' }) 39 | res.json(deviceStore.getListOfItems()); 40 | } 41 | catch (err) { 42 | res.status(500).send({ "message": "Cannot merge this data" }) 43 | } 44 | }); 45 | 46 | return api; 47 | } -------------------------------------------------------------------------------- /src/server/api/template.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { DeviceStore } from '../store/deviceStore' 3 | import { DCMtoMockDevice } from '../core/templates'; 4 | 5 | export default function (deviceStore: DeviceStore) { 6 | let api = Router(); 7 | 8 | api.post('/reapply', function (req, res) { 9 | deviceStore.reapplyTemplate(req.body.payload.templateId, req.body.payload.devices, req.body.payload.all); 10 | res.json({ devices: deviceStore.getListOfItems() }); 11 | res.end(); 12 | }); 13 | 14 | return api; 15 | } -------------------------------------------------------------------------------- /src/server/config.ts: -------------------------------------------------------------------------------- 1 | export enum REPORTING_MODES { 2 | UX = "UX", 3 | SERVER = "SERVER", 4 | MIXED = "MIXED" 5 | } 6 | 7 | export const GLOBAL_CONTEXT = { 8 | OPERATION_MODE: REPORTING_MODES.UX, 9 | LATEST_VERSION: false, 10 | IOTEDGE_WORKLOADURI: process.env.IOTEDGE_WORKLOADURI, 11 | IOTEDGE_DEVICEID: process.env.IOTEDGE_DEVICEID, 12 | IOTEDGE_MODULEID: process.env.IOTEDGE_MODULEID, 13 | IOTEDGE_MODULEGENERATIONID: process.env.IOTEDGE_MODULEGENERATIONID, 14 | IOTEDGE_IOTHUBHOSTNAME: process.env.IOTEDGE_IOTHUBHOSTNAME, 15 | IOTEDGE_AUTHSCHEME: process.env.IOTEDGE_AUTHSCHEME 16 | }; 17 | 18 | export class Config { 19 | // app settings 20 | public static APP_PORT: string = '17456'; 21 | public static APP_HEIGHT: number = 767; 22 | public static APP_WIDTH: number = 1023; 23 | public static MAX_NUM_DEVICES: number = 3000; 24 | 25 | // reporting settings 26 | public static CONSOLE_LOGGING: boolean = true; 27 | public static CONTROL_LOGGING: boolean = true; 28 | public static PROPERTY_LOGGING: boolean = false; 29 | public static STATE_LOGGING: boolean = true; 30 | public static STATS_LOGGING: boolean = true; 31 | 32 | // dev settings 33 | public static NODE_MODE: boolean = false; 34 | public static WEBAPI_LOGGING: boolean = false; 35 | public static DEV_TOOLS: boolean = false; 36 | 37 | // cache settings 38 | public static CACHE_CENTRAL_TEMPLATES: boolean = false; 39 | } 40 | -------------------------------------------------------------------------------- /src/server/core/iotHub.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionString } from 'azure-iot-common'; 2 | import * as iothub from 'azure-iothub'; 3 | import * as uuidV4 from 'uuid/v4'; 4 | import * as crypto from 'crypto'; 5 | 6 | /* - NOT USED FEATURE - LEAVE FOR FUTURE */ 7 | export class IotHub { 8 | 9 | static CreateDevice(connectionString: string) { 10 | 11 | // Create a new device 12 | var device = { 13 | deviceId: uuidV4() 14 | }; 15 | 16 | return new Promise((resolve, reject) => { 17 | if (connectionString === '') { reject('Connection string for IoT Hub is empty.'); return; } 18 | try { 19 | var registry = iothub.Registry.fromConnectionString(connectionString); 20 | registry.create(device, function (err: any, deviceInfo: any, res: any) { 21 | if (err) { reject(err.toString()); } 22 | if (res) { 23 | console.log('-----> ' + Date.now() + ' DEVICE CREATE [' + res.statusCode + '] ' + res.statusMessage); 24 | if (deviceInfo) { 25 | resolve(deviceInfo); 26 | } 27 | else { 28 | reject('No DeviceInfo from device create. Check subscription'); 29 | } 30 | } 31 | else { 32 | reject('No response from device create. Check subscription'); 33 | } 34 | }) 35 | } 36 | catch (ex) { 37 | reject(ex.message); 38 | } 39 | }) 40 | } 41 | 42 | static GetDevices(connectionString: string) { 43 | 44 | return new Promise((resolve, reject) => { 45 | if (connectionString === '') { reject('Connection string for IoT Hub is empty.'); return; } 46 | try { 47 | let registry = iothub.Registry.fromConnectionString(connectionString); 48 | registry.list(function (err, deviceList) { 49 | if (err) { reject(err.toString()); } 50 | if (deviceList) { 51 | let cn = ConnectionString.parse(connectionString); 52 | let deviceList2 = [] 53 | deviceList.map((item: any) => { 54 | let o = item; 55 | o.connectionString = 'HostName=' + cn.HostName + ';DeviceId=' + item.deviceId + ';SharedAccessKey=' + item.authentication.SymmetricKey.primaryKey; 56 | deviceList2.push(o); 57 | }) 58 | resolve(deviceList2); 59 | } 60 | else { 61 | reject('No Devices found'); 62 | } 63 | }) 64 | } 65 | catch (ex) { 66 | reject(ex.message); 67 | } 68 | }) 69 | } 70 | 71 | static GetTwin(connectionString: string, deviceId: string) { 72 | 73 | return new Promise((resolve, reject) => { 74 | if (connectionString === '') { reject('Connection string for IoT Hub is empty.'); return; } 75 | try { 76 | let registry = iothub.Registry.fromConnectionString(connectionString); 77 | 78 | registry.getTwin(deviceId, function (err, twin) { 79 | if (err) { reject(err.toString()); } 80 | if (twin) { 81 | let payload = JSON.stringify(twin, null, 2); 82 | resolve(payload); 83 | } 84 | else { 85 | reject('Device not found'); 86 | } 87 | }) 88 | } 89 | catch (ex) { 90 | reject(ex.message); 91 | } 92 | }) 93 | } 94 | 95 | static WriteTwin(connectionString: string, properties: any, deviceId: string) { 96 | 97 | return new Promise((resolve, reject) => { 98 | if (connectionString === '') { reject('Connection string for IoT Hub is empty.'); return; } 99 | try { 100 | var etag = crypto.randomBytes(2).toString('hex'); 101 | var payload = { properties: { desired: properties } } 102 | 103 | let registry = iothub.Registry.fromConnectionString(connectionString); 104 | registry.updateTwin(deviceId, payload, etag, function (err, twin) { 105 | if (err) { reject(err.toString()); } 106 | if (twin) { 107 | let payload = JSON.stringify(twin, null, 2); 108 | resolve(payload); 109 | } 110 | else { 111 | reject('Device not found'); 112 | } 113 | }) 114 | } 115 | catch (ex) { 116 | reject(ex.message); 117 | } 118 | }) 119 | } 120 | 121 | static DeleteDevice(connectionString: string, deviceId: string) { 122 | 123 | return new Promise((resolve, reject) => { 124 | if (connectionString === '') { reject('Connection string for IoT Hub is empty.'); return; } 125 | try { 126 | let registry = iothub.Registry.fromConnectionString(connectionString); 127 | registry.delete(deviceId, function (err) { 128 | if (err) { reject(err.toString()); } 129 | registry.list(function (err2, deviceList) { 130 | if (err2) { reject(err2.toString()); } 131 | if (deviceList) { 132 | resolve(deviceList); 133 | } 134 | else { 135 | reject('No Devices found'); 136 | } 137 | }) 138 | }) 139 | } 140 | catch (ex) { 141 | reject(ex.message); 142 | } 143 | }) 144 | } 145 | } -------------------------------------------------------------------------------- /src/server/core/messageService.ts: -------------------------------------------------------------------------------- 1 | import { Config, GLOBAL_CONTEXT } from '../config'; 2 | import { REPORTING_MODES } from '../config'; 3 | 4 | export class ServerSideMessageService { 5 | 6 | private eventMessage: Array = []; 7 | private eventData = {}; 8 | private eventControl = {}; 9 | private eventState = {}; 10 | private eventStats = {}; 11 | private timers = {}; 12 | private messageCount = 1; 13 | private controlCount = 1; 14 | private statsCount = 1; 15 | 16 | end = (type: string) => { 17 | clearInterval(this.timers[type]); 18 | } 19 | 20 | endAll = () => { 21 | for (const timer in this.timers) { 22 | this.end(this.timers[timer]); 23 | } 24 | } 25 | 26 | clearState = () => { 27 | this.eventData = {}; 28 | this.eventControl = {}; 29 | this.eventState = {}; 30 | this.eventStats = {}; 31 | } 32 | 33 | sendConsoleUpdate(message: string) { 34 | if (Config.CONSOLE_LOGGING) { 35 | if (message.length > 0 && message[0] != '[') { message = ` ${message}`; } 36 | const msg = `[${new Date().toISOString()}]${message}`; 37 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.MIXED || GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { 38 | console.log(msg); 39 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { return; } 40 | } 41 | this.eventMessage.push(msg); 42 | } 43 | } 44 | 45 | sendAsLiveUpdate(group: string, payload: any) { 46 | if (Config.PROPERTY_LOGGING && Object.keys(payload).length > 0) { 47 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.MIXED || GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { 48 | console.log(`[${new Date().toISOString()}][LOG][PROPERTY REPORTING] ${JSON.stringify(payload)}`); 49 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { return; } 50 | } 51 | const o = this.eventData[group]; 52 | if (o == null) { this.eventData[group] = payload; } 53 | else { for (const key in payload) { this.eventData[group][key] = payload[key] } } 54 | } 55 | } 56 | 57 | sendAsControlPlane(payload: any) { 58 | if (Config.CONTROL_LOGGING) { 59 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.MIXED || GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { 60 | console.log(`[${new Date().toISOString()}][LOG][CONTROL PLANE REPORTING] ${JSON.stringify(payload)}`); 61 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { return; } 62 | } 63 | for (const key in payload) { this.eventControl[key] = payload[key] } 64 | } 65 | } 66 | 67 | sendAsStateChange(payload: any) { 68 | if (Config.STATE_LOGGING) { 69 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.MIXED || GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { 70 | console.log(`[${new Date().toISOString()}][LOG][STATE CHANGE REPORTING] ${JSON.stringify(payload)}`); 71 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { return; } 72 | } 73 | for (const key in payload) { this.eventState[key] = payload[key] } 74 | } 75 | } 76 | 77 | sendAsStats(payload: any) { 78 | if (Config.STATS_LOGGING) { 79 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.MIXED || GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { 80 | console.log(`[${new Date().toISOString()}][LOG][STATS REPORTING] ${JSON.stringify(payload)}`); 81 | if (GLOBAL_CONTEXT.OPERATION_MODE === REPORTING_MODES.SERVER) { return; } 82 | } 83 | for (const key in payload) { this.eventStats[key] = payload[key] } 84 | } 85 | } 86 | 87 | removeStatsOrControl(id: string) { 88 | delete this.eventControl[id]; 89 | delete this.eventStats[id]; 90 | } 91 | 92 | messageLoop = (res) => { 93 | this.timers['messageLoop'] = setInterval(() => { 94 | if (this.eventMessage.length > 0) { 95 | let msg = `id: ${this.messageCount}\n` 96 | for (const event of this.eventMessage) { msg = msg + `data: ${event}\n`; } 97 | res.write(msg + `\n`); 98 | this.eventMessage = []; 99 | this.messageCount++ 100 | } 101 | }, 255) 102 | } 103 | 104 | controlLoop = (res) => { 105 | this.timers['controlLoop'] = setInterval(() => { 106 | if (Object.keys(this.eventControl).length > 0) { 107 | res.write(`id: ${this.controlCount}\ndata: ${JSON.stringify(this.eventControl)} \n\n`); 108 | this.controlCount++; 109 | } 110 | }, 995) 111 | } 112 | 113 | dataLoop = (res) => { 114 | this.timers['dataLoop'] = setInterval(() => { 115 | if (Object.keys(this.eventData).length > 0) { 116 | res.write(`data: ${JSON.stringify(this.eventData)} \n\n`); 117 | this.eventData = {}; 118 | } 119 | }, 1525) 120 | } 121 | 122 | stateLoop = (res) => { 123 | this.timers['stateLoop'] = setInterval(() => { 124 | if (Object.keys(this.eventState).length > 0) { 125 | res.write(`data: ${JSON.stringify(this.eventState)} \n\n`); 126 | this.eventState = {}; 127 | } 128 | }, 2895) 129 | } 130 | 131 | statsLoop = (res) => { 132 | this.timers['statsLoop'] = setInterval(() => { 133 | if (Object.keys(this.eventStats).length > 0) { 134 | res.write(`id: ${this.statsCount}\ndata: ${JSON.stringify(this.eventStats)} \n\n`); 135 | this.statsCount++; 136 | } 137 | }, 5000) 138 | } 139 | } -------------------------------------------------------------------------------- /src/server/core/utils.ts: -------------------------------------------------------------------------------- 1 | import * as rw from 'random-words'; 2 | import * as randomLocation from 'random-location'; 3 | 4 | export function getDeviceId(connString: string) { 5 | var arr = /DeviceId=(.*);/g.exec(connString); 6 | if (arr && arr.length > 0) { 7 | return arr[1]; 8 | } 9 | return null; 10 | } 11 | 12 | export function getHostName(connString: string) { 13 | var arr = /HostName=(.*);/g.exec(connString); 14 | if (arr && arr.length > 0) { 15 | return arr[1]; 16 | } 17 | return null; 18 | } 19 | 20 | // this test for a particular state. not for general use 21 | export function isEmptyOrUndefined(obj: any) { 22 | return !(obj && Object.keys(obj).length > 0 && obj.constructor === Object); 23 | } 24 | 25 | export function isNumeric(n) { 26 | return !isNaN(parseFloat(n)) && isFinite(n); 27 | } 28 | 29 | export function isObject(o) { 30 | return (typeof o === "object" || typeof o === 'function') && (o !== null); 31 | } 32 | 33 | // create a string or non string value for a object property value 34 | export function formatValue(asString: boolean, value: any) { 35 | try { 36 | if (asString === false && (value.toString().toLowerCase() === "true" || value.toString().toLowerCase() === "false")) { 37 | return (value.toString().toLowerCase() === "true"); 38 | } else if (asString === true) { 39 | return value.toString(); 40 | } else { 41 | let res = parseFloat(value); 42 | if (!isNumeric(res)) { 43 | res = value.toString(); 44 | } 45 | return res; 46 | } 47 | } 48 | catch { 49 | return null 50 | } 51 | } 52 | 53 | export function getRandomNumberBetweenRange(min: number, max: number, floor: boolean) { 54 | const mn = formatValue(false, min); 55 | const val = (Math.random() * (formatValue(false, max) - mn)) + mn; 56 | return floor ? Math.floor(val) : val; 57 | } 58 | 59 | export function getRandomValue(schema: string, min?: number, max?: number) { 60 | if (schema === 'double') { return getRandomNumberBetweenRange(min, max, false); } 61 | if (schema === 'float') { return getRandomNumberBetweenRange(min, max, false); } 62 | if (schema === 'integer') { return getRandomNumberBetweenRange(min, max, true); } 63 | if (schema === 'long') { return getRandomNumberBetweenRange(min, max, true); } 64 | if (schema === 'boolean') { return Math.random() >= 0.5; } 65 | if (schema === 'string') { return rw(); } 66 | const dt = new Date(); 67 | if (schema === 'dateTime') { return dt.toISOString(); } 68 | if (schema === 'date') { return dt.toISOString().substr(0, 10); } 69 | if (schema === 'time') { return dt.toISOString().substr(11, 8); } 70 | if (schema === 'duration') { return dt.toISOString().substr(11, 8); } 71 | } 72 | 73 | export function getRandomMap() { 74 | return {} 75 | } 76 | 77 | export function getRandomVector(min: number, max: number) { 78 | return { 79 | "x": getRandomNumberBetweenRange(min, max, true), 80 | "y": getRandomNumberBetweenRange(min, max, true), 81 | "z": getRandomNumberBetweenRange(min, max, true) 82 | } 83 | } 84 | 85 | export function getRandomGeo(lat?: number, long?: number, alt?: number, radius?: number) { 86 | const randomPoint = randomLocation.randomCirclePoint({ 87 | latitude: lat || 51.508009, 88 | longitude: long || -0.128114 89 | }, radius || 25000) 90 | return { 91 | "lat": randomPoint.latitude, 92 | "lon": randomPoint.longitude, 93 | "alt": alt || 100 94 | } 95 | } 96 | 97 | export function decodeModuleKey(key: string): any { 98 | const r = new RegExp(`\<(.*)\>(.*)?`) 99 | const m = key.match(r); 100 | if (!m || m.length != 3) { return key; } 101 | return { deviceId: m[1], moduleId: m[2] }; 102 | } 103 | 104 | export function getModuleKey(deviceId: string, moduleId: string) { 105 | return `<${deviceId}>${moduleId}`; 106 | } -------------------------------------------------------------------------------- /src/server/framework/AssociativeStore.ts: -------------------------------------------------------------------------------- 1 | export class AssociativeStore { 2 | 3 | private store: T = null; 4 | 5 | constructor() { 6 | this.initStore(); 7 | } 8 | 9 | public initStore = () => { 10 | this.store = {}; 11 | } 12 | 13 | public deleteStore = () => { 14 | this.initStore(); 15 | } 16 | 17 | public count = (): number => { 18 | if (this.store != null) { 19 | return Object.keys(this.store).length; 20 | } else { return 0; } 21 | } 22 | 23 | public getItem = (id: string): any => { 24 | return this.store[id]; 25 | } 26 | 27 | public setItem = (item: T, id: string) => { 28 | this.store[id] = item; 29 | } 30 | 31 | public deleteItem = (id: string) => { 32 | delete this.store[id]; 33 | } 34 | 35 | public getAllItems = (): Array => { 36 | let arr = []; 37 | for (const key in this.store) { arr.push(this.store[key]) } 38 | return arr; 39 | } 40 | 41 | public createStoreFromArray = (arr: Array) => { 42 | this.initStore(); 43 | for (const i in arr) { this.store[arr[i]['_id']] = arr[i]; } 44 | } 45 | } -------------------------------------------------------------------------------- /src/server/interfaces/device.ts: -------------------------------------------------------------------------------- 1 | export interface Component { 2 | enabled: boolean; 3 | name: string; 4 | } 5 | 6 | export interface DeviceType { 7 | mock: boolean; 8 | direction: 'd2c' | 'c2d'; 9 | } 10 | 11 | export interface RunLoop { 12 | _ms: number; 13 | include: boolean; 14 | unit: 'secs' | 'mins'; 15 | value: number; 16 | valueMax: number; 17 | onStartUp?: boolean; 18 | override?: boolean 19 | } 20 | 21 | export interface MockSensor { 22 | _id: string; 23 | _hasState: boolean; 24 | _type: 'fan' | 'hotplate' | 'battery' | 'random' | 'function' | 'inc' | 'dec'; 25 | _value: number; 26 | init: number; 27 | running?: number; 28 | variance?: number; 29 | timeToRunning?: number; 30 | function?: string; 31 | reset?: number; 32 | } 33 | 34 | export interface Property { 35 | _id: string; 36 | _type: "property"; 37 | _matchedId?: string; 38 | name: string; 39 | enabled: boolean; 40 | component: Component; 41 | string: boolean; 42 | value: any; 43 | sdk: string; 44 | type: DeviceType; 45 | version: number; 46 | propertyObject: PropertyObjectDefault | PropertyObjectTemplated; 47 | runloop?: RunLoop; 48 | mock?: MockSensor; 49 | color?: string; 50 | asProperty?: boolean; 51 | asPropertyId?: string; 52 | asPropertyConvention?: boolean; 53 | asPropertyVersion?: boolean; 54 | asPropertyVersionPayload?: any; 55 | } 56 | 57 | export interface Method { 58 | _id: string; 59 | _type: "method"; 60 | execution: "direct" | "cloud"; 61 | name: string; 62 | enabled?: boolean; 63 | component: Component; 64 | status: string; 65 | receivedParams: string; 66 | payload: any; 67 | color?: string; 68 | asProperty?: boolean; 69 | asPropertyId?: string; 70 | asPropertyConvention?: boolean; 71 | asPropertyVersion?: boolean; 72 | asPropertyVersionPayload?: any; 73 | } 74 | 75 | export interface PropertyObjectDefault { 76 | type: "default"; 77 | } 78 | 79 | export interface PropertyObjectTemplated { 80 | type: "templated" 81 | template: any; 82 | } 83 | 84 | export interface Plan { 85 | loop: boolean, 86 | startup: Array, 87 | timeline: Array, 88 | random: Array, 89 | receive: Array 90 | } 91 | 92 | export class Device { 93 | public _id: string; 94 | public configuration: DeviceConfiguration; 95 | public comms: Array; 96 | public plan: Plan; 97 | public plugin: string; 98 | 99 | constructor() { 100 | this.comms = new Array(); 101 | this.configuration = new DeviceConfiguration(); 102 | } 103 | } 104 | 105 | export class DeviceConfiguration { 106 | public _kind: 'dps' | 'hub' | 'template' | 'edge' | 'module' | 'moduleHosted' | 'leafDevice'; 107 | public _deviceList?: []; 108 | public _plugIns?: []; 109 | public _modules?: []; 110 | public deviceId?: string; 111 | public devices?: []; 112 | public mockDeviceName?: string; 113 | public mockDeviceCount?: number; 114 | public mockDeviceCountMax?: number; 115 | public mockDeviceCloneId?: string; 116 | public connectionString?: string; 117 | public scopeId?: string; 118 | public dpsPayload?: any; 119 | public sasKey?: string; 120 | public isMasterKey?: boolean; 121 | public capabilityModel?: any; 122 | public capabilityUrn?: string; 123 | public machineState?: string; 124 | public machineStateClipboard?: string; 125 | public planMode?: boolean; 126 | public modules?: Array = []; 127 | public modulesDocker?: any; 128 | public leafDevices?: Array = []; 129 | public centralAdded?: boolean; 130 | public plugIn?: string; 131 | public geo?: number; 132 | public gatewayId?: string; 133 | public gatewayDeviceId?: string; 134 | public gatewayScopeId?: string; 135 | public gatewaySasKey?: string; 136 | } -------------------------------------------------------------------------------- /src/server/interfaces/payload.ts: -------------------------------------------------------------------------------- 1 | export interface ValueByIdPayload { 2 | _id: any 3 | } 4 | 5 | export interface DesiredPayload { 6 | payload: any, 7 | convention: boolean, 8 | value: any, 9 | version: number, 10 | component: boolean 11 | } -------------------------------------------------------------------------------- /src/server/interfaces/plugin.ts: -------------------------------------------------------------------------------- 1 | export interface PlugIn { 2 | usage: string; 3 | initialize: Function; 4 | reset: Function; 5 | configureDevice: Function; 6 | postConnect: Function; 7 | stopDevice: Function; 8 | propertyResponse: Function; 9 | commandResponse: Function; 10 | desiredResponse: Function; 11 | } -------------------------------------------------------------------------------- /src/server/plugins/devicemove.ts: -------------------------------------------------------------------------------- 1 | import { PlugIn } from '../interfaces/plugin' 2 | 3 | // This class name is used in the device configuration and UX 4 | export class DeviceMove implements PlugIn { 5 | 6 | // Sample code 7 | private devices = {}; 8 | private deviceConfigurations = {}; 9 | private deviceConfigurationCallbacks = {}; 10 | 11 | // this is used by the UX to show some information about the plugin 12 | public usage: string = "This is a sample plugin that implements IDeviceMove" 13 | 14 | // this is called when mock-devices first starts. time hear adds to start up time 15 | public initialize = () => { 16 | return undefined; 17 | } 18 | 19 | // not implemented 20 | public reset = () => { 21 | return undefined; 22 | } 23 | 24 | // this is called when a device is added or it's configuration has changed i.e. one of the capabilities has changed 25 | public configureDevice = (deviceId: string, running: boolean, configuration: any, cb: any) => { 26 | if (!running) { 27 | this.devices[deviceId] = {}; 28 | } 29 | this.deviceConfigurations[configuration.deviceId] = configuration; 30 | this.deviceConfigurationCallbacks[configuration.deviceId] = cb; 31 | } 32 | 33 | // this is called when a device has gone through dps/hub connection cycles and is ready to send data 34 | public postConnect = (deviceId: string) => { 35 | return undefined; 36 | } 37 | 38 | // this is called when a device has fully stopped sending data 39 | public stopDevice = (deviceId: string) => { 40 | return undefined; 41 | } 42 | 43 | // this is called during the loop cycle for a given capability or if Send is pressed in UX 44 | public propertyResponse = (deviceId: string, capability: any, payload: any) => { 45 | return undefined; 46 | } 47 | 48 | // this is called when the device is sent a C2D Command or Direct Method 49 | public commandResponse = (deviceId: string, capability: any, payload: any) => { 50 | if (capability.name === 'DeviceMove') { 51 | this.deviceConfigurations[deviceId].scopeId = payload; 52 | this.deviceConfigurationCallbacks[deviceId](this.deviceConfigurations[deviceId]); 53 | return true; 54 | } 55 | return undefined; 56 | } 57 | 58 | // this is called when the device is sent a desired twin property 59 | public desiredResponse = (deviceId: string, capability: any) => { 60 | return undefined; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/server/plugins/increment.ts: -------------------------------------------------------------------------------- 1 | import { PlugIn } from '../interfaces/plugin' 2 | 3 | // This class name is used in the device configuration and UX 4 | export class Increment implements PlugIn { 5 | 6 | // Sample code 7 | private devices = {}; 8 | 9 | // this is used by the UX to show some information about the plugin 10 | public usage: string = "This is a sample plugin that will provide an integer that increments by 1 on every loop or manual send. Acts on the device for all capabilities" 11 | 12 | // this is called when mock-devices first starts. time hear adds to start up time 13 | public initialize = () => { 14 | return undefined; 15 | } 16 | 17 | // not implemented 18 | public reset = () => { 19 | return undefined; 20 | } 21 | 22 | // this is called when a device is added or it's configuration has changed i.e. one of the capabilities has changed 23 | public configureDevice = (deviceId: string, running: boolean) => { 24 | if (!running) { 25 | this.devices[deviceId] = {}; 26 | } 27 | } 28 | 29 | // this is called when a device has gone through dps/hub connection cycles and is ready to send data 30 | public postConnect = (deviceId: string) => { 31 | return undefined; 32 | } 33 | 34 | // this is called when a device has fully stopped sending data 35 | public stopDevice = (deviceId: string) => { 36 | return undefined; 37 | } 38 | 39 | // this is called during the loop cycle for a given capability or if Send is pressed in UX 40 | public propertyResponse = (deviceId: string, capability: any, payload: any) => { 41 | if (Object.getOwnPropertyNames(this.devices[deviceId]).indexOf(capability._id) > -1) { 42 | this.devices[deviceId][capability._id] = this.devices[deviceId][capability._id] + 1; 43 | } else { 44 | this.devices[deviceId][capability._id] = 0; 45 | } 46 | return this.devices[deviceId][capability._id]; 47 | } 48 | 49 | // this is called when the device is sent a C2D Command or Direct Method 50 | public commandResponse = (deviceId: string, capability: any) => { 51 | return undefined; 52 | } 53 | 54 | // this is called when the device is sent a desired twin property 55 | public desiredResponse = (deviceId: string, capability: any) => { 56 | return undefined; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/server/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './increment'; 2 | export * from './location'; 3 | export * from './devicemove'; -------------------------------------------------------------------------------- /src/server/store/sensorStore.ts: -------------------------------------------------------------------------------- 1 | import * as uuidV4 from 'uuid/v4'; 2 | import { SimulationStore } from '../store/simulationStore'; 3 | 4 | export class SensorStore { 5 | private simulationStore = new SimulationStore(); 6 | private mocks = this.simulationStore.get()["mocks"]; 7 | 8 | public getListOfItems = () => { 9 | return [ 10 | this.getNewSensor('battery'), 11 | this.getNewSensor('hotplate'), 12 | this.getNewSensor('fan'), 13 | this.getNewSensor('random'), 14 | this.getNewSensor('function'), 15 | this.getNewSensor('inc'), 16 | this.getNewSensor('dec') 17 | ] 18 | } 19 | 20 | public getNewSensor = (type: string) => { 21 | let base = {} 22 | switch (type) { 23 | case 'battery': 24 | base = { 25 | _type: "battery", 26 | _value: 0, 27 | init: 100, 28 | running: 0, 29 | variance: 0.1, 30 | timeToRunning: 86400000, 31 | reset: null, 32 | _resx: { 33 | init: "Start", 34 | running: "End", 35 | variance: "Varies %", 36 | timeToRunning: "End (ms)", 37 | reset: "Reset" 38 | } 39 | } 40 | break; 41 | case 'hotplate': 42 | base = { 43 | _type: "hotplate", 44 | _value: 0, 45 | init: 0, 46 | running: 275, 47 | variance: 0.1, 48 | timeToRunning: 28800000, 49 | reset: null, 50 | _resx: { 51 | init: "Start", 52 | running: "End", 53 | variance: "Varies %", 54 | timeToRunning: "End (ms)", 55 | reset: "Reset" 56 | } 57 | } 58 | break; 59 | case 'fan': 60 | base = { 61 | _type: "fan", 62 | _value: 0, 63 | init: 0, 64 | variance: 2.5, 65 | running: 2000, 66 | timeToRunning: 1, 67 | _resx: { 68 | init: "Initial", 69 | running: "Expected", 70 | variance: "Varies %", 71 | timeToRunning: "Starts" 72 | } 73 | } 74 | break; 75 | case 'random': 76 | base = { 77 | _type: "random", 78 | _value: 0, 79 | variance: 3, 80 | init: 0, 81 | _resx: { 82 | init: "Initial", 83 | variance: "Length" 84 | } 85 | } 86 | break; 87 | case 'inc': 88 | base = { 89 | _type: "inc", 90 | _value: 0, 91 | variance: 1, 92 | init: 0, 93 | reset: null, 94 | _resx: { 95 | init: "Initial", 96 | variance: "Step", 97 | reset: "Reset" 98 | } 99 | } 100 | break; 101 | case 'dec': 102 | base = { 103 | _type: "dec", 104 | _value: 10000, 105 | variance: 1, 106 | init: 10000, 107 | reset: null, 108 | _resx: { 109 | init: "Initial", 110 | variance: "Step", 111 | reset: "Reset" 112 | } 113 | } 114 | break; 115 | case 'function': 116 | base = { 117 | _type: "function", 118 | _value: 0, 119 | init: 0, 120 | function: "http://myfunctionUrl", 121 | _resx: { 122 | init: "Initial", 123 | function: "Url" 124 | } 125 | } 126 | break; 127 | } 128 | return Object.assign({ _id: uuidV4(), _hasState: false }, base, this.mocks[type]) 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /src/server/store/simulationStore.ts: -------------------------------------------------------------------------------- 1 | import * as uuidV4 from 'uuid/v4'; 2 | import simulation from '../api/simulation'; 3 | 4 | export class SimulationStore { 5 | 6 | static simulation = { 7 | "bulk": { 8 | "mode": "random", 9 | "random": { 10 | "min": 5000, 11 | "max": 90000 12 | }, 13 | "batch": { 14 | "size": 10, 15 | "delay": 5000 16 | } 17 | }, 18 | "ranges": { 19 | "AUTO_INTEGER": { 20 | "min": 1, 21 | "max": 5000 22 | }, 23 | "AUTO_DOUBLE": { 24 | "min": 1, 25 | "max": 5000 26 | }, 27 | "AUTO_LONG": { 28 | "min": 1, 29 | "max": 30 | 5000 31 | }, 32 | "AUTO_FLOAT": { 33 | "min": 1, 34 | "max": 5000 35 | }, 36 | "AUTO_VECTOR": { 37 | "min": 1, 38 | "max": 500 39 | } 40 | }, 41 | "runloop": { 42 | "secs": { 43 | "min": 30, 44 | "max": 90 45 | }, 46 | "mins": { 47 | "min": 5, 48 | "max": 60 49 | } 50 | }, 51 | "geo": [ 52 | { 53 | "latitude": 51.508009, 54 | "longitude": -0.128114, 55 | "altitude": 100, 56 | "radius": 25000 57 | }, // London - England 58 | { 59 | "latitude": 47.608013, 60 | "longitude": -122.335167, 61 | "altitude": 100, 62 | "radius": 30000 63 | }, // Seattle - US West Coast 64 | { 65 | "latitude": 39.8952663456671, 66 | "longitude": -169.80377348147474, 67 | "altitude": 0, 68 | "radius": 1500000 69 | }, // Atlantic - Ocean 70 | { 71 | "latitude": 40.736291221818526, 72 | "longitude": -74.17785632140426, 73 | "altitude": 100, 74 | "radius": 20000 75 | }, // Newark - US East Coast 76 | { 77 | "latitude": 47.37358185559139, 78 | "longitude": 8.5511341111357, 79 | "altitude": 100, 80 | "radius": 90000 81 | }, // Zurich - Europe 82 | { 83 | "latitude": 33.533965950364625, 84 | "longitude": -43.060207013303, 85 | "altitude": 0, 86 | "radius": 1500000 87 | }, // Pacific - Ocean 88 | ], 89 | "colors": { 90 | "Default": "#333", 91 | "Color1": "#3a1e1e", 92 | "Color2": "#383a1e", 93 | "Color3": "#3e2e12", 94 | "Color4": "#ad3523", 95 | "Color5": "#313546", 96 | "Color6": "#205375", 97 | "Color7": "#4a4646", 98 | "Color8": "#774b00", 99 | "Color9": "#941d49", 100 | "Color10": "#000" 101 | }, 102 | "simulation": { 103 | "firmware": 30000, 104 | "connect": 5000, 105 | "restart": { 106 | min: 8, 107 | max: 16 108 | }, 109 | "sasExpire": 168, 110 | "dpsRetires": 10 111 | }, 112 | "commands": { 113 | "reboot": "reboot", 114 | "firmware": "firmware", 115 | "shutdown": "shutdown" 116 | }, 117 | "mocks": { 118 | "battery": { 119 | "init": 100, 120 | "running": 0, 121 | "variance": 0.1, 122 | "timeToRunning": 60400000 123 | }, 124 | "hotplate": { 125 | "init": 0, 126 | "running": 275, 127 | "variance": 0.1, 128 | "timeToRunning": 28800000 129 | }, 130 | "fan": { 131 | "init": 0, 132 | "variance": 2.5, 133 | "running": 2000, 134 | "timeToRunning": 1 135 | }, 136 | "random": { 137 | "init": 0, 138 | "variance": 3 139 | } 140 | }, 141 | "plan": { 142 | "startDelay": 2000, 143 | "timelineDelay": 5000 144 | }, 145 | "snippets": { 146 | "DTDLv1": { 147 | "value": "DESIRED_VALUE", 148 | "ac": 200, 149 | "ad": "completed", 150 | "av": "DESIRED_VERSION" 151 | }, 152 | "DTDLv2": { 153 | "value": "DESIRED_VALUE", 154 | "status": "completed", 155 | "message": "test message", 156 | "statusCode": 200, 157 | "desiredVersion": "DESIRED_VERSION" 158 | }, 159 | "Response": { 160 | "result": "OK" 161 | }, 162 | "Value": { 163 | "value": null 164 | }, 165 | "Empty": {} 166 | }, 167 | "dcm": { 168 | "import": { 169 | "interfaceAsComponents": false 170 | } 171 | }, 172 | "ux": { 173 | "device": { 174 | "expandPropertyCard": true 175 | } 176 | } 177 | } 178 | 179 | public get(): {} { return SimulationStore.simulation; } 180 | 181 | public set(payload: any) { SimulationStore.simulation = payload; } 182 | } -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/favicon.ico -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | mock-devices v10.3 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license/free. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OFL license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: normal; 9 | font-display: auto; 10 | src: url("../webfonts/fa-brands-400.eot"); 11 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 12 | 13 | .fab { 14 | font-family: 'Font Awesome 5 Brands'; } 15 | -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"} -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: auto; 10 | src: url("../webfonts/fa-regular-400.eot"); 11 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 12 | 13 | .far { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400} -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | font-display: auto; 10 | src: url("../webfonts/fa-solid-900.eot"); 11 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 12 | 13 | .fa, 14 | .fas { 15 | font-family: 'Font Awesome 5 Free'; 16 | font-weight: 900; } 17 | -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900} -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/css/svg-with-js.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.8.2 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;transform:translate(-50%,-50%);transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;transform:scale(.25);transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;transform:scale(.25);transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;transform:scale(.25);transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetunez/mock-devices/e4d4a8c3d06fb6ee2e7dbd506d33d730aa9e7a5c/static/vendor/fontawesome-free-5.8.2-web/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /tsconfig.client.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "outDir": "./_dist/client/", 10 | "jsx": "react", 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": [ 15 | "src/client/**/*" 16 | ], 17 | "types": [ 18 | "react" 19 | ], 20 | "exclude": [ 21 | "node_modules/**/*" 22 | ] 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "outDir": "./_dist/server/", 10 | "allowSyntheticDefaultImports": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": [ 14 | "src/server/**/*" 15 | ], 16 | "types": [ 17 | "electron" 18 | ], 19 | "exclude": [ 20 | "node_modules/**/*", 21 | "src/client/**/*" 22 | ] 23 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | module.exports = { 5 | mode: 'none', 6 | entry: ['./src/client/app.tsx', "./src/client/app.scss"], 7 | output: { 8 | filename: 'bundle.js', 9 | path: path.resolve(__dirname, '_dist/client') 10 | }, 11 | devtool: 'inline-source-map', 12 | resolve: { 13 | extensions: ['.tsx', '.ts', '.js', 'scss', 'css'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.tsx?$/, 19 | loader: 'ts-loader', 20 | exclude: /node_modules/, 21 | options: { 22 | instance: 'ux', 23 | configFile: path.resolve(__dirname, 'tsconfig.client.json') 24 | } 25 | }, 26 | { 27 | test: /\.s?css$/, 28 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] 29 | }, 30 | { 31 | test: /\.svg$/, 32 | use: ['file-loader'], 33 | }, 34 | { 35 | test: /\.md$/, 36 | use: ['file-loader'], 37 | }, 38 | ] 39 | }, 40 | externals: { 41 | "react": "React", 42 | "react-dom": "ReactDOM" 43 | }, 44 | plugins: [ 45 | new MiniCssExtractPlugin({ 46 | filename: 'app.css', 47 | allChucks: true 48 | }), 49 | ] 50 | }; --------------------------------------------------------------------------------