├── .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 |
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 |
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 |
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 |
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 | };
--------------------------------------------------------------------------------