diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 1cf8902b9c..2dd348c84c 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -19,8 +19,9 @@ "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-rpc-terminal-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -37,8 +38,9 @@ "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", "controllerScript": "var requestTimeout = 500;\nvar commandStatusPollingInterval = 200;\n\nvar welcome = 'Welcome to ThingsBoard RPC remote shell.\\n';\n\nvar terminal, rpcEnabled, simulated, deviceName, cwd;\nvar commandExecuting = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n rpcEnabled = subscription.rpcEnabled;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n } else {\n deviceName = 'Simulated';\n simulated = true;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n \n terminal = $('#device-terminal', self.ctx.$container).terminal(\n function (command) {\n if (command && command.trim().length) {\n try {\n if (simulated) {\n this.echo(command);\n } else {\n sendCommand(this, command);\n }\n } catch(e) {\n this.error(e + '');\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: false,\n enabled: rpcEnabled,\n prompt: rpcEnabled ? currentPrompt : '',\n name: 'shell',\n pauseEvents: false,\n keydown: function (e, term) {\n if ((e.which == 67 || e.which == 68) && e.ctrlKey) { // CTRL+C || CTRL+D\n if (commandExecuting) {\n terminateCommand(term);\n return false;\n }\n }\n },\n onInit: initTerm\n }\n );\n \n};\n\nfunction initTerm(terminal) {\n terminal.echo(welcome);\n if (!rpcEnabled) {\n terminal.error('Target device is not set!\\n');\n } else {\n terminal.echo('Current target device for RPC terminal: [[b;#fff;]' + deviceName + ']\\n');\n if (!simulated) {\n terminal.pause();\n getTermInfo(terminal,\n function (remoteTermInfo) {\n if (remoteTermInfo) {\n terminal.echo('Remote platform info:');\n terminal.echo('OS: [[b;#fff;]' + remoteTermInfo.platform + ']');\n if (remoteTermInfo.release) {\n terminal.echo('OS release: [[b;#fff;]' + remoteTermInfo.release + ']');\n }\n terminal.echo('\\r');\n } else {\n terminal.echo('[[;#f00;]Unable to get remote platform info.\\nDevice is not responding.]\\n');\n }\n terminal.resume();\n });\n }\n }\n}\n\nfunction currentPrompt(callback) {\n if (cwd) {\n callback('[[b;#2196f3;]' + deviceName +']: [[b;#8bc34a;]' + cwd +']> ');\n } else {\n callback('[[b;#8bc34a;]' + deviceName +']> ');\n }\n}\n\nfunction getTermInfo(terminal, callback) {\n self.ctx.controlApi.sendTwoWayCommand('getTermInfo', null, requestTimeout).subscribe(\n function (termInfo) {\n cwd = termInfo.cwd;\n if (callback) {\n callback(termInfo);\n } \n },\n function () {\n if (callback) {\n callback(null);\n }\n }\n );\n}\n\nfunction sendCommand(terminal, command) {\n terminal.pause();\n var sendCommandRequest = {\n command: command,\n cwd: cwd\n };\n self.ctx.controlApi.sendTwoWayCommand('sendCommand', sendCommandRequest, requestTimeout).subscribe(\n function (responseBody) {\n if (responseBody && responseBody.ok) {\n commandExecuting = true;\n setTimeout( pollCommandStatus.bind(null,terminal), commandStatusPollingInterval );\n } else {\n var error = responseBody ? responseBody.error : 'Unhandled error.';\n terminal.error(error);\n terminal.resume();\n }\n },\n function () {\n onRpcError(terminal);\n }\n );\n}\n\nfunction terminateCommand(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('terminateCommand', null, requestTimeout).subscribe(\n function (responseBody) {\n if (!responseBody.ok) {\n commandExecuting = false;\n terminal.error(responseBody.error);\n terminal.resume();\n } \n },\n function () {\n onRpcError(terminal);\n }\n ); \n}\n\nfunction onRpcError(terminal) {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.resume();\n}\n\nfunction pollCommandStatus(terminal) {\n self.ctx.controlApi.sendTwoWayCommand('getCommandStatus', null, requestTimeout).subscribe(\n function (commandStatusResponse) {\n for (var i=0;i", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\",\n \"default\": 0\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\",\n \"default\": 100\n },\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"number\",\n \"default\": 50\n },\n \"title\": {\n \"title\": \"Knob title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"getValueMethod\": {\n \"title\": \"Get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"Set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"minValue\", \"maxValue\", \"getValueMethod\", \"setValueMethod\", \"requestTimeout\"]\n },\n \"form\": [\n \"minValue\",\n \"maxValue\",\n \"initialValue\",\n \"title\",\n \"getValueMethod\",\n \"setValueMethod\",\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-knob-control-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"maxValue\":100,\"initialValue\":50,\"minValue\":0,\"title\":\"Knob control\",\"getValueMethod\":\"getValue\",\"setValueMethod\":\"setValue\"},\"title\":\"Knob Control\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" } }, @@ -111,8 +114,9 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"performCheckStatus\": {\n \"title\": \"Perform RPC device status check\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"checkStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"performCheckStatus\",\n \"checkStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_value_fn\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-led-indicator-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":500,\"initialValue\":true,\"title\":\"Led indicator\",\"ledColor\":\"#4caf50\",\"valueAttribute\":\"value\",\"retrieveValueMethod\":\"attribute\",\"parseValueFunction\":\"return data ? true : false;\",\"performCheckStatus\":true,\"checkStatusMethod\":\"checkStatus\"},\"title\":\"Led indicator\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"decimals\":2}" } }, @@ -129,8 +133,9 @@ "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "controllerScript": "var requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n \n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n if (rpcParams.length) {\n try {\n rpcParams = JSON.parse(rpcParams);\n } catch (e) {}\n }\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout, requestPersistent, persistentPollingInterval);\n }\n commandPromise.subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n );\n };\n}\n\nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Send RPC\"\n },\n \"oneWayElseTwoWay\": {\n \"title\": \"Is One Way Command\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showError\": {\n \"title\": \"Show RPC command execution error\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"methodName\": {\n \"title\": \"RPC method\",\n \"type\": \"string\",\n \"default\": \"rpcCommand\"\n },\n \"methodParams\": {\n \"title\": \"RPC method params\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 5000\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n \"oneWayElseTwoWay\",\n \"showError\",\n \"methodName\",\n {\n \"key\": \"methodParams\",\n \"type\": \"json\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-send-rpc-widget-settings", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, @@ -147,8 +152,9 @@ "templateHtml": "
\n
\n {{title}}\n
\n
\n
\n \n
\n
\n
\n {{ error }}\n
\n
", "templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .mat-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}", "controllerScript": "self.onInit = function() {\n self.ctx.ngZone.run(function() {\n init(); \n self.ctx.detectChanges();\n });\n};\n\nfunction init() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n let attributeService = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('attributeService'));\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n let entityId = {\n entityType: \"DEVICE\",\n id: self.ctx.defaultSubscription.targetDeviceId\n };\n attributeService.saveEntityAttributes(entityId,\n entityAttributeType, attributes).subscribe(\n function success() {\n self.ctx.$scope.error = \"\";\n self.ctx.detectChanges();\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n self.ctx.detectChanges();\n }\n }\n\n );\n };\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Update device attribute\"\n },\n \"entityAttributeType\": {\n \"title\": \"Device attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"entityParameters\": {\n \"title\": \"Device attribute parameters\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n {\n \"key\": \"entityAttributeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n }, {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }]\n }, {\n \"key\": \"entityParameters\",\n \"type\": \"json\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}", + "settingsSchema": "", "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-update-device-attribute-widget-settings", "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html new file mode 100644 index 0000000000..d60481fc8c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html @@ -0,0 +1,41 @@ + + + {{ (keyType === dataKeyType.attribute + ? attributeLabel + : timeseriesLabel) | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts new file mode 100644 index 0000000000..46ce0034ee --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts @@ -0,0 +1,216 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { Observable, of } from 'rxjs'; +import { IAliasController } from '@core/api/widget-api.models'; +import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { DataKey } from '@shared/models/widget.models'; +import { EntityService } from '@core/http/entity.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-device-key-autocomplete', + templateUrl: './device-key-autocomplete.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceKeyAutocompleteComponent), + multi: true + } + ] +}) +export class DeviceKeyAutocompleteComponent extends PageComponent implements OnInit, ControlValueAccessor, OnChanges { + + @ViewChild('keyInput') keyInput: ElementRef; + + @Input() + disabled: boolean; + + @Input() + aliasController: IAliasController; + + @Input() + targetDeviceAliasId: string; + + @Input() + keyType: DataKeyType; + + @Input() + attributeLabel = 'widgets.rpc.attribute-value-key'; + + @Input() + timeseriesLabel = 'widgets.rpc.timeseries-value-key'; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + dataKeyType = DataKeyType; + + private modelValue: string; + + private propagateChange = null; + + public deviceKeyFormGroup: FormGroup; + + filteredKeys: Observable>; + keySearchText = ''; + + private latestKeySearchResult: Array = null; + private keysFetchObservable$: Observable> = null; + + constructor(protected store: Store, + private translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.deviceKeyFormGroup = this.fb.group({ + key: [null, this.required ? [Validators.required] : []] + }); + this.deviceKeyFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + + this.filteredKeys = this.deviceKeyFormGroup.get('key').valueChanges + .pipe( + map(value => value ? value : ''), + mergeMap(name => this.fetchKeys(name) ) + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'targetDeviceAliasId' || propName === 'keyType') { + this.clearKeysCache(); + } + } + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.deviceKeyFormGroup.disable({emitEvent: false}); + } else { + this.deviceKeyFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string): void { + this.modelValue = value; + this.deviceKeyFormGroup.patchValue( + {key: value}, {emitEvent: false} + ); + } + + clearKey() { + this.deviceKeyFormGroup.get('key').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.keyInput.nativeElement.blur(); + this.keyInput.nativeElement.focus(); + }, 0); + } + + onFocus() { + this.deviceKeyFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); + } + + private updateModel() { + const value: string = this.deviceKeyFormGroup.get('key').value; + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + private clearKeysCache(): void { + this.latestKeySearchResult = null; + this.keysFetchObservable$ = null; + } + + private fetchKeys(searchText?: string): Observable> { + if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { + this.keySearchText = searchText; + const dataKeyFilter = this.createKeyFilter(this.keySearchText); + return this.getKeys().pipe( + map(name => name.filter(dataKeyFilter)), + tap(res => this.latestKeySearchResult = res) + ); + } + return of(this.latestKeySearchResult); + } + + private getKeys() { + if (this.keysFetchObservable$ === null) { + let fetchObservable: Observable>; + if (this.targetDeviceAliasId) { + const dataKeyTypes = [this.keyType]; + fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes); + } else { + fetchObservable = of([]); + } + this.keysFetchObservable$ = fetchObservable.pipe( + map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), + publishReplay(1), + refCount() + ); + } + return this.keysFetchObservable$; + } + + private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { + return this.aliasController.getAliasInfo(entityAliasId).pipe( + mergeMap((aliasInfo) => { + return this.entityService.getEntityKeysByEntityFilter( + aliasInfo.entityFilter, + dataKeyTypes, + {ignoreLoading: true, ignoreErrors: true} + ).pipe( + catchError(() => of([])) + ); + }), + catchError(() => of([] as Array)) + ); + } + + private createKeyFilter(query: string): (key: string) => boolean { + const lowercaseQuery = query.toLowerCase(); + return key => key.toLowerCase().startsWith(lowercaseQuery); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html new file mode 100644 index 0000000000..1788a74446 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.html @@ -0,0 +1,81 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.knob-title + + +
+
+ widgets.rpc.value-settings + + widgets.rpc.initial-value + + +
+ + widgets.rpc.min-value + + + + widgets.rpc.max-value + + +
+ + widgets.rpc.get-value-method + + + + widgets.rpc.set-value-method + + +
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts new file mode 100644 index 0000000000..dae3045110 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/knob-control-widget-settings.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-knob-control-widget-settings', + templateUrl: './knob-control-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class KnobControlWidgetSettingsComponent extends WidgetSettingsComponent { + + knobControlWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.knobControlWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + minValue: 0, + maxValue: 100, + initialValue: 50, + getValueMethod: 'getValue', + setValueMethod: 'setValue', + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.knobControlWidgetSettingsForm = this.fb.group({ + + // Common settings + + title: [settings.title, []], + + // Value settings + + initialValue: [settings.initialValue, []], + minValue: [settings.minValue, [Validators.required]], + maxValue: [settings.maxValue, [Validators.required]], + + getValueMethod: [settings.getValueMethod, [Validators.required]], + setValueMethod: [settings.setValueMethod, [Validators.required]], + + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]], + + // --> Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]] + }); + } + + protected validatorTriggers(): string[] { + return ['requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const requestPersistent: boolean = this.knobControlWidgetSettingsForm.get('requestPersistent').value; + if (requestPersistent) { + this.knobControlWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.knobControlWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.knobControlWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html new file mode 100644 index 0000000000..c4e934eabe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.html @@ -0,0 +1,104 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.led-title + + + + +
+
+ widgets.rpc.value-settings + + {{ 'widgets.rpc.initial-value' | translate }} + +
+ widgets.rpc.check-status-settings + + {{ 'widgets.rpc.perform-rpc-status-check' | translate }} + + + widgets.rpc.retrieve-led-status-value-method + + + {{ 'widgets.rpc.retrieve-value-method-attribute' | translate }} + + + {{ 'widgets.rpc.retrieve-value-method-timeseries' | translate }} + + + + + + + widgets.rpc.check-status-method + + + + +
+
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts new file mode 100644 index 0000000000..d940e0f64a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts @@ -0,0 +1,133 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetService } from '@core/http/widget.service'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; + +@Component({ + selector: 'tb-led-indicator-widget-settings', + templateUrl: './led-indicator-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent { + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + get targetDeviceAliasId(): string { + const aliasIds = this.widget.config.targetDeviceAliasIds; + if (aliasIds && aliasIds.length) { + return aliasIds[0]; + } + return null; + } + + dataKeyType = DataKeyType; + + ledIndicatorWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private widgetService: WidgetService, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.ledIndicatorWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + ledColor: 'green', + initialValue: false, + performCheckStatus: true, + checkStatusMethod: 'checkStatus', + retrieveValueMethod: 'attribute', + valueAttribute: 'value', + parseValueFunction: 'return data ? true : false;', + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.ledIndicatorWidgetSettingsForm = this.fb.group({ + + // Common settings + + title: [settings.title, []], + ledColor: [settings.ledColor, []], + + // Value settings + + initialValue: [settings.initialValue, []], + + // --> Check status settings + + performCheckStatus: [settings.performCheckStatus, []], + retrieveValueMethod: [settings.retrieveValueMethod, []], + checkStatusMethod: [settings.checkStatusMethod, [Validators.required]], + valueAttribute: [settings.valueAttribute, [Validators.required]], + parseValueFunction: [settings.parseValueFunction, []], + + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0)]], + + // --> Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]] + }); + } + + protected validatorTriggers(): string[] { + return ['performCheckStatus', 'requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const performCheckStatus: boolean = this.ledIndicatorWidgetSettingsForm.get('performCheckStatus').value; + const requestPersistent: boolean = this.ledIndicatorWidgetSettingsForm.get('requestPersistent').value; + if (performCheckStatus) { + this.ledIndicatorWidgetSettingsForm.get('valueAttribute').disable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').enable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestTimeout').enable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestPersistent').enable({emitEvent: false}); + if (requestPersistent) { + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + } else { + this.ledIndicatorWidgetSettingsForm.get('valueAttribute').enable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').disable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestTimeout').disable({emitEvent}); + this.ledIndicatorWidgetSettingsForm.get('requestPersistent').disable({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.ledIndicatorWidgetSettingsForm.get('valueAttribute').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('checkStatusMethod').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('requestTimeout').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('requestPersistent').updateValueAndValidity({emitEvent: false}); + this.ledIndicatorWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html new file mode 100644 index 0000000000..1f027eb8b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.html @@ -0,0 +1,40 @@ + +
+
+ widgets.rpc.button-style + + {{ 'widgets.rpc.button-raised' | translate }} + + + {{ 'widgets.rpc.button-primary' | translate }} + +
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts new file mode 100644 index 0000000000..e01c4df9e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-button-style.component.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +export interface RpcButtonStyle { + isRaised: boolean; + isPrimary: boolean; + bgColor: string; + textColor: string; +} + +@Component({ + selector: 'tb-rpc-button-style', + templateUrl: './rpc-button-style.component.html', + styleUrls: ['./../widget-settings.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RpcButtonStyleComponent), + multi: true + } + ] +}) +export class RpcButtonStyleComponent extends PageComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + private modelValue: RpcButtonStyle; + + private propagateChange = null; + + public rpcButtonStyleFormGroup: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.rpcButtonStyleFormGroup = this.fb.group({ + isRaised: [true, []], + isPrimary: [false, []], + bgColor: [null, []], + textColor: [null, []] + }); + this.rpcButtonStyleFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.rpcButtonStyleFormGroup.disable({emitEvent: false}); + } else { + this.rpcButtonStyleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: RpcButtonStyle): void { + this.modelValue = value; + this.rpcButtonStyleFormGroup.patchValue( + value, {emitEvent: false} + ); + } + + private updateModel() { + this.modelValue = this.rpcButtonStyleFormGroup.value; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html new file mode 100644 index 0000000000..67d7caa29f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.html @@ -0,0 +1,27 @@ + + +
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts new file mode 100644 index 0000000000..d901ad2636 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-shell-widget-settings.component.ts @@ -0,0 +1,53 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-rpc-shell-widget-settings', + templateUrl: './rpc-shell-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class RpcShellWidgetSettingsComponent extends WidgetSettingsComponent { + + rpcShellWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.rpcShellWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + requestTimeout: 500 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.rpcShellWidgetSettingsForm = this.fb.group({ + // RPC settings + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html new file mode 100644 index 0000000000..79c34bfdea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.html @@ -0,0 +1,49 @@ + + +
+
+ widgets.rpc.rpc-settings + + widgets.rpc.request-timeout + + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts new file mode 100644 index 0000000000..906c0c71b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-rpc-terminal-widget-settings', + templateUrl: './rpc-terminal-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class RpcTerminalWidgetSettingsComponent extends WidgetSettingsComponent { + + rpcTerminalWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.rpcTerminalWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + requestTimeout: 500, + requestPersistent: false, + persistentPollingInterval: 5000 + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.rpcTerminalWidgetSettingsForm = this.fb.group({ + // RPC settings + + requestTimeout: [settings.requestTimeout, [Validators.min(0), Validators.required]], + + // --> Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]] + }); + } + + protected validatorTriggers(): string[] { + return ['requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const requestPersistent: boolean = this.rpcTerminalWidgetSettingsForm.get('requestPersistent').value; + if (requestPersistent) { + this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.rpcTerminalWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html new file mode 100644 index 0000000000..c36a21c349 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html @@ -0,0 +1,79 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.widget-title + + + + widgets.rpc.button-label + + +
+
+ widgets.rpc.rpc-settings + + {{ 'widgets.rpc.is-one-way-command' | translate }} + + + widgets.rpc.rpc-method + + + + + + widgets.rpc.request-timeout + + + + {{ 'widgets.rpc.show-rpc-error' | translate }} + +
+ widgets.rpc.persistent-rpc-settings + + + + + {{ 'widgets.rpc.request-persistent' | translate }} + + + + widget-config.advanced-settings + + + + + widgets.rpc.persistent-polling-interval + + + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts new file mode 100644 index 0000000000..051b780ca9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ContentType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-send-rpc-widget-settings', + templateUrl: './send-rpc-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class SendRpcWidgetSettingsComponent extends WidgetSettingsComponent { + + sendRpcWidgetSettingsForm: FormGroup; + + contentTypes = ContentType; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.sendRpcWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + buttonText: 'Send RPC', + oneWayElseTwoWay: true, + showError: false, + methodName: 'rpcCommand', + methodParams: '{}', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 5000, + styleButton: { + isRaised: true, + isPrimary: false, + bgColor: null, + textColor: null + } + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.sendRpcWidgetSettingsForm = this.fb.group({ + title: [settings.title, []], + buttonText: [settings.buttonText, []], + + // RPC settings + + oneWayElseTwoWay: [settings.oneWayElseTwoWay, []], + methodName: [settings.methodName, []], + methodParams: [settings.methodParams, []], + requestTimeout: [settings.requestTimeout, [Validators.min(0)]], + showError: [settings.showError, []], + + // Persistent RPC settings + + requestPersistent: [settings.requestPersistent, []], + persistentPollingInterval: [settings.persistentPollingInterval, [Validators.min(1000)]], + + styleButton: [settings.styleButton, []] + }); + } + + protected validatorTriggers(): string[] { + return ['requestPersistent']; + } + + protected updateValidators(emitEvent: boolean): void { + const requestPersistent: boolean = this.sendRpcWidgetSettingsForm.get('requestPersistent').value; + if (requestPersistent) { + this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').enable({emitEvent}); + } else { + this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').disable({emitEvent}); + } + this.sendRpcWidgetSettingsForm.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html index 3bb4e01573..ec2228cc78 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.html @@ -40,30 +40,15 @@ - - {{ (switchRpcSettingsFormGroup.get('retrieveValueMethod').value === 'attribute' - ? 'widgets.rpc.attribute-value-key' - : 'widgets.rpc.timeseries-value-key') | translate }} - - - - - - - - + + widgets.rpc.get-value-method diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts index 6be84b4241..711d0a0275 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-rpc-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -31,10 +31,7 @@ import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetService } from '@core/http/widget.service'; -import { Observable, of } from 'rxjs'; import { IAliasController } from '@core/api/widget-api.models'; -import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; -import { DataKey } from '@shared/models/widget.models'; import { EntityService } from '@core/http/entity.service'; export declare type RpcRetrieveValueMethod = 'none' | 'rpc' | 'attribute' | 'timeseries'; @@ -84,7 +81,7 @@ export function switchRpcDefaultSettings(): SwitchRpcSettings { } ] }) -export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator, OnChanges { +export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { @ViewChild('keyInput') keyInput: ElementRef; @@ -97,6 +94,8 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, @Input() targetDeviceAliasId: string; + dataKeyType = DataKeyType; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); private modelValue: SwitchRpcSettings; @@ -105,12 +104,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, public switchRpcSettingsFormGroup: FormGroup; - filteredKeys: Observable>; - keySearchText = ''; - - private latestKeySearchResult: Array = null; - private keysFetchObservable$: Observable> = null; - constructor(protected store: Store, private translate: TranslateService, private widgetService: WidgetService, @@ -148,7 +141,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, persistentPollingInterval: [5000, [Validators.min(1000)]], }); this.switchRpcSettingsFormGroup.get('retrieveValueMethod').valueChanges.subscribe(() => { - this.clearKeysCache(); this.updateValidators(true); }); this.switchRpcSettingsFormGroup.get('requestPersistent').valueChanges.subscribe(() => { @@ -158,23 +150,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, this.updateModel(); }); this.updateValidators(false); - - this.filteredKeys = this.switchRpcSettingsFormGroup.get('valueKey').valueChanges - .pipe( - map(value => value ? value : ''), - mergeMap(name => this.fetchKeys(name) ) - ); - } - - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (!change.firstChange && change.currentValue !== change.previousValue) { - if (propName === 'targetDeviceAliasId') { - this.clearKeysCache(); - } - } - } } registerOnChange(fn: any): void { @@ -201,14 +176,6 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, this.updateValidators(false); } - clearKey() { - this.switchRpcSettingsFormGroup.get('valueKey').patchValue(null, {emitEvent: true}); - setTimeout(() => { - this.keyInput.nativeElement.blur(); - this.keyInput.nativeElement.focus(); - }, 0); - } - public validate(c: FormControl) { return this.switchRpcSettingsFormGroup.valid ? null : { switchRpcSettings: { @@ -249,60 +216,4 @@ export class SwitchRpcSettingsComponent extends PageComponent implements OnInit, this.switchRpcSettingsFormGroup.get('parseValueFunction').updateValueAndValidity({emitEvent: false}); this.switchRpcSettingsFormGroup.get('persistentPollingInterval').updateValueAndValidity({emitEvent: false}); } - - private clearKeysCache(): void { - this.latestKeySearchResult = null; - this.keysFetchObservable$ = null; - } - - private fetchKeys(searchText?: string): Observable> { - if (this.keySearchText !== searchText || this.latestKeySearchResult === null) { - this.keySearchText = searchText; - const dataKeyFilter = this.createKeyFilter(this.keySearchText); - return this.getKeys().pipe( - map(name => name.filter(dataKeyFilter)), - tap(res => this.latestKeySearchResult = res) - ); - } - return of(this.latestKeySearchResult); - } - - private getKeys() { - if (this.keysFetchObservable$ === null) { - let fetchObservable: Observable>; - if (this.targetDeviceAliasId) { - const retrieveValueMethod: RpcRetrieveValueMethod = this.switchRpcSettingsFormGroup.get('retrieveValueMethod').value; - const dataKeyTypes = retrieveValueMethod === 'attribute' ? [DataKeyType.attribute] : [DataKeyType.timeseries]; - fetchObservable = this.fetchEntityKeys(this.targetDeviceAliasId, dataKeyTypes); - } else { - fetchObservable = of([]); - } - this.keysFetchObservable$ = fetchObservable.pipe( - map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), - publishReplay(1), - refCount() - ); - } - return this.keysFetchObservable$; - } - - private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array): Observable> { - return this.aliasController.getAliasInfo(entityAliasId).pipe( - mergeMap((aliasInfo) => { - return this.entityService.getEntityKeysByEntityFilter( - aliasInfo.entityFilter, - dataKeyTypes, - {ignoreLoading: true, ignoreErrors: true} - ).pipe( - catchError(() => of([])) - ); - }), - catchError(() => of([] as Array)) - ); - } - - private createKeyFilter(query: string): (key: string) => boolean { - const lowercaseQuery = query.toLowerCase(); - return key => key.toLowerCase().startsWith(lowercaseQuery); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html new file mode 100644 index 0000000000..ef9ee32174 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.html @@ -0,0 +1,51 @@ + + +
+
+ widgets.rpc.common-settings + + widgets.rpc.widget-title + + + + widgets.rpc.button-label + + + + widgets.rpc.device-attribute-scope + + + {{ 'widgets.rpc.server-attribute' | translate }} + + + {{ 'widgets.rpc.shared-attribute' | translate }} + + + + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts new file mode 100644 index 0000000000..6689112bf7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component.ts @@ -0,0 +1,68 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ContentType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-update-device-attribute-widget-settings', + templateUrl: './update-device-attribute-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class UpdateDeviceAttributeWidgetSettingsComponent extends WidgetSettingsComponent { + + updateDeviceAttributeWidgetSettingsForm: FormGroup; + + contentTypes = ContentType; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.updateDeviceAttributeWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + title: '', + buttonText: 'Update device attribute', + entityAttributeType: 'SERVER_SCOPE', + entityParameters: '{}', + styleButton: { + isRaised: true, + isPrimary: false, + bgColor: null, + textColor: null + } + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.updateDeviceAttributeWidgetSettingsForm = this.fb.group({ + title: [settings.title, []], + buttonText: [settings.buttonText, []], + entityAttributeType: [settings.entityAttributeType, []], + entityParameters: [settings.entityParameters, []], + styleButton: [settings.styleButton, []] + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index e893b74b7e..c7b3948427 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -119,6 +119,28 @@ import { import { PersistentTableWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/persistent-table-widget-settings.component'; +import { RpcButtonStyleComponent } from '@home/components/widget/lib/settings/control/rpc-button-style.component'; +import { + UpdateDeviceAttributeWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/update-device-attribute-widget-settings.component'; +import { + SendRpcWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/send-rpc-widget-settings.component'; +import { + DeviceKeyAutocompleteComponent +} from '@home/components/widget/lib/settings/control/device-key-autocomplete.component'; +import { + LedIndicatorWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/led-indicator-widget-settings.component'; +import { + KnobControlWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/knob-control-widget-settings.component'; +import { + RpcTerminalWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/rpc-terminal-widget-settings.component'; +import { + RpcShellWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/rpc-shell-widget-settings.component'; @NgModule({ declarations: [ @@ -159,11 +181,19 @@ import { FlotPieKeySettingsComponent, ChartWidgetSettingsComponent, DoughnutChartWidgetSettingsComponent, + DeviceKeyAutocompleteComponent, SwitchRpcSettingsComponent, RoundSwitchWidgetSettingsComponent, SwitchControlWidgetSettingsComponent, SlideToggleWidgetSettingsComponent, - PersistentTableWidgetSettingsComponent + PersistentTableWidgetSettingsComponent, + RpcButtonStyleComponent, + UpdateDeviceAttributeWidgetSettingsComponent, + SendRpcWidgetSettingsComponent, + LedIndicatorWidgetSettingsComponent, + KnobControlWidgetSettingsComponent, + RpcTerminalWidgetSettingsComponent, + RpcShellWidgetSettingsComponent ], imports: [ CommonModule, @@ -208,11 +238,19 @@ import { FlotPieKeySettingsComponent, ChartWidgetSettingsComponent, DoughnutChartWidgetSettingsComponent, + DeviceKeyAutocompleteComponent, SwitchRpcSettingsComponent, RoundSwitchWidgetSettingsComponent, SwitchControlWidgetSettingsComponent, SlideToggleWidgetSettingsComponent, - PersistentTableWidgetSettingsComponent + PersistentTableWidgetSettingsComponent, + RpcButtonStyleComponent, + UpdateDeviceAttributeWidgetSettingsComponent, + SendRpcWidgetSettingsComponent, + LedIndicatorWidgetSettingsComponent, + KnobControlWidgetSettingsComponent, + RpcTerminalWidgetSettingsComponent, + RpcShellWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -249,5 +287,11 @@ export const widgetSettingsComponentsMap: {[key: string]: Type -
diff --git a/ui-ngx/src/app/shared/components/json-content.component.scss b/ui-ngx/src/app/shared/components/json-content.component.scss index fd2afd9e72..124092718a 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.scss +++ b/ui-ngx/src/app/shared/components/json-content.component.scss @@ -13,44 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { +.tb-json-content { position: relative; .fill-height { height: 100%; } -} -.tb-json-content-toolbar { - button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { - align-items: center; - vertical-align: middle; - min-width: 32px; - min-height: 15px; - padding: 4px; - margin: 0; - font-size: .8rem; - line-height: 15px; - color: #7b7b7b; - background: rgba(220, 220, 220, .35); - &:not(:last-child) { - margin-right: 4px; - } + &:not(.tb-fullscreen) { + padding-bottom: 15px; } -} -.tb-json-content-panel { - height: 100%; - margin-left: 15px; - border: 1px solid #c0c0c0; + .tb-json-content-toolbar { + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } + } - #tb-json-input { - width: 100%; - min-width: 200px; + .tb-json-content-panel { height: 100%; + border: 1px solid #c0c0c0; + + #tb-json-input { + width: 100%; + min-width: 200px; + height: 100%; - &:not(.fill-height) { - min-height: 200px; + &:not(.fill-height) { + min-height: 200px; + } } } } diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts index 89d1a85c25..edf6e51ac8 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.ts +++ b/ui-ngx/src/app/shared/components/json-content.component.ts @@ -15,6 +15,7 @@ /// import { + ChangeDetectorRef, Component, ElementRef, forwardRef, @@ -23,7 +24,7 @@ import { OnDestroy, OnInit, SimpleChanges, - ViewChild + ViewChild, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import { Ace } from 'ace-builds'; @@ -53,7 +54,8 @@ import { beautifyJs } from '@shared/models/beautify.models'; useExisting: forwardRef(() => JsonContentComponent), multi: true, } - ] + ], + encapsulation: ViewEncapsulation.None }) export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { @@ -116,7 +118,8 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid constructor(public elementRef: ElementRef, protected store: Store, - private raf: RafService) { + private raf: RafService, + private cd: ChangeDetectorRef) { } ngOnInit(): void { @@ -151,9 +154,12 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.updateView(); } }); - this.jsonEditor.on('blur', () => { - this.contentValid = !this.validateContent || this.doValidate(true); - }); + if (this.validateContent) { + this.jsonEditor.on('blur', () => { + this.contentValid = this.doValidate(true); + this.cd.markForCheck(); + }); + } this.editorResize$ = new ResizeObserver(() => { this.onAceEditorResize(); }); @@ -225,12 +231,13 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.propagateChange(this.contentBody); this.contentValid = this.doValidate(true); this.propagateChange(this.contentBody); + this.cd.markForCheck(); } } private doValidate(showErrorToast = false): boolean { try { - if (this.validateContent && this.contentType === ContentType.JSON) { + if (this.contentType === ContentType.JSON) { JSON.parse(this.contentBody); } return true; @@ -283,6 +290,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid this.contentBody = editorValue; this.contentValid = !this.validateOnChange || this.doValidate(); this.propagateChange(this.contentBody); + this.cd.markForCheck(); } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6f8117c77d..38b693b4a8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3628,7 +3628,7 @@ "set-value-method": "RPC set value method", "convert-value-function": "Convert value function", "rpc-settings": "RPC settings", - "request-timeout": "RPC request timeout", + "request-timeout": "RPC request timeout (ms)", "persistent-rpc-settings": "Persistent RPC settings", "request-persistent": "RPC request persistent", "persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response", @@ -3642,7 +3642,34 @@ "slider-color": "Slider color", "slider-color-primary": "Primary", "slider-color-accent": "Accent", - "slider-color-warn": "Warn" + "slider-color-warn": "Warn", + "button-style": "Button style", + "button-raised": "Raised button", + "button-primary": "Primary color", + "button-background-color": "Button background color", + "button-text-color": "Button text color", + "widget-title": "Widget title", + "button-label": "Button label", + "device-attribute-scope": "Device attribute scope", + "server-attribute": "Server attribute", + "shared-attribute": "Shared attribute", + "device-attribute-parameters": "Device attribute parameters", + "is-one-way-command": "Is one way command", + "rpc-method": "RPC method", + "rpc-method-params": "RPC method params", + "show-rpc-error": "Show RPC command execution error", + "led-title": "LED title", + "led-color": "LED color", + "check-status-settings": "Check status settings", + "perform-rpc-status-check": "Perform RPC device status check", + "retrieve-led-status-value-method": "Retrieve led status value using method", + "led-status-value-attribute": "Device attribute containing led status value", + "led-status-value-timeseries": "Device timeseries containing led status value", + "check-status-method": "RPC check device status method", + "parse-led-status-value-function": "Parse led status value function", + "knob-title": "Knob title", + "min-value": "Minimum value", + "max-value": "Maximum value" }, "maps": { "select-entity": "Select entity",