Commit f78b8e55f12d65ce77e580e1a5a7e8cf60c8eb40
Committed by
GitHub
Merge pull request #24 from thingsboard/feature/ui-improvements
Feature/ui improvements
Showing
33 changed files
with
888 additions
and
223 deletions
... | ... | @@ -134,7 +134,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps', |
134 | 134 | |
135 | 135 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
136 | 136 | VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map', |
137 | -'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color.substr(1);\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker; \n }\n \n function createTooltip(marker, pattern, replaceInfo) {\n var infowindow = new google.maps.InfoWindow({\n content: ''''\n });\n marker.addListener(''click'', function() {\n infowindow.open(map, marker);\n });\n tooltips.push( {\n infowindow: infowindow,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n\n function createPolyline(locations, settings) {\n var polyline = new google.maps.Polyline({\n path: locations,\n strokeColor: settings.color,\n strokeOpacity: settings.strokeOpacity,\n strokeWeight: settings.strokeWeight,\n map: map\n });\n \n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n \n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = new google.maps.LatLng(lat, lng);\n locations.push(location);\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getPath();\n if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n route.polyline.setPath(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setPosition(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getPath()) {\n var locations = polyline.getPath();\n for (var i = 0; i < locations.getLength(); i++) {\n bounds.extend(locations.getAt(i));\n }\n }\n }\n \n function loadRoutes(data) {\n var bounds = new google.maps.LatLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var zoomLevel = defaultZoomLevel || map.getZoom();\n this.setZoom(zoomLevel);\n if (!defaultZoomLevel && this.getZoom() > 15) {\n this.setZoom(15);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n if (!dontFitMapBounds) {\n var bounds = new google.maps.LatLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n }\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.infowindow.setContent(text);\n }\n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all routes\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":false,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}],\"defaultZoomLevel\":16},\"title\":\"Route Map\"}"}', | |
137 | +'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 100px;\n white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n }\n\n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n\n var mapId = '''' + Math.random().toString(36).substr(2, 9);\n \n function clearGlobalId() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n }\n }\n \n $window.gm_authFailure = function() {\n if ($window.loadingGmId && $window.loadingGmId === mapId) {\n $window.loadingGmId = null;\n $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n displayError($window.gmApiKeys[apiKey].error);\n }\n };\n \n function displayError(message) {\n $(containerElement).html(\n \"<div class=''error''>\"+ message + \"</div>\"\n );\n }\n\n var initMapFunctionName = ''initGoogleMap_'' + mapId;\n $window[initMapFunctionName] = function() {\n lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n function success() {\n initMap();\n },\n function fail() {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n \n }; \n \n var apiKey = settings.gmApiKey || '''';\n\n if (apiKey && apiKey.length > 0) {\n if (!$window.gmApiKeys) {\n $window.gmApiKeys = {};\n }\n if ($window.gmApiKeys[apiKey]) {\n if ($window.gmApiKeys[apiKey].error) {\n displayError($window.gmApiKeys[apiKey].error);\n } else {\n initMap();\n }\n } else {\n $window.gmApiKeys[apiKey] = {};\n var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n \n $window.loadingGmId = mapId;\n lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n function success() {\n setTimeout(clearGlobalId, 2000);\n },\n function fail(e) {\n clearGloabalId();\n $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n displayError($window.gmApiKeys[apiKey].error);\n }\n );\n }\n } else {\n displayError(''No Google Map Api Key provided!'');\n }\n\n function initMap() {\n \n map = new google.maps.Map(containerElement, {\n scrollwheel: false,\n zoom: defaultZoomLevel || 8\n });\n\n }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color.substr(1);\n var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n new google.maps.Size(21, 34),\n new google.maps.Point(0,0),\n new google.maps.Point(10, 34));\n var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n new google.maps.Size(40, 37),\n new google.maps.Point(0, 0),\n new google.maps.Point(12, 35)); \n var marker;\n if (settings.showLabel) { \n marker = new MarkerWithLabel({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow,\n labelContent: ''<b>''+settings.label+''</b>'',\n labelClass: \"tb-labels\",\n labelAnchor: new google.maps.Point(50, 55)\n }); \n } else {\n marker = new google.maps.Marker({\n position: location, \n map: map,\n icon: pinImage,\n shadow: pinShadow\n }); \n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker; \n }\n \n function createTooltip(marker, pattern, replaceInfo) {\n var infowindow = new google.maps.InfoWindow({\n content: ''''\n });\n marker.addListener(''click'', function() {\n infowindow.open(map, marker);\n });\n tooltips.push( {\n infowindow: infowindow,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n\n function createPolyline(locations, settings) {\n var polyline = new google.maps.Polyline({\n path: locations,\n strokeColor: settings.color,\n strokeOpacity: settings.strokeOpacity,\n strokeWeight: settings.strokeWeight,\n map: map\n });\n \n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (!a[i].equals(b[i])) return false;\n }\n return true;\n }\n \n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = new google.maps.LatLng(lat, lng);\n if (i == 0 || !locations[locations.length-1].equals(location)) {\n locations.push(location);\n }\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getPath();\n if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n route.polyline.setPath(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setPosition(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n }\n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getPath()) {\n var locations = polyline.getPath();\n for (var i = 0; i < locations.getLength(); i++) {\n bounds.extend(locations.getAt(i));\n }\n }\n }\n \n function loadRoutes(data) {\n var bounds = new google.maps.LatLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = new google.maps.LatLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n var newZoomLevel = map.getZoom();\n if (dontFitMapBounds && defaultZoomLevel) {\n newZoomLevel = defaultZoomLevel;\n }\n map.setZoom(newZoomLevel);\n if (!defaultZoomLevel && map.getZoom() > 18) {\n map.setZoom(18);\n }\n });\n map.fitBounds(bounds);\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n google.maps.event.trigger(map, \"resize\");\n if (!dontFitMapBounds) {\n var bounds = new google.maps.LatLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n }\n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n tooltip.infowindow.setContent(text);\n }\n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"gmApiKey\": {\n \"title\": \"Google Maps API Key\",\n \"type\": \"string\"\n },\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all routes\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n \"gmApiKey\"\n ]\n },\n \"form\": [\n \"gmApiKey\",\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}]},\"title\":\"Route Map\"}"}', | |
138 | 138 | 'Route Map' ); |
139 | 139 | |
140 | 140 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
... | ... | @@ -208,6 +208,11 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap', |
208 | 208 | 'OpenStreetMap' ); |
209 | 209 | |
210 | 210 | INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) |
211 | +VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'route_map_openstreetmap', | |
212 | +'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n \n if (settings.defaultZoomLevel) {\n if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n }\n }\n \n dontFitMapBounds = settings.fitMapBounds === false;\n \n function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n var match = varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split('':'');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n \n if (label.startsWith(''#'')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = datasourceOffset + n;\n }\n }\n if (variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < datasource.dataKeys.length; i++) {\n var dataKey = datasource.dataKeys[i];\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = datasourceOffset + i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n \n var configuredRoutesSettings = settings.routesSettings;\n if (!configuredRoutesSettings) {\n configuredRoutesSettings = [];\n }\n \n var datasourceOffset = 0;\n for (var i=0;i<datasources.length;i++) {\n routesSettings[i] = {\n latKeyName: \"lat\",\n lngKeyName: \"lng\",\n showLabel: true,\n label: datasources[i].name, \n color: \"#FE7569\",\n strokeWeight: 2,\n strokeOpacity: 1.0,\n tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n };\n if (configuredRoutesSettings[i]) {\n \n routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n \n routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n \n routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHex() : routesSettings[i].color;\n routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity; \n }\n datasourceOffset += datasources[i].dataKeys.length;\n }\n \n map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n attribution: ''© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n timeWindow, sizeChanged) {\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n \n function padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n \n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n \n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split(''.'');\n s = int - strVal[0].length;\n \n for (; i < s; ++i) {\n strVal[0] = ''0'' + strVal[0];\n }\n \n strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n }\n \n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n \n for (; i < s; ++i) {\n strVal = ''0'' + strVal;\n }\n \n strVal = (n ? ''-'' : '''') + strVal;\n }\n \n return strVal;\n } \n \n function createMarker(location, settings) {\n var pinColor = settings.color;\n\n var icon = L.icon({\n iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n iconSize: [21, 34],\n iconAnchor: [10, 34],\n popupAnchor: [0, -34],\n shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n shadowSize: [40, 37],\n shadowAnchor: [12, 35]\n });\n \n var marker = L.marker(location, {icon: icon}).addTo(map);\n if (settings.showLabel) {\n marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n }\n \n createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n \n return marker;\n }\n \n \n function createTooltip(marker, pattern, replaceInfo) {\n var popup = L.popup();\n popup.setContent('''');\n marker.bindPopup(popup, {autoClose: false, closeOnClick: false});\n tooltips.push( {\n popup: popup,\n pattern: pattern,\n replaceInfo: replaceInfo\n });\n }\n \n function createPolyline(locations, settings) {\n var polyline = L.polyline(locations, \n {\n color: \"#\" + settings.color,\n opacity: settings.strokeOpacity,\n weight: settings.strokeWeight\n }\n ).addTo(map);\n return polyline; \n } \n \n function arraysEqual(a, b) {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a.length != b.length) return false;\n\n for (var i = 0; i < a.length; ++i) {\n if (!a[i].equals(b[i])) return false;\n }\n return true;\n }\n \n function updateRoute(route, data) {\n if (route.latIndex > -1 && route.lngIndex > -1) {\n var latData = data[route.latIndex].data;\n var lngData = data[route.lngIndex].data;\n if (latData.length > 0 && lngData.length > 0) {\n var locations = [];\n for (var i = 0; i < latData.length; i++) {\n var lat = latData[i][1];\n var lng = lngData[i][1];\n var location = L.latLng(lat, lng);\n if (i == 0 || !locations[locations.length-1].equals(location)) {\n locations.push(location);\n }\n }\n var markerLocation;\n if (locations.length > 0) {\n markerLocation = locations[locations.length-1];\n }\n if (!route.polyline) {\n route.polyline = createPolyline(locations, route.settings);\n if (markerLocation) {\n route.marker = createMarker(markerLocation, route.settings);\n }\n polylines.push(route.polyline);\n return true;\n } else {\n var prevPath = route.polyline.getLatLngs();\n if (!prevPath || !arraysEqual(prevPath, locations)) {\n route.polyline.setLatLngs(locations);\n if (markerLocation) {\n if (!route.marker) {\n route.marker = createMarker(markerLocation, route.settings);\n } else {\n route.marker.setLatLng(markerLocation);\n }\n }\n return true;\n }\n }\n }\n }\n return false;\n } \n \n function extendBounds(bounds, polyline) {\n if (polyline && polyline.getLatLngs()) {\n bounds.extend(polyline.getBounds());\n }\n }\n \n function loadRoutes(data) {\n var bounds = L.latLngBounds();\n routes = [];\n var datasourceIndex = -1;\n var routeSettings;\n var datasource;\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n if (!datasource || datasource != datasourceData.datasource) {\n datasourceIndex++;\n datasource = datasourceData.datasource;\n routeSettings = routesSettings[datasourceIndex];\n }\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === routeSettings.latKeyName ||\n dataKey.label === routeSettings.lngKeyName) {\n var route = routes[datasourceIndex];\n if (!route) {\n route = {\n latIndex: -1,\n lngIndex: -1,\n settings: routeSettings\n };\n routes[datasourceIndex] = route;\n } else if (route.polyline) {\n continue;\n }\n if (dataKey.label === routeSettings.latKeyName) {\n route.latIndex = i;\n } else {\n route.lngIndex = i;\n }\n if (route.latIndex > -1 && route.lngIndex > -1) {\n updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n }\n }\n fitMapBounds(bounds);\n }\n \n function updateRoutes(data) {\n var routesChanged = false;\n var bounds = L.latLngBounds();\n for (var r in routes) {\n var route = routes[r];\n routesChanged |= updateRoute(route, data);\n if (route.polyline) {\n extendBounds(bounds, route.polyline);\n }\n }\n if (!dontFitMapBounds && routesChanged) {\n fitMapBounds(bounds);\n }\n }\n \n function fitMapBounds(bounds) {\n map.once(''zoomend'', function(event) {\n var newZoomLevel = map.getZoom();\n if (dontFitMapBounds && defaultZoomLevel) {\n newZoomLevel = defaultZoomLevel;\n }\n map.setZoom(newZoomLevel, {animate: false});\n if (!defaultZoomLevel && this.getZoom() > 18) {\n map.setZoom(18, {animate: false});\n }\n });\n map.fitBounds(bounds, {padding: [50, 50], animate: false});\n }\n\n if (map) {\n if (data) {\n if (!routes) {\n loadRoutes(data);\n } else {\n updateRoutes(data);\n }\n }\n if (sizeChanged) {\n map.invalidateSize(true);\n if (!dontFitMapBounds) {\n var bounds = L.latLngBounds();\n for (var p in polylines) {\n extendBounds(bounds, polylines[p]);\n }\n fitMapBounds(bounds);\n } \n }\n \n for (var t in tooltips) {\n var tooltip = tooltips[t];\n var text = tooltip.pattern;\n var replaceInfo = tooltip.replaceInfo;\n if (replaceInfo && replaceInfo.variables) {\n for (var v in replaceInfo.variables) {\n var variableInfo = replaceInfo.variables[v];\n var txtVal = '''';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n }\n tooltip.popup.setContent(text);\n } \n \n }\n\n};","settingsSchema":"{\n \"schema\": {\n \"title\": \"Route Map Configuration\",\n \"type\": \"object\",\n \"properties\": {\n \"defaultZoomLevel\": {\n \"title\": \"Default map zoom level (1 - 20)\",\n \"type\": \"number\"\n },\n \"fitMapBounds\": {\n \"title\": \"Fit map bounds to cover all markers\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"routesSettings\": {\n \"title\": \"Routes settings, same order as datasources\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Route settings\",\n \"type\": \"object\",\n \"properties\": {\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"lat\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"lng\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"tooltipPattern\": {\n \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units'' )\",\n \"type\": \"string\",\n \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n },\n \"strokeWeight\": {\n \"title\": \"Stroke weight\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"strokeOpacity\": {\n \"title\": \"Stroke opacity\",\n \"type\": \"number\",\n \"default\": 1.0\n }\n }\n }\n }\n },\n \"required\": [\n ]\n },\n \"form\": [\n \"defaultZoomLevel\",\n \"fitMapBounds\",\n {\n \"key\": \"routesSettings\",\n \"items\": [\n \"routesSettings[].latKeyName\",\n \"routesSettings[].lngKeyName\",\n \"routesSettings[].showLabel\",\n \"routesSettings[].label\",\n \"routesSettings[].tooltipPattern\",\n {\n \"key\": \"routesSettings[].color\",\n \"type\": \"color\"\n },\n \"routesSettings[].strokeWeight\",\n \"routesSettings[].strokeOpacity\"\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8950926999078694,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.2757675428823283,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\",\"strokeWeight\":4,\"label\":\"First route\",\"color\":\"#3d5afe\",\"strokeOpacity\":1}]},\"title\":\"Route Map - OpenStreetMap\"}"}', | |
213 | +'Route Map - OpenStreetMap' ); | |
214 | + | |
215 | +INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" ) | |
211 | 216 | VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'temperature_radial_gauge_canvas_gauges', |
212 | 217 | '{"type":"latest","sizeX":6,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n data) {\n gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge''); \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = 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 \"unitTitle\": {\n \"title\": \"Unit title\",\n \"type\": \"string\",\n \"default\": null\n },\n \"showUnitTitle\": {\n \"title\": \"Show unit title\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"units\": {\n \"title\": \"Units\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"majorTicksCount\": {\n \"title\": \"Major ticks count\",\n \"type\": \"number\",\n \"default\": null\n },\n \"minorTicks\": {\n \"title\": \"Minor ticks count\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"valueBox\": {\n \"title\": \"Show value box\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"valueInt\": {\n \"title\": \"Digits count for integer part of value\",\n \"type\": \"number\",\n \"default\": 3\n },\n \"valueDec\": {\n \"title\": \"Digits count for decimal part of value\",\n \"type\": \"number\",\n \"default\": 2\n },\n \"defaultColor\": {\n \"title\": \"Default color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorPlate\": {\n \"title\": \"Plate color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"colorMajorTicks\": {\n \"title\": \"Major ticks color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"colorMinorTicks\": {\n \"title\": \"Minor ticks color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorNeedle\": {\n \"title\": \"Needle color\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleEnd\": {\n \"title\": \"Needle color - end gradient\",\n \"type\": \"string\",\n \"default\": null\n },\n \"colorNeedleShadowUp\": {\n \"title\": \"Upper half of the needle shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(2,255,255,0.2)\"\n },\n \"colorNeedleShadowDown\": {\n \"title\": \"Drop shadow needle color.\",\n \"type\": \"string\",\n \"default\": \"rgba(188,143,143,0.45)\"\n },\n \"colorValueBoxRect\": {\n \"title\": \"Value box rectangle stroke color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n },\n \"colorValueBoxRectEnd\": {\n \"title\": \"Value box rectangle stroke color - end gradient\",\n \"type\": \"string\",\n \"default\": \"#666\"\n },\n \"colorValueBoxBackground\": {\n \"title\": \"Value box background color\",\n \"type\": \"string\",\n \"default\": \"#babab2\"\n },\n \"colorValueBoxShadow\": {\n \"title\": \"Value box shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,1)\"\n },\n \"highlights\": {\n \"title\": \"Highlights\",\n \"type\": \"array\",\n \"items\": {\n \"title\": \"Highlight\",\n \"type\": \"object\",\n \"properties\": {\n \"from\": {\n \"title\": \"From\",\n \"type\": \"number\"\n },\n \"to\": {\n \"title\": \"To\",\n \"type\": \"number\"\n },\n \"color\": {\n \"title\": \"Color\",\n \"type\": \"string\"\n }\n }\n }\n },\n \"highlightsWidth\": {\n \"title\": \"Highlights width\",\n \"type\": \"number\",\n \"default\": 15\n },\n \"showBorder\": {\n \"title\": \"Show border\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"numbersFont\": {\n \"title\": \"Tick numbers font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 18\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": null\n }\n }\n },\n \"titleFont\": {\n \"title\": \"Title text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 24\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"unitsFont\": {\n \"title\": \"Units text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 22\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#888\"\n }\n }\n },\n \"valueFont\": {\n \"title\": \"Value text font\",\n \"type\": \"object\",\n \"properties\": {\n \"family\": {\n \"title\": \"Font family\",\n \"type\": \"string\",\n \"default\": \"RobotoDraft\"\n },\n \"size\": {\n \"title\": \"Size\",\n \"type\": \"number\",\n \"default\": 40\n },\n \"style\": {\n \"title\": \"Style\",\n \"type\": \"string\",\n \"default\": \"normal\"\n },\n \"weight\": {\n \"title\": \"Weight\",\n \"type\": \"string\",\n \"default\": \"500\"\n },\n \"color\": {\n \"title\": \"color\",\n \"type\": \"string\",\n \"default\": \"#444\"\n },\n \"shadowColor\": {\n \"title\": \"Shadow color\",\n \"type\": \"string\",\n \"default\": \"rgba(0,0,0,0.3)\"\n }\n }\n },\n \"animation\": {\n \"title\": \"Enable animation\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"animationDuration\": {\n \"title\": \"Animation duration\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"animationRule\": {\n \"title\": \"Animation rule\",\n \"type\": \"string\",\n \"default\": \"cycle\"\n },\n \"startAngle\": {\n \"title\": \"Start ticks angle\",\n \"type\": \"number\",\n \"default\": 45\n },\n \"ticksAngle\": {\n \"title\": \"Ticks angle\",\n \"type\": \"number\",\n \"default\": 270\n },\n \"needleCircleSize\": {\n \"title\": \"Needle circle size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"startAngle\",\n \"ticksAngle\",\n \"needleCircleSize\",\n \"minValue\",\n \"maxValue\",\n \"unitTitle\",\n \"showUnitTitle\",\n \"units\",\n \"majorTicksCount\",\n \"minorTicks\",\n \"valueBox\",\n \"valueInt\",\n \"valueDec\",\n {\n \"key\": \"defaultColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorPlate\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMajorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorMinorTicks\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedle\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowUp\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorNeedleShadowDown\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRect\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxRectEnd\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxBackground\",\n \"type\": \"color\"\n },\n {\n \"key\": \"colorValueBoxShadow\",\n \"type\": \"color\"\n },\n {\n \"key\": \"highlights\",\n \"items\": [\n \"highlights[].from\",\n \"highlights[].to\",\n {\n \"key\": \"highlights[].color\",\n \"type\": \"color\"\n }\n ]\n },\n \"highlightsWidth\",\n \"showBorder\",\n {\n \"key\": \"numbersFont\",\n \"items\": [\n \"numbersFont.family\",\n \"numbersFont.size\",\n {\n \"key\": \"numbersFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"numbersFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"titleFont\",\n \"items\": [\n \"titleFont.family\",\n \"titleFont.size\",\n {\n \"key\": \"titleFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"titleFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"titleFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"unitsFont\",\n \"items\": [\n \"unitsFont.family\",\n \"unitsFont.size\",\n {\n \"key\": \"unitsFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"unitsFont.color\",\n \"type\": \"color\"\n }\n ]\n },\n {\n \"key\": \"valueFont\",\n \"items\": [\n \"valueFont.family\",\n \"valueFont.size\",\n {\n \"key\": \"valueFont.style\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"italic\",\n \"label\": \"Italic\"\n },\n {\n \"value\": \"oblique\",\n \"label\": \"Oblique\"\n }\n ]\n },\n {\n \"key\": \"valueFont.weight\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"normal\",\n \"label\": \"Normal\"\n },\n {\n \"value\": \"bold\",\n \"label\": \"Bold\"\n },\n {\n \"value\": \"bolder\",\n \"label\": \"Bolder\"\n },\n {\n \"value\": \"lighter\",\n \"label\": \"Lighter\"\n },\n {\n \"value\": \"100\",\n \"label\": \"100\"\n },\n {\n \"value\": \"200\",\n \"label\": \"200\"\n },\n {\n \"value\": \"300\",\n \"label\": \"300\"\n },\n {\n \"value\": \"400\",\n \"label\": \"400\"\n },\n {\n \"value\": \"500\",\n \"label\": \"500\"\n },\n {\n \"value\": \"600\",\n \"label\": \"600\"\n },\n {\n \"value\": \"700\",\n \"label\": \"800\"\n },\n {\n \"value\": \"800\",\n \"label\": \"800\"\n },\n {\n \"value\": \"900\",\n \"label\": \"900\"\n }\n ]\n },\n {\n \"key\": \"valueFont.color\",\n \"type\": \"color\"\n },\n {\n \"key\": \"valueFont.shadowColor\",\n \"type\": \"color\"\n }\n ]\n }, \n \"animation\",\n \"animationDuration\",\n {\n \"key\": \"animationRule\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"linear\",\n \"label\": \"Linear\"\n },\n {\n \"value\": \"quad\",\n \"label\": \"Quad\"\n },\n {\n \"value\": \"quint\",\n \"label\": \"Quint\"\n },\n {\n \"value\": \"cycle\",\n \"label\": \"Cycle\"\n },\n {\n \"value\": \"bounce\",\n \"label\": \"Bounce\"\n },\n {\n \"value\": \"elastic\",\n \"label\": \"Elastic\"\n },\n {\n \"value\": \"dequad\",\n \"label\": \"Dequad\"\n },\n {\n \"value\": \"dequint\",\n \"label\": \"Dequint\"\n },\n {\n \"value\": \"decycle\",\n \"label\": \"Decycle\"\n },\n {\n \"value\": \"debounce\",\n \"label\": \"Debounce\"\n },\n {\n \"value\": \"delastic\",\n \"label\": \"Delastic\"\n }\n ]\n }\n ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":60,\"startAngle\":67.5,\"ticksAngle\":225,\"showBorder\":true,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":-60,\"to\":-50,\"color\":\"#42a5f5\"},{\"from\":-50,\"to\":-40,\"color\":\"rgba(66, 165, 245, 0.83)\"},{\"from\":-40,\"to\":-30,\"color\":\"rgba(66, 165, 245, 0.66)\"},{\"from\":-30,\"to\":-20,\"color\":\"rgba(66, 165, 245, 0.5)\"},{\"from\":-20,\"to\":-10,\"color\":\"rgba(66, 165, 245, 0.33)\"},{\"from\":-10,\"to\":0,\"color\":\"rgba(66, 165, 245, 0.16)\"},{\"from\":0,\"to\":10,\"color\":\"rgba(229, 115, 115, 0.16)\"},{\"from\":10,\"to\":20,\"color\":\"rgba(229, 115, 115, 0.33)\"},{\"from\":20,\"to\":30,\"color\":\"rgba(229, 115, 115, 0.5)\"},{\"from\":30,\"to\":40,\"color\":\"rgba(229, 115, 115, 0.66)\"},{\"from\":40,\"to\":50,\"color\":\"rgba(229, 115, 115, 0.83)\"},{\"from\":50,\"to\":60,\"color\":\"#e57373\"}],\"showUnitTitle\":true,\"colorPlate\":\"#cfd8dc\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"valueDec\":1,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1000,\"animationRule\":\"bounce\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"°C\",\"majorTicksCount\":12,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":20,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"normal\",\"color\":\"#263238\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"family\":\"Segment7Standard\",\"size\":30,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\",\"unitTitle\":\"Temperature\",\"minValue\":-60},\"title\":\"Temperature radial gauge - Canvas Gauges\"}"}', |
213 | 218 | 'Temperature radial gauge - Canvas Gauges' ); | ... | ... |
... | ... | @@ -49,6 +49,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller'; |
49 | 49 | import thingsboardMenu from './services/menu.service'; |
50 | 50 | import thingsboardUtils from './common/utils.service'; |
51 | 51 | import thingsboardTypes from './common/types.constant'; |
52 | +import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; | |
52 | 53 | import thingsboardHelp from './help/help.directive'; |
53 | 54 | import thingsboardToast from './services/toast'; |
54 | 55 | import thingsboardHome from './layout'; |
... | ... | @@ -95,6 +96,7 @@ angular.module('thingsboard', [ |
95 | 96 | thingsboardMenu, |
96 | 97 | thingsboardUtils, |
97 | 98 | thingsboardTypes, |
99 | + thingsboardKeyboardShortcut, | |
98 | 100 | thingsboardHelp, |
99 | 101 | thingsboardToast, |
100 | 102 | thingsboardHome, | ... | ... |
... | ... | @@ -23,6 +23,7 @@ import thingsboardWidget from './widget.directive'; |
23 | 23 | import thingsboardToast from '../services/toast'; |
24 | 24 | import thingsboardTimewindow from './timewindow.directive'; |
25 | 25 | import thingsboardEvents from './tb-event-directives'; |
26 | +import thingsboardMousepointMenu from './mousepoint-menu.directive'; | |
26 | 27 | |
27 | 28 | /* eslint-disable import/no-unresolved, import/default */ |
28 | 29 | |
... | ... | @@ -38,6 +39,7 @@ export default angular.module('thingsboard.directives.dashboard', [thingsboardTy |
38 | 39 | thingsboardWidget, |
39 | 40 | thingsboardTimewindow, |
40 | 41 | thingsboardEvents, |
42 | + thingsboardMousepointMenu, | |
41 | 43 | gridster.name]) |
42 | 44 | .directive('tbDashboard', Dashboard) |
43 | 45 | .name; |
... | ... | @@ -59,7 +61,10 @@ function Dashboard() { |
59 | 61 | isRemoveActionEnabled: '=', |
60 | 62 | onEditWidget: '&?', |
61 | 63 | onRemoveWidget: '&?', |
64 | + onWidgetMouseDown: '&?', | |
62 | 65 | onWidgetClicked: '&?', |
66 | + prepareDashboardContextMenu: '&?', | |
67 | + prepareWidgetContextMenu: '&?', | |
63 | 68 | loadWidgets: '&?', |
64 | 69 | onInit: '&?', |
65 | 70 | onInitFailed: '&?', |
... | ... | @@ -75,8 +80,9 @@ function Dashboard() { |
75 | 80 | function DashboardController($scope, $rootScope, $element, $timeout, $log, toast, types) { |
76 | 81 | |
77 | 82 | var highlightedMode = false; |
78 | - var highlightedIndex = -1; | |
79 | - var mouseDownIndex = -1; | |
83 | + var highlightedWidget = null; | |
84 | + var selectedWidget = null; | |
85 | + var mouseDownWidget = -1; | |
80 | 86 | var widgetMouseMoved = false; |
81 | 87 | |
82 | 88 | var gridsterParent = null; |
... | ... | @@ -117,6 +123,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
117 | 123 | vm.isWidgetExpanded = false; |
118 | 124 | vm.isHighlighted = isHighlighted; |
119 | 125 | vm.isNotHighlighted = isNotHighlighted; |
126 | + vm.selectWidget = selectWidget; | |
127 | + vm.getSelectedWidget = getSelectedWidget; | |
120 | 128 | vm.highlightWidget = highlightWidget; |
121 | 129 | vm.resetHighlight = resetHighlight; |
122 | 130 | |
... | ... | @@ -134,6 +142,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
134 | 142 | vm.removeWidget = removeWidget; |
135 | 143 | vm.loading = loading; |
136 | 144 | |
145 | + vm.openDashboardContextMenu = openDashboardContextMenu; | |
146 | + vm.openWidgetContextMenu = openWidgetContextMenu; | |
147 | + | |
148 | + vm.getEventGridPosition = getEventGridPosition; | |
149 | + | |
150 | + vm.contextMenuItems = []; | |
151 | + vm.contextMenuEvent = null; | |
152 | + | |
153 | + vm.widgetContextMenuItems = []; | |
154 | + vm.widgetContextMenuEvent = null; | |
155 | + | |
137 | 156 | //$element[0].onmousemove=function(){ |
138 | 157 | // widgetMouseMove(); |
139 | 158 | // } |
... | ... | @@ -305,7 +324,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
305 | 324 | } |
306 | 325 | |
307 | 326 | function resetWidgetClick () { |
308 | - mouseDownIndex = -1; | |
327 | + mouseDownWidget = -1; | |
309 | 328 | widgetMouseMoved = false; |
310 | 329 | } |
311 | 330 | |
... | ... | @@ -315,25 +334,27 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
315 | 334 | } |
316 | 335 | |
317 | 336 | function widgetMouseDown ($event, widget) { |
318 | - mouseDownIndex = vm.widgets.indexOf(widget); | |
337 | + mouseDownWidget = widget; | |
319 | 338 | widgetMouseMoved = false; |
339 | + if (vm.onWidgetMouseDown) { | |
340 | + vm.onWidgetMouseDown({event: $event, widget: widget}); | |
341 | + } | |
320 | 342 | } |
321 | 343 | |
322 | 344 | function widgetMouseMove () { |
323 | - if (mouseDownIndex > -1) { | |
345 | + if (mouseDownWidget) { | |
324 | 346 | widgetMouseMoved = true; |
325 | 347 | } |
326 | 348 | } |
327 | 349 | |
328 | 350 | function widgetMouseUp ($event, widget) { |
329 | 351 | $timeout(function () { |
330 | - if (!widgetMouseMoved && mouseDownIndex > -1) { | |
331 | - var index = vm.widgets.indexOf(widget); | |
332 | - if (index === mouseDownIndex) { | |
352 | + if (!widgetMouseMoved && mouseDownWidget) { | |
353 | + if (widget === mouseDownWidget) { | |
333 | 354 | widgetClicked($event, widget); |
334 | 355 | } |
335 | 356 | } |
336 | - mouseDownIndex = -1; | |
357 | + mouseDownWidget = null; | |
337 | 358 | widgetMouseMoved = false; |
338 | 359 | }, 0); |
339 | 360 | } |
... | ... | @@ -347,6 +368,41 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
347 | 368 | } |
348 | 369 | } |
349 | 370 | |
371 | + function openDashboardContextMenu($event, $mdOpenMousepointMenu) { | |
372 | + if (vm.prepareDashboardContextMenu) { | |
373 | + vm.contextMenuItems = vm.prepareDashboardContextMenu(); | |
374 | + if (vm.contextMenuItems && vm.contextMenuItems.length > 0) { | |
375 | + vm.contextMenuEvent = $event; | |
376 | + $mdOpenMousepointMenu($event); | |
377 | + } | |
378 | + } | |
379 | + } | |
380 | + | |
381 | + function openWidgetContextMenu($event, widget, $mdOpenMousepointMenu) { | |
382 | + if (vm.prepareWidgetContextMenu) { | |
383 | + vm.widgetContextMenuItems = vm.prepareWidgetContextMenu({widget: widget}); | |
384 | + if (vm.widgetContextMenuItems && vm.widgetContextMenuItems.length > 0) { | |
385 | + vm.widgetContextMenuEvent = $event; | |
386 | + $mdOpenMousepointMenu($event); | |
387 | + } | |
388 | + } | |
389 | + } | |
390 | + | |
391 | + function getEventGridPosition(event) { | |
392 | + var pos = { | |
393 | + row: 0, | |
394 | + column: 0 | |
395 | + } | |
396 | + var offset = gridsterParent.offset(); | |
397 | + var x = event.pageX - offset.left + gridsterParent.scrollLeft(); | |
398 | + var y = event.pageY - offset.top + gridsterParent.scrollTop(); | |
399 | + if (gridster) { | |
400 | + pos.row = gridster.pixelsToRows(y); | |
401 | + pos.column = gridster.pixelsToColumns(x); | |
402 | + } | |
403 | + return pos; | |
404 | + } | |
405 | + | |
350 | 406 | function editWidget ($event, widget) { |
351 | 407 | resetWidgetClick(); |
352 | 408 | if ($event) { |
... | ... | @@ -367,10 +423,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
367 | 423 | } |
368 | 424 | } |
369 | 425 | |
370 | - function highlightWidget(widgetIndex, delay) { | |
426 | + function highlightWidget(widget, delay) { | |
371 | 427 | highlightedMode = true; |
372 | - highlightedIndex = widgetIndex; | |
373 | - var item = $('.gridster-item', gridster.$element)[widgetIndex]; | |
428 | + highlightedWidget = widget; | |
429 | + var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)]; | |
374 | 430 | if (item) { |
375 | 431 | var height = $(item).outerHeight(true); |
376 | 432 | var rectHeight = gridsterParent.height(); |
... | ... | @@ -385,17 +441,39 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast |
385 | 441 | } |
386 | 442 | } |
387 | 443 | |
444 | + function selectWidget(widget, delay) { | |
445 | + selectedWidget = widget; | |
446 | + var item = $('.gridster-item', gridster.$element)[vm.widgets.indexOf(widget)]; | |
447 | + if (item) { | |
448 | + var height = $(item).outerHeight(true); | |
449 | + var rectHeight = gridsterParent.height(); | |
450 | + var offset = (rectHeight - height) / 2; | |
451 | + var scrollTop = item.offsetTop; | |
452 | + if (offset > 0) { | |
453 | + scrollTop -= offset; | |
454 | + } | |
455 | + gridsterParent.animate({ | |
456 | + scrollTop: scrollTop | |
457 | + }, delay); | |
458 | + } | |
459 | + } | |
460 | + | |
461 | + function getSelectedWidget() { | |
462 | + return selectedWidget; | |
463 | + } | |
464 | + | |
388 | 465 | function resetHighlight() { |
389 | 466 | highlightedMode = false; |
390 | - highlightedIndex = -1; | |
467 | + highlightedWidget = null; | |
468 | + selectedWidget = null; | |
391 | 469 | } |
392 | 470 | |
393 | 471 | function isHighlighted(widget) { |
394 | - return highlightedMode && vm.widgets.indexOf(widget) === highlightedIndex; | |
472 | + return (highlightedMode && highlightedWidget === widget) || (selectedWidget === widget); | |
395 | 473 | } |
396 | 474 | |
397 | 475 | function isNotHighlighted(widget) { |
398 | - return highlightedMode && vm.widgets.indexOf(widget) != highlightedIndex; | |
476 | + return highlightedMode && highlightedWidget != widget; | |
399 | 477 | } |
400 | 478 | |
401 | 479 | function widgetColor(widget) { | ... | ... |
... | ... | @@ -20,6 +20,7 @@ div.tb-widget { |
20 | 20 | height: 100%; |
21 | 21 | margin: 0; |
22 | 22 | overflow: hidden; |
23 | + outline: none; | |
23 | 24 | @include transition(all .2s ease-in-out); |
24 | 25 | |
25 | 26 | .tb-widget-title { |
... | ... | @@ -91,6 +92,7 @@ md-content.tb-dashboard-content { |
91 | 92 | left: 0; |
92 | 93 | right: 0; |
93 | 94 | bottom: 0; |
95 | + outline: none; | |
94 | 96 | } |
95 | 97 | |
96 | 98 | .tb-widget-error-container { | ... | ... |
... | ... | @@ -19,64 +19,90 @@ |
19 | 19 | ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit"> |
20 | 20 | <md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular> |
21 | 21 | </md-content> |
22 | -<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap> | |
23 | - <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;"> | |
24 | - <div id="gridster-child" gridster="vm.gridsterOpts"> | |
25 | - <ul> | |
26 | - <!-- ng-click="widgetClicked($event, widget)" --> | |
27 | - <li gridster-item="widget" ng-repeat="widget in vm.widgets"> | |
28 | - <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp" | |
29 | - ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}" | |
30 | - tb-mousedown="vm.widgetMouseDown($event, widget)" | |
31 | - tb-mousemove="vm.widgetMouseMove($event, widget)" | |
32 | - tb-mouseup="vm.widgetMouseUp($event, widget)" | |
33 | - style=" | |
34 | - cursor: pointer; | |
35 | - color: {{vm.widgetColor(widget)}}; | |
36 | - background-color: {{vm.widgetBackgroundColor(widget)}}; | |
37 | - padding: {{vm.widgetPadding(widget)}} | |
38 | - "> | |
39 | - <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> | |
40 | - <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span> | |
41 | - <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> | |
42 | - </div> | |
43 | - <div class="tb-widget-actions" layout="row" layout-align="start center"> | |
44 | - <md-button id="expand-button" | |
45 | - aria-label="{{ 'fullscreen.fullscreen' | translate }}" | |
46 | - class="md-icon-button md-primary"></md-button> | |
47 | - <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded" | |
48 | - ng-disabled="vm.loading()" | |
49 | - class="md-icon-button md-primary" | |
50 | - ng-click="vm.editWidget($event, widget)" | |
51 | - aria-label="{{ 'widget.edit' | translate }}"> | |
52 | - <md-tooltip md-direction="top"> | |
53 | - {{ 'widget.edit' | translate }} | |
54 | - </md-tooltip> | |
55 | - <md-icon class="material-icons"> | |
56 | - edit | |
57 | - </md-icon> | |
58 | - </md-button> | |
59 | - <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded" | |
60 | - ng-disabled="vm.loading()" | |
61 | - class="md-icon-button md-primary" | |
62 | - ng-click="vm.removeWidget($event, widget)" | |
63 | - aria-label="{{ 'widget.remove' | translate }}"> | |
64 | - <md-tooltip md-direction="top"> | |
65 | - {{ 'widget.remove' | translate }} | |
66 | - </md-tooltip> | |
67 | - <md-icon class="material-icons"> | |
68 | - close | |
69 | - </md-icon> | |
70 | - </md-button> | |
71 | - </div> | |
72 | - <div flex layout="column" class="tb-widget-content"> | |
73 | - <div flex tb-widget | |
74 | - locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }"> | |
22 | +<md-menu md-position-mode="target target" tb-mousepoint-menu> | |
23 | + <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)"> | |
24 | + <div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;"> | |
25 | + <div id="gridster-child" gridster="vm.gridsterOpts"> | |
26 | + <ul> | |
27 | + <!-- ng-click="widgetClicked($event, widget)" --> | |
28 | + <li gridster-item="widget" ng-repeat="widget in vm.widgets"> | |
29 | + <md-menu md-position-mode="target target" tb-mousepoint-menu> | |
30 | + <div tb-expand-fullscreen | |
31 | + expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp" | |
32 | + ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}" | |
33 | + tb-mousedown="vm.widgetMouseDown($event, widget)" | |
34 | + tb-mousemove="vm.widgetMouseMove($event, widget)" | |
35 | + tb-mouseup="vm.widgetMouseUp($event, widget)" | |
36 | + ng-click="" | |
37 | + tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)" | |
38 | + style=" | |
39 | + cursor: pointer; | |
40 | + color: {{vm.widgetColor(widget)}}; | |
41 | + background-color: {{vm.widgetBackgroundColor(widget)}}; | |
42 | + padding: {{vm.widgetPadding(widget)}} | |
43 | + "> | |
44 | + <div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> | |
45 | + <span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span> | |
46 | + <tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> | |
47 | + </div> | |
48 | + <div class="tb-widget-actions" layout="row" layout-align="start center"> | |
49 | + <md-button id="expand-button" | |
50 | + ng-show="!vm.isEdit" | |
51 | + aria-label="{{ 'fullscreen.fullscreen' | translate }}" | |
52 | + class="md-icon-button md-primary"></md-button> | |
53 | + <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded" | |
54 | + ng-disabled="vm.loading()" | |
55 | + class="md-icon-button md-primary" | |
56 | + ng-click="vm.editWidget($event, widget)" | |
57 | + aria-label="{{ 'widget.edit' | translate }}"> | |
58 | + <md-tooltip md-direction="top"> | |
59 | + {{ 'widget.edit' | translate }} | |
60 | + </md-tooltip> | |
61 | + <md-icon class="material-icons"> | |
62 | + edit | |
63 | + </md-icon> | |
64 | + </md-button> | |
65 | + <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded" | |
66 | + ng-disabled="vm.loading()" | |
67 | + class="md-icon-button md-primary" | |
68 | + ng-click="vm.removeWidget($event, widget)" | |
69 | + aria-label="{{ 'widget.remove' | translate }}"> | |
70 | + <md-tooltip md-direction="top"> | |
71 | + {{ 'widget.remove' | translate }} | |
72 | + </md-tooltip> | |
73 | + <md-icon class="material-icons"> | |
74 | + close | |
75 | + </md-icon> | |
76 | + </md-button> | |
77 | + </div> | |
78 | + <div flex layout="column" class="tb-widget-content"> | |
79 | + <div flex tb-widget | |
80 | + locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }"> | |
81 | + </div> | |
82 | + </div> | |
75 | 83 | </div> |
76 | - </div> | |
77 | - </div> | |
78 | - </li> | |
79 | - </ul> | |
84 | + <md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()"> | |
85 | + <md-menu-item ng-repeat ="item in vm.widgetContextMenuItems"> | |
86 | + <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)"> | |
87 | + <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon> | |
88 | + <span translate>{{item.value}}</span> | |
89 | + <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span> | |
90 | + </md-button> | |
91 | + </md-menu-item> | |
92 | + </md-menu-content> | |
93 | + </md-menu> | |
94 | + </li> | |
95 | + </ul> | |
96 | + </div> | |
80 | 97 | </div> |
81 | - </div> | |
82 | -</md-content> | |
\ No newline at end of file | ||
98 | + </md-content> | |
99 | + <md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()"> | |
100 | + <md-menu-item ng-repeat ="item in vm.contextMenuItems"> | |
101 | + <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)"> | |
102 | + <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon> | |
103 | + <span translate>{{item.value}}</span> | |
104 | + <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span> | |
105 | + </md-button> | |
106 | + </md-menu-item> | |
107 | + </md-menu-content> | |
108 | +</md-menu> | |
\ No newline at end of file | ... | ... |
... | ... | @@ -13,6 +13,9 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | + | |
17 | +@import '../../scss/constants'; | |
18 | + | |
16 | 19 | .tb-device-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete { |
17 | 20 | .tb-not-found { |
18 | 21 | display: block; |
... | ... | @@ -27,3 +30,16 @@ |
27 | 30 | white-space: normal !important; |
28 | 31 | } |
29 | 32 | } |
33 | + | |
34 | +tb-datasource-device { | |
35 | + @media (min-width: $layout-breakpoint-gt-sm) { | |
36 | + padding-left: 4px; | |
37 | + padding-right: 4px; | |
38 | + } | |
39 | + tb-device-alias-select { | |
40 | + @media (min-width: $layout-breakpoint-gt-sm) { | |
41 | + width: 200px; | |
42 | + max-width: 200px; | |
43 | + } | |
44 | + } | |
45 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -15,16 +15,16 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<section flex layout='row' layout-align="start center"> | |
19 | - <tb-device-alias-select flex="40" | |
18 | +<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center"> | |
19 | + <tb-device-alias-select | |
20 | 20 | tb-required="true" |
21 | 21 | device-aliases="deviceAliases" |
22 | 22 | ng-model="deviceAlias" |
23 | 23 | on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})"> |
24 | 24 | </tb-device-alias-select> |
25 | - <section flex="120" layout='column'> | |
26 | - <section flex layout='row' layout-align="start center"> | |
27 | - <md-chips flex style="padding-left: 4px;" | |
25 | + <section flex layout='column'> | |
26 | + <section flex layout='column' layout-align="center" style="padding-left: 4px;"> | |
27 | + <md-chips flex | |
28 | 28 | id="timeseries_datakey_chips" |
29 | 29 | ng-required="true" |
30 | 30 | ng-model="timeseriesDataKeys" md-autocomplete-snap |
... | ... | @@ -56,14 +56,19 @@ |
56 | 56 | </md-not-found> |
57 | 57 | </md-autocomplete> |
58 | 58 | <md-chip-template> |
59 | - <div layout="row" layout-align="start center"> | |
59 | + <div layout="row" layout-align="start center" class="tb-attribute-chip"> | |
60 | 60 | <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> |
61 | 61 | <div class="tb-color-result" ng-style="{background: $chip.color}"></div> |
62 | 62 | </div> |
63 | - <div> | |
64 | - {{$chip.label}}: | |
65 | - <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong> | |
66 | - <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong> | |
63 | + <div layout="row" flex> | |
64 | + <div class="tb-chip-label"> | |
65 | + {{$chip.label}} | |
66 | + </div> | |
67 | + <div class="tb-chip-separator">: </div> | |
68 | + <div class="tb-chip-label"> | |
69 | + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong> | |
70 | + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong> | |
71 | + </div> | |
67 | 72 | </div> |
68 | 73 | <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> |
69 | 74 | <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> |
... | ... | @@ -71,7 +76,7 @@ |
71 | 76 | </div> |
72 | 77 | </md-chip-template> |
73 | 78 | </md-chips> |
74 | - <md-chips flex ng-if="widgetType === types.widgetType.latest.value" style="padding-left: 4px;" | |
79 | + <md-chips flex ng-if="widgetType === types.widgetType.latest.value" | |
75 | 80 | id="attribute_datakey_chips" |
76 | 81 | ng-required="true" |
77 | 82 | ng-model="attributeDataKeys" md-autocomplete-snap |
... | ... | @@ -103,19 +108,24 @@ |
103 | 108 | </md-not-found> |
104 | 109 | </md-autocomplete> |
105 | 110 | <md-chip-template> |
106 | - <div layout="row" layout-align="start center"> | |
107 | - <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | |
108 | - <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | |
109 | - </div> | |
110 | - <div> | |
111 | - {{$chip.label}}: | |
112 | - <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong> | |
113 | - <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong> | |
114 | - </div> | |
115 | - <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | |
116 | - <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | |
117 | - </md-button> | |
118 | - </div> | |
111 | + <div layout="row" layout-align="start center" class="tb-attribute-chip"> | |
112 | + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | |
113 | + <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | |
114 | + </div> | |
115 | + <div layout="row" flex> | |
116 | + <div class="tb-chip-label"> | |
117 | + {{$chip.label}} | |
118 | + </div> | |
119 | + <div class="tb-chip-separator">: </div> | |
120 | + <div class="tb-chip-label"> | |
121 | + <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong> | |
122 | + <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong> | |
123 | + </div> | |
124 | + </div> | |
125 | + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | |
126 | + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | |
127 | + </md-button> | |
128 | + </div> | |
119 | 129 | </md-chip-template> |
120 | 130 | </md-chips> |
121 | 131 | </section> | ... | ... |
... | ... | @@ -13,6 +13,9 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | + | |
17 | +@import '../../scss/constants'; | |
18 | + | |
16 | 19 | .tb-func-datakey-autocomplete { |
17 | 20 | .tb-not-found { |
18 | 21 | display: block; |
... | ... | @@ -27,3 +30,9 @@ |
27 | 30 | white-space: normal !important; |
28 | 31 | } |
29 | 32 | } |
33 | + | |
34 | +tb-datasource-func { | |
35 | + @media (min-width: $layout-breakpoint-gt-sm) { | |
36 | + padding-left: 8px; | |
37 | + } | |
38 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -15,8 +15,8 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<section flex layout='column'> | |
19 | - <md-chips flex style="padding-left: 4px;" | |
18 | +<section flex layout='column' style="padding-left: 4px;"> | |
19 | + <md-chips flex | |
20 | 20 | id="function_datakey_chips" |
21 | 21 | ng-required="true" |
22 | 22 | ng-model="funcDataKeys" md-autocomplete-snap |
... | ... | @@ -48,18 +48,23 @@ |
48 | 48 | </md-not-found> |
49 | 49 | </md-autocomplete> |
50 | 50 | <md-chip-template> |
51 | - <div layout="row" layout-align="start center"> | |
52 | - <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | |
53 | - <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | |
54 | - </div> | |
55 | - <div> | |
56 | - {{$chip.label}}: | |
57 | - <strong>{{$chip.name}}</strong> | |
58 | - </div> | |
59 | - <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | |
60 | - <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | |
61 | - </md-button> | |
62 | - </div> | |
51 | + <div layout="row" layout-align="start center" class="tb-attribute-chip"> | |
52 | + <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;"> | |
53 | + <div class="tb-color-result" ng-style="{background: $chip.color}"></div> | |
54 | + </div> | |
55 | + <div layout="row" flex> | |
56 | + <div class="tb-chip-label"> | |
57 | + {{$chip.label}} | |
58 | + </div> | |
59 | + <div class="tb-chip-separator">: </div> | |
60 | + <div class="tb-chip-label"> | |
61 | + <strong>{{$chip.name}}</strong> | |
62 | + </div> | |
63 | + </div> | |
64 | + <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32"> | |
65 | + <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon> | |
66 | + </md-button> | |
67 | + </div> | |
63 | 68 | </md-chip-template> |
64 | 69 | </md-chips> |
65 | 70 | <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert"> | ... | ... |
... | ... | @@ -38,6 +38,7 @@ |
38 | 38 | |
39 | 39 | .tb-color-preview { |
40 | 40 | content: ''; |
41 | + min-width: 24px; | |
41 | 42 | width: 24px; |
42 | 43 | height: 24px; |
43 | 44 | border: 2px solid #fff; |
... | ... | @@ -52,3 +53,14 @@ |
52 | 53 | height: 100%; |
53 | 54 | } |
54 | 55 | } |
56 | + | |
57 | +.tb-attribute-chip { | |
58 | + .tb-chip-label { | |
59 | + overflow: hidden; | |
60 | + text-overflow: ellipsis; | |
61 | + white-space: nowrap; | |
62 | + } | |
63 | + .tb-chip-separator { | |
64 | + white-space: pre; | |
65 | + } | |
66 | +} | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<section flex layout='row' layout-align="start center" class="tb-datasource"> | |
18 | +<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center" class="tb-datasource"> | |
19 | 19 | <md-input-container style="min-width: 110px;"> |
20 | 20 | <md-select placeholder="{{ 'datasource.type' | translate }}" required id="datasourceType" ng-model="model.type"> |
21 | 21 | <md-option ng-repeat="datasourceType in datasourceTypes" value="{{datasourceType}}"> |
... | ... | @@ -23,15 +23,15 @@ |
23 | 23 | </md-option> |
24 | 24 | </md-select> |
25 | 25 | </md-input-container> |
26 | - <section flex layout='row' layout-align="start center" class="datasource" ng-switch on="model.type"> | |
27 | - <tb-datasource-func flex style="padding-left: 8px;" | |
26 | + <section flex class="datasource" ng-switch on="model.type"> | |
27 | + <tb-datasource-func flex | |
28 | 28 | ng-switch-default |
29 | 29 | ng-model="model" |
30 | 30 | datakey-settings-schema="datakeySettingsSchema" |
31 | 31 | ng-required="model.type === types.datasourceType.function" |
32 | 32 | generate-data-key="generateDataKey({chip: chip, type: type})"> |
33 | 33 | </tb-datasource-func> |
34 | - <tb-datasource-device flex style="padding-left: 4px; padding-right: 4px;" | |
34 | + <tb-datasource-device flex | |
35 | 35 | ng-model="model" |
36 | 36 | datakey-settings-schema="datakeySettingsSchema" |
37 | 37 | ng-switch-when="device" | ... | ... |
... | ... | @@ -20,15 +20,28 @@ |
20 | 20 | font-weight: 400; |
21 | 21 | text-transform: uppercase; |
22 | 22 | margin: 20px 8px 0 0; |
23 | + overflow: hidden; | |
24 | + text-overflow: ellipsis; | |
25 | + white-space: nowrap; | |
26 | + width: inherit; | |
23 | 27 | } |
24 | 28 | |
25 | 29 | .tb-details-subtitle { |
26 | 30 | font-size: 1.000rem; |
27 | 31 | margin: 10px 0; |
28 | 32 | opacity: 0.8; |
33 | + overflow: hidden; | |
34 | + text-overflow: ellipsis; | |
35 | + white-space: nowrap; | |
36 | + width: inherit; | |
29 | 37 | } |
30 | 38 | |
31 | 39 | md-sidenav.tb-sidenav-details { |
40 | + .md-toolbar-tools { | |
41 | + min-height: 100px; | |
42 | + max-height: 120px; | |
43 | + height: 100%; | |
44 | + } | |
32 | 45 | width: 100% !important; |
33 | 46 | max-width: 100% !important; |
34 | 47 | z-index: 59 !important; | ... | ... |
... | ... | @@ -22,13 +22,12 @@ |
22 | 22 | layout="column"> |
23 | 23 | <header> |
24 | 24 | <md-toolbar class="md-theme-light" ng-style="{'height':headerHeightPx+'px'}"> |
25 | - <div class="md-toolbar-tools"> | |
26 | - <div class="md-toolbar-tools" layout="column" layout-align="start start"> | |
25 | + <div class="md-toolbar-tools" layout="row"> | |
26 | + <div flex class="md-toolbar-tools" layout="column" layout-align="start start"> | |
27 | 27 | <span class="tb-details-title">{{headerTitle}}</span> |
28 | 28 | <span class="tb-details-subtitle">{{headerSubtitle}}</span> |
29 | 29 | <span style="width: 100%;" ng-transclude="headerPane"></span> |
30 | 30 | </div> |
31 | - <span flex></span> | |
32 | 31 | <div ng-transclude="detailsButtons"></div> |
33 | 32 | <md-button class="md-icon-button" ng-click="closeDetails()"> |
34 | 33 | <md-icon aria-label="close" class="material-icons">close</md-icon> | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +export default angular.module('thingsboard.filters.keyboardShortcut', []) | |
17 | + .filter('keyboardShortcut', KeyboardShortcut) | |
18 | + .name; | |
19 | + | |
20 | +/*@ngInject*/ | |
21 | +function KeyboardShortcut($window) { | |
22 | + return function(str) { | |
23 | + if (!str) return; | |
24 | + var keys = str.split('-'); | |
25 | + var isOSX = /Mac OS X/.test($window.navigator.userAgent); | |
26 | + | |
27 | + var seperator = (!isOSX || keys.length > 2) ? '+' : ''; | |
28 | + | |
29 | + var abbreviations = { | |
30 | + M: isOSX ? '⌘' : 'Ctrl', | |
31 | + A: isOSX ? 'Option' : 'Alt', | |
32 | + S: 'Shift' | |
33 | + }; | |
34 | + | |
35 | + return keys.map(function(key, index) { | |
36 | + var last = index == keys.length - 1; | |
37 | + return last ? key : abbreviations[key]; | |
38 | + }).join(seperator); | |
39 | + }; | |
40 | +} | ... | ... |
1 | +/* | |
2 | + * Copyright © 2016 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +export default angular.module('thingsboard.directives.mousepointMenu', []) | |
18 | + .directive('tbMousepointMenu', MousepointMenu) | |
19 | + .name; | |
20 | + | |
21 | +/*@ngInject*/ | |
22 | +function MousepointMenu() { | |
23 | + | |
24 | + var linker = function ($scope, $element, $attrs, RightClickContextMenu) { | |
25 | + | |
26 | + $scope.$mdOpenMousepointMenu = function($event){ | |
27 | + RightClickContextMenu.offsets = function(){ | |
28 | + var offset = $element.offset(); | |
29 | + var x = $event.pageX - offset.left; | |
30 | + var y = $event.pageY - offset.top; | |
31 | + | |
32 | + var offsets = { | |
33 | + left: x, | |
34 | + top: y | |
35 | + } | |
36 | + return offsets; | |
37 | + } | |
38 | + RightClickContextMenu.open($event); | |
39 | + }; | |
40 | + | |
41 | + $scope.$mdCloseMousepointMenu = function() { | |
42 | + RightClickContextMenu.close(); | |
43 | + } | |
44 | + } | |
45 | + | |
46 | + return { | |
47 | + restrict: "A", | |
48 | + link: linker, | |
49 | + require: 'mdMenu' | |
50 | + }; | |
51 | +} | ... | ... |
... | ... | @@ -20,7 +20,7 @@ const PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; |
20 | 20 | var tbEventDirectives = {}; |
21 | 21 | |
22 | 22 | angular.forEach( |
23 | - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), | |
23 | + 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave contextmenu keydown keyup keypress submit focus blur copy cut paste'.split(' '), | |
24 | 24 | function(eventName) { |
25 | 25 | var directiveName = directiveNormalize('tb-' + eventName); |
26 | 26 | tbEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse) { | ... | ... |
... | ... | @@ -25,7 +25,7 @@ |
25 | 25 | <input name="title" ng-model="title"> |
26 | 26 | </md-input-container> |
27 | 27 | <span translate>widget-config.general-settings</span> |
28 | - <div layout="row" layout-align="start center"> | |
28 | + <div layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center"> | |
29 | 29 | <div layout="row" layout-padding> |
30 | 30 | <md-checkbox flex aria-label="{{ 'widget-config.display-title' | translate }}" |
31 | 31 | ng-model="showTitle">{{ 'widget-config.display-title' | translate }} |
... | ... | @@ -80,7 +80,7 @@ |
80 | 80 | <div flex layout="row" layout-align="start center" |
81 | 81 | style="padding: 0 0 0 10px; margin: 5px;"> |
82 | 82 | <span translate style="min-width: 110px;">widget-config.datasource-type</span> |
83 | - <span translate flex | |
83 | + <span hide show-gt-sm translate flex | |
84 | 84 | style="padding-left: 10px;">widget-config.datasource-parameters</span> |
85 | 85 | <span style="min-width: 40px;"></span> |
86 | 86 | </div> | ... | ... |
... | ... | @@ -55,12 +55,26 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) { |
55 | 55 | if (widgetsBundles.length > 0) { |
56 | 56 | scope.widgetsBundle = widgetsBundles[0]; |
57 | 57 | } |
58 | + } else if (angular.isDefined(scope.selectBundleAlias)) { | |
59 | + selectWidgetsBundleByAlias(scope.selectBundleAlias); | |
58 | 60 | } |
59 | 61 | }, |
60 | 62 | function fail() { |
61 | 63 | } |
62 | 64 | ); |
63 | 65 | |
66 | + function selectWidgetsBundleByAlias(alias) { | |
67 | + if (scope.widgetsBundles && alias) { | |
68 | + for (var w in scope.widgetsBundles) { | |
69 | + var widgetsBundle = scope.widgetsBundles[w]; | |
70 | + if (widgetsBundle.alias === alias) { | |
71 | + scope.widgetsBundle = widgetsBundle; | |
72 | + break; | |
73 | + } | |
74 | + } | |
75 | + } | |
76 | + } | |
77 | + | |
64 | 78 | scope.isSystem = function(item) { |
65 | 79 | return item && item.tenantId.id === types.id.nullUid; |
66 | 80 | } |
... | ... | @@ -79,6 +93,12 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) { |
79 | 93 | scope.updateView(); |
80 | 94 | }); |
81 | 95 | |
96 | + scope.$watch('selectBundleAlias', function (newVal, prevVal) { | |
97 | + if (newVal !== prevVal) { | |
98 | + selectWidgetsBundleByAlias(scope.selectBundleAlias); | |
99 | + } | |
100 | + }); | |
101 | + | |
82 | 102 | $compile(element.contents())(scope); |
83 | 103 | } |
84 | 104 | |
... | ... | @@ -90,7 +110,8 @@ function WidgetsBundleSelect($compile, $templateCache, widgetService, types) { |
90 | 110 | bundlesScope: '@', |
91 | 111 | theForm: '=?', |
92 | 112 | tbRequired: '=?', |
93 | - selectFirstBundle: '=' | |
113 | + selectFirstBundle: '=', | |
114 | + selectBundleAlias: '=?' | |
94 | 115 | } |
95 | 116 | }; |
96 | 117 | } |
\ No newline at end of file | ... | ... |
... | ... | @@ -35,10 +35,12 @@ tb-widgets-bundle-select { |
35 | 35 | |
36 | 36 | tb-widgets-bundle-select, .tb-widgets-bundle-select { |
37 | 37 | .md-text { |
38 | + display: block; | |
38 | 39 | width: 100%; |
39 | 40 | } |
40 | 41 | .tb-bundle-item { |
41 | - display: block; | |
42 | + display: inline-block; | |
43 | + width: 100%; | |
42 | 44 | span { |
43 | 45 | display: inline-block; |
44 | 46 | vertical-align: middle; | ... | ... |
... | ... | @@ -28,6 +28,10 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti |
28 | 28 | |
29 | 29 | vm.gridSettings = gridSettings || {}; |
30 | 30 | |
31 | + if (angular.isUndefined(vm.gridSettings.showTitle)) { | |
32 | + vm.gridSettings.showTitle = true; | |
33 | + } | |
34 | + | |
31 | 35 | vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)'; |
32 | 36 | vm.gridSettings.columns = vm.gridSettings.columns || 24; |
33 | 37 | vm.gridSettings.margins = vm.gridSettings.margins || [10, 10]; | ... | ... |
... | ... | @@ -31,6 +31,11 @@ |
31 | 31 | <md-dialog-content> |
32 | 32 | <div class="md-dialog-content"> |
33 | 33 | <fieldset ng-disabled="loading"> |
34 | + <div layout="row" layout-padding> | |
35 | + <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}" | |
36 | + ng-model="vm.gridSettings.showTitle">{{ 'dashboard.display-title' | translate }} | |
37 | + </md-checkbox> | |
38 | + </div> | |
34 | 39 | <md-input-container class="md-block"> |
35 | 40 | <label translate>dashboard.columns-count</label> |
36 | 41 | <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10" | ... | ... |
... | ... | @@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html'; |
23 | 23 | |
24 | 24 | /*@ngInject*/ |
25 | 25 | export default function DashboardController(types, widgetService, userService, |
26 | - dashboardService, $window, $rootScope, | |
26 | + dashboardService, itembuffer, hotkeys, $window, $rootScope, | |
27 | 27 | $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { |
28 | 28 | |
29 | 29 | var user = userService.getCurrentUser(); |
... | ... | @@ -48,7 +48,10 @@ export default function DashboardController(types, widgetService, userService, |
48 | 48 | vm.addWidgetFromType = addWidgetFromType; |
49 | 49 | vm.dashboardInited = dashboardInited; |
50 | 50 | vm.dashboardInitFailed = dashboardInitFailed; |
51 | + vm.widgetMouseDown = widgetMouseDown; | |
51 | 52 | vm.widgetClicked = widgetClicked; |
53 | + vm.prepareDashboardContextMenu = prepareDashboardContextMenu; | |
54 | + vm.prepareWidgetContextMenu = prepareWidgetContextMenu; | |
52 | 55 | vm.editWidget = editWidget; |
53 | 56 | vm.isTenantAdmin = isTenantAdmin; |
54 | 57 | vm.loadDashboard = loadDashboard; |
... | ... | @@ -63,6 +66,7 @@ export default function DashboardController(types, widgetService, userService, |
63 | 66 | vm.toggleDashboardEditMode = toggleDashboardEditMode; |
64 | 67 | vm.onRevertWidgetEdit = onRevertWidgetEdit; |
65 | 68 | vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType; |
69 | + vm.displayTitle = displayTitle; | |
66 | 70 | |
67 | 71 | vm.widgetsBundle; |
68 | 72 | |
... | ... | @@ -194,6 +198,7 @@ export default function DashboardController(types, widgetService, userService, |
194 | 198 | |
195 | 199 | function dashboardInited(dashboard) { |
196 | 200 | vm.dashboardContainer = dashboard; |
201 | + initHotKeys(); | |
197 | 202 | } |
198 | 203 | |
199 | 204 | function isTenantAdmin() { |
... | ... | @@ -289,18 +294,194 @@ export default function DashboardController(types, widgetService, userService, |
289 | 294 | var delayOffset = transition ? 350 : 0; |
290 | 295 | var delay = transition ? 400 : 300; |
291 | 296 | $timeout(function () { |
292 | - vm.dashboardContainer.highlightWidget(vm.editingWidgetIndex, delay); | |
297 | + vm.dashboardContainer.highlightWidget(widget, delay); | |
293 | 298 | }, delayOffset, false); |
294 | 299 | } |
295 | 300 | } |
296 | 301 | } |
297 | 302 | |
303 | + function widgetMouseDown($event, widget) { | |
304 | + if (vm.isEdit && !vm.isEditingWidget) { | |
305 | + vm.dashboardContainer.selectWidget(widget, 0); | |
306 | + } | |
307 | + } | |
308 | + | |
298 | 309 | function widgetClicked($event, widget) { |
299 | 310 | if (vm.isEditingWidget) { |
300 | 311 | editWidget($event, widget); |
301 | 312 | } |
302 | 313 | } |
303 | 314 | |
315 | + function isHotKeyAllowed(event) { | |
316 | + var target = event.target || event.srcElement; | |
317 | + var scope = angular.element(target).scope(); | |
318 | + return scope && scope.$parent !== $rootScope; | |
319 | + } | |
320 | + | |
321 | + function initHotKeys() { | |
322 | + $translate(['action.copy', 'action.paste', 'action.delete']).then(function (translations) { | |
323 | + hotkeys.bindTo($scope) | |
324 | + .add({ | |
325 | + combo: 'ctrl+c', | |
326 | + description: translations['action.copy'], | |
327 | + callback: function (event) { | |
328 | + if (isHotKeyAllowed(event) && | |
329 | + vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { | |
330 | + var widget = vm.dashboardContainer.getSelectedWidget(); | |
331 | + if (widget) { | |
332 | + event.preventDefault(); | |
333 | + copyWidget(event, widget); | |
334 | + } | |
335 | + } | |
336 | + } | |
337 | + }) | |
338 | + .add({ | |
339 | + combo: 'ctrl+v', | |
340 | + description: translations['action.paste'], | |
341 | + callback: function (event) { | |
342 | + if (isHotKeyAllowed(event) && | |
343 | + vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { | |
344 | + if (itembuffer.hasWidget()) { | |
345 | + event.preventDefault(); | |
346 | + pasteWidget(event); | |
347 | + } | |
348 | + } | |
349 | + } | |
350 | + }) | |
351 | + .add({ | |
352 | + combo: 'ctrl+x', | |
353 | + description: translations['action.delete'], | |
354 | + callback: function (event) { | |
355 | + if (isHotKeyAllowed(event) && | |
356 | + vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { | |
357 | + var widget = vm.dashboardContainer.getSelectedWidget(); | |
358 | + if (widget) { | |
359 | + event.preventDefault(); | |
360 | + removeWidget(event, widget); | |
361 | + } | |
362 | + } | |
363 | + } | |
364 | + }); | |
365 | + }); | |
366 | + } | |
367 | + | |
368 | + function prepareDashboardContextMenu() { | |
369 | + var dashboardContextActions = []; | |
370 | + if (vm.isEdit && !vm.isEditingWidget && !vm.widgetEditMode) { | |
371 | + dashboardContextActions.push( | |
372 | + { | |
373 | + action: openDashboardSettings, | |
374 | + enabled: true, | |
375 | + value: "dashboard.settings", | |
376 | + icon: "settings" | |
377 | + } | |
378 | + ); | |
379 | + dashboardContextActions.push( | |
380 | + { | |
381 | + action: openDeviceAliases, | |
382 | + enabled: true, | |
383 | + value: "device.aliases", | |
384 | + icon: "devices_other" | |
385 | + } | |
386 | + ); | |
387 | + dashboardContextActions.push( | |
388 | + { | |
389 | + action: pasteWidget, | |
390 | + enabled: itembuffer.hasWidget(), | |
391 | + value: "action.paste", | |
392 | + icon: "content_paste", | |
393 | + shortcut: "M-V" | |
394 | + } | |
395 | + ); | |
396 | + } | |
397 | + return dashboardContextActions; | |
398 | + } | |
399 | + | |
400 | + function pasteWidget($event) { | |
401 | + var pos = vm.dashboardContainer.getEventGridPosition($event); | |
402 | + itembuffer.pasteWidget(vm.dashboard, pos); | |
403 | + } | |
404 | + | |
405 | + function prepareWidgetContextMenu() { | |
406 | + var widgetContextActions = []; | |
407 | + if (vm.isEdit && !vm.isEditingWidget) { | |
408 | + widgetContextActions.push( | |
409 | + { | |
410 | + action: editWidget, | |
411 | + enabled: true, | |
412 | + value: "action.edit", | |
413 | + icon: "edit" | |
414 | + } | |
415 | + ); | |
416 | + if (!vm.widgetEditMode) { | |
417 | + widgetContextActions.push( | |
418 | + { | |
419 | + action: copyWidget, | |
420 | + enabled: true, | |
421 | + value: "action.copy", | |
422 | + icon: "content_copy", | |
423 | + shortcut: "M-C" | |
424 | + } | |
425 | + ); | |
426 | + widgetContextActions.push( | |
427 | + { | |
428 | + action: removeWidget, | |
429 | + enabled: true, | |
430 | + value: "action.delete", | |
431 | + icon: "clear", | |
432 | + shortcut: "M-X" | |
433 | + } | |
434 | + ); | |
435 | + } | |
436 | + } | |
437 | + return widgetContextActions; | |
438 | + } | |
439 | + | |
440 | + function copyWidget($event, widget) { | |
441 | + var aliasesInfo = { | |
442 | + datasourceAliases: {}, | |
443 | + targetDeviceAliases: {} | |
444 | + }; | |
445 | + var originalColumns = 24; | |
446 | + if (vm.dashboard.configuration.gridSettings && | |
447 | + vm.dashboard.configuration.gridSettings.columns) { | |
448 | + originalColumns = vm.dashboard.configuration.gridSettings.columns; | |
449 | + } | |
450 | + if (widget.config && vm.dashboard.configuration | |
451 | + && vm.dashboard.configuration.deviceAliases) { | |
452 | + var deviceAlias; | |
453 | + if (widget.config.datasources) { | |
454 | + for (var i=0;i<widget.config.datasources.length;i++) { | |
455 | + var datasource = widget.config.datasources[i]; | |
456 | + if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) { | |
457 | + deviceAlias = vm.dashboard.configuration.deviceAliases[datasource.deviceAliasId]; | |
458 | + if (deviceAlias) { | |
459 | + aliasesInfo.datasourceAliases[i] = { | |
460 | + aliasName: deviceAlias.alias, | |
461 | + deviceId: deviceAlias.deviceId | |
462 | + } | |
463 | + } | |
464 | + } | |
465 | + } | |
466 | + } | |
467 | + if (widget.config.targetDeviceAliasIds) { | |
468 | + for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) { | |
469 | + var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i]; | |
470 | + if (targetDeviceAliasId) { | |
471 | + deviceAlias = vm.dashboard.configuration.deviceAliases[targetDeviceAliasId]; | |
472 | + if (deviceAlias) { | |
473 | + aliasesInfo.targetDeviceAliases[i] = { | |
474 | + aliasName: deviceAlias.alias, | |
475 | + deviceId: deviceAlias.deviceId | |
476 | + } | |
477 | + } | |
478 | + } | |
479 | + } | |
480 | + } | |
481 | + } | |
482 | + itembuffer.copyWidget(widget, aliasesInfo, originalColumns); | |
483 | + } | |
484 | + | |
304 | 485 | function helpLinkIdForWidgetType() { |
305 | 486 | var link = 'widgetsConfig'; |
306 | 487 | if (vm.editingWidget && vm.editingWidget.type) { |
... | ... | @@ -322,6 +503,15 @@ export default function DashboardController(types, widgetService, userService, |
322 | 503 | return link; |
323 | 504 | } |
324 | 505 | |
506 | + function displayTitle() { | |
507 | + if (vm.dashboard && vm.dashboard.configuration.gridSettings && | |
508 | + angular.isDefined(vm.dashboard.configuration.gridSettings.showTitle)) { | |
509 | + return vm.dashboard.configuration.gridSettings.showTitle; | |
510 | + } else { | |
511 | + return true; | |
512 | + } | |
513 | + } | |
514 | + | |
325 | 515 | function onRevertWidgetEdit(widgetForm) { |
326 | 516 | if (widgetForm.$dirty) { |
327 | 517 | widgetForm.$setPristine(); |
... | ... | @@ -331,7 +521,9 @@ export default function DashboardController(types, widgetService, userService, |
331 | 521 | |
332 | 522 | function saveWidget(widgetForm) { |
333 | 523 | widgetForm.$setPristine(); |
334 | - vm.widgets[vm.editingWidgetIndex] = angular.copy(vm.editingWidget); | |
524 | + var widget = angular.copy(vm.editingWidget); | |
525 | + vm.widgets[vm.editingWidgetIndex] = widget; | |
526 | + vm.dashboardContainer.highlightWidget(widget, 0); | |
335 | 527 | } |
336 | 528 | |
337 | 529 | function onEditWidgetClosed() { |
... | ... | @@ -421,8 +613,8 @@ export default function DashboardController(types, widgetService, userService, |
421 | 613 | }); |
422 | 614 | } |
423 | 615 | |
424 | - function toggleDashboardEditMode() { | |
425 | - vm.isEdit = !vm.isEdit; | |
616 | + function setEditMode(isEdit, revert) { | |
617 | + vm.isEdit = isEdit; | |
426 | 618 | if (vm.isEdit) { |
427 | 619 | if (vm.widgetEditMode) { |
428 | 620 | vm.prevWidgets = angular.copy(vm.widgets); |
... | ... | @@ -433,14 +625,23 @@ export default function DashboardController(types, widgetService, userService, |
433 | 625 | if (vm.widgetEditMode) { |
434 | 626 | vm.widgets = vm.prevWidgets; |
435 | 627 | } else { |
436 | - vm.dashboard = vm.prevDashboard; | |
437 | - vm.widgets = vm.dashboard.configuration.widgets; | |
628 | + if (vm.dashboardContainer) { | |
629 | + vm.dashboardContainer.resetHighlight(); | |
630 | + } | |
631 | + if (revert) { | |
632 | + vm.dashboard = vm.prevDashboard; | |
633 | + vm.widgets = vm.dashboard.configuration.widgets; | |
634 | + } | |
438 | 635 | } |
439 | 636 | } |
440 | 637 | } |
441 | 638 | |
639 | + function toggleDashboardEditMode() { | |
640 | + setEditMode(!vm.isEdit, true); | |
641 | + } | |
642 | + | |
442 | 643 | function saveDashboard() { |
443 | - vm.isEdit = false; | |
644 | + setEditMode(false, false); | |
444 | 645 | notifyDashboardUpdated(); |
445 | 646 | } |
446 | 647 | ... | ... |
... | ... | @@ -16,7 +16,7 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 18 | <md-content flex tb-expand-fullscreen="vm.widgetEditMode" hide-expand-button="vm.widgetEditMode"> |
19 | - <section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap | |
19 | + <!--section ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" layout="row" layout-wrap | |
20 | 20 | class="tb-header-buttons tb-top-header-buttons md-fab" ng-style="{'right': '50px'}"> |
21 | 21 | <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit" ng-disabled="loading" |
22 | 22 | class="tb-btn-header md-accent md-hue-2 md-fab md-fab-bottom-right" |
... | ... | @@ -37,7 +37,7 @@ |
37 | 37 | <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}" |
38 | 38 | options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon> |
39 | 39 | </md-button> |
40 | - </section> | |
40 | + </section--> | |
41 | 41 | <section ng-show="!loading && vm.noData()" layout-align="center center" |
42 | 42 | ng-class="{'tb-padded' : !vm.widgetEditMode}" |
43 | 43 | style="text-transform: uppercase; display: flex; z-index: 1;" |
... | ... | @@ -51,7 +51,7 @@ |
51 | 51 | </md-button> |
52 | 52 | </section> |
53 | 53 | <section ng-if="!vm.widgetEditMode" class="tb-dashboard-title" layout="row" layout-align="center center"> |
54 | - <h3 ng-show="!vm.isEdit">{{ vm.dashboard.title }}</h3> | |
54 | + <h3 ng-show="!vm.isEdit && vm.displayTitle()">{{ vm.dashboard.title }}</h3> | |
55 | 55 | <md-input-container ng-show="vm.isEdit" class="md-block" style="height: 30px;"> |
56 | 56 | <label translate>dashboard.title</label> |
57 | 57 | <input class="tb-dashboard-title" required name="title" ng-model="vm.dashboard.title"> |
... | ... | @@ -64,7 +64,7 @@ |
64 | 64 | </md-button> |
65 | 65 | </section> |
66 | 66 | <div class="tb-absolute-fill" |
67 | - ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }"> | |
67 | + ng-class="{ 'tb-padded' : !vm.widgetEditMode && (vm.isEdit || vm.displayTitle()), 'tb-shrinked' : vm.isEditingWidget }"> | |
68 | 68 | <tb-dashboard |
69 | 69 | dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor, |
70 | 70 | 'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')', |
... | ... | @@ -82,7 +82,11 @@ |
82 | 82 | is-edit-action-enabled="vm.isEdit || vm.widgetEditMode" |
83 | 83 | is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode" |
84 | 84 | on-edit-widget="vm.editWidget(event, widget)" |
85 | + on-widget-mouse-down="vm.widgetMouseDown(event, widget)" | |
85 | 86 | on-widget-clicked="vm.widgetClicked(event, widget)" |
87 | + on-widget-context-menu="vm.widgetContextMenu(event, widget)" | |
88 | + prepare-dashboard-context-menu="vm.prepareDashboardContextMenu()" | |
89 | + prepare-widget-context-menu="vm.prepareWidgetContextMenu(widget)" | |
86 | 90 | on-remove-widget="vm.removeWidget(event, widget)" |
87 | 91 | load-widgets="vm.loadDashboard()" |
88 | 92 | on-init="vm.dashboardInited(dashboard)" |
... | ... | @@ -176,8 +180,8 @@ |
176 | 180 | </div> |
177 | 181 | </tb-details-sidenav> |
178 | 182 | <!-- </section> --> |
179 | - <section layout="row" layout-wrap class="tb-footer-buttons md-fab "> | |
180 | - <md-button ng-disabled="loading" ng-if="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode" | |
183 | + <section layout="row" layout-wrap class="tb-footer-buttons md-fab"> | |
184 | + <md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode" | |
181 | 185 | class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)" |
182 | 186 | aria-label="{{ 'dashboard.add-widget' | translate }}"> |
183 | 187 | <md-tooltip md-direction="top"> |
... | ... | @@ -185,5 +189,25 @@ |
185 | 189 | </md-tooltip> |
186 | 190 | <ng-md-icon icon="add"></ng-md-icon> |
187 | 191 | </md-button> |
192 | + <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading" | |
193 | + class="tb-btn-footer md-accent md-hue-2 md-fab" | |
194 | + aria-label="{{ 'action.apply' | translate }}" | |
195 | + ng-click="vm.saveDashboard()"> | |
196 | + <md-tooltip md-direction="top"> | |
197 | + {{ 'action.apply-changes' | translate }} | |
198 | + </md-tooltip> | |
199 | + <ng-md-icon icon="done"></ng-md-icon> | |
200 | + </md-button> | |
201 | + <md-button ng-show="!vm.isAddingWidget && !loading && !vm.widgetEditMode" | |
202 | + ng-if="vm.isTenantAdmin()" ng-disabled="loading" | |
203 | + class="tb-btn-footer md-accent md-hue-2 md-fab" | |
204 | + aria-label="{{ 'action.edit-mode' | translate }}" | |
205 | + ng-click="vm.toggleDashboardEditMode()"> | |
206 | + <md-tooltip md-direction="top"> | |
207 | + {{ (vm.isEdit ? 'action.decline-changes' : 'action.enter-edit-mode') | translate }} | |
208 | + </md-tooltip> | |
209 | + <ng-md-icon icon="{{vm.isEdit ? 'close' : 'edit'}}" | |
210 | + options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon> | |
211 | + </md-button> | |
188 | 212 | </section> |
189 | 213 | </md-content> | ... | ... |
... | ... | @@ -29,6 +29,7 @@ import thingsboardDashboard from '../components/dashboard.directive'; |
29 | 29 | import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive'; |
30 | 30 | import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive'; |
31 | 31 | import thingsboardTypes from '../common/types.constant'; |
32 | +import thingsboardItemBuffer from '../services/item-buffer.service'; | |
32 | 33 | |
33 | 34 | import DashboardRoutes from './dashboard.routes'; |
34 | 35 | import DashboardsController from './dashboards.controller'; |
... | ... | @@ -45,6 +46,7 @@ export default angular.module('thingsboard.dashboard', [ |
45 | 46 | uiRouter, |
46 | 47 | gridster.name, |
47 | 48 | thingsboardTypes, |
49 | + thingsboardItemBuffer, | |
48 | 50 | thingsboardGrid, |
49 | 51 | thingsboardApiWidget, |
50 | 52 | thingsboardApiUser, | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | /*@ngInject*/ |
17 | -export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, dashboardService, deviceId, deviceName, widget) { | |
17 | +export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, deviceId, deviceName, widget) { | |
18 | 18 | |
19 | 19 | var vm = this; |
20 | 20 | |
... | ... | @@ -34,62 +34,20 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, |
34 | 34 | function add() { |
35 | 35 | $scope.theForm.$setPristine(); |
36 | 36 | var theDashboard; |
37 | - var deviceAliases; | |
38 | - widget.col = 0; | |
39 | - widget.sizeX /= 2; | |
40 | - widget.sizeY /= 2; | |
41 | 37 | if (vm.addToDashboardType === 0) { |
42 | 38 | theDashboard = vm.dashboard; |
43 | - if (!theDashboard.configuration) { | |
44 | - theDashboard.configuration = {}; | |
45 | - } | |
46 | - deviceAliases = theDashboard.configuration.deviceAliases; | |
47 | - if (!deviceAliases) { | |
48 | - deviceAliases = {}; | |
49 | - theDashboard.configuration.deviceAliases = deviceAliases; | |
50 | - } | |
51 | - var newAliasId; | |
52 | - for (var aliasId in deviceAliases) { | |
53 | - if (deviceAliases[aliasId].deviceId === deviceId) { | |
54 | - newAliasId = aliasId; | |
55 | - break; | |
56 | - } | |
57 | - } | |
58 | - if (!newAliasId) { | |
59 | - var newAliasName = createDeviceAliasName(deviceAliases, deviceName); | |
60 | - newAliasId = 0; | |
61 | - for (aliasId in deviceAliases) { | |
62 | - newAliasId = Math.max(newAliasId, aliasId); | |
63 | - } | |
64 | - newAliasId++; | |
65 | - deviceAliases[newAliasId] = {alias: newAliasName, deviceId: deviceId}; | |
66 | - } | |
67 | - widget.config.datasources[0].deviceAliasId = newAliasId; | |
68 | - | |
69 | - if (!theDashboard.configuration.widgets) { | |
70 | - theDashboard.configuration.widgets = []; | |
71 | - } | |
72 | - | |
73 | - var row = 0; | |
74 | - for (var w in theDashboard.configuration.widgets) { | |
75 | - var existingWidget = theDashboard.configuration.widgets[w]; | |
76 | - var wRow = existingWidget.row ? existingWidget.row : 0; | |
77 | - var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1; | |
78 | - var bottom = wRow + wSizeY; | |
79 | - row = Math.max(row, bottom); | |
80 | - } | |
81 | - widget.row = row; | |
82 | - theDashboard.configuration.widgets.push(widget); | |
83 | 39 | } else { |
84 | 40 | theDashboard = vm.newDashboard; |
85 | - deviceAliases = {}; | |
86 | - deviceAliases['1'] = {alias: deviceName, deviceId: deviceId}; | |
87 | - theDashboard.configuration = {}; | |
88 | - theDashboard.configuration.widgets = []; | |
89 | - widget.row = 0; | |
90 | - theDashboard.configuration.widgets.push(widget); | |
91 | - theDashboard.configuration.deviceAliases = deviceAliases; | |
92 | 41 | } |
42 | + var aliasesInfo = { | |
43 | + datasourceAliases: {}, | |
44 | + targetDeviceAliases: {} | |
45 | + }; | |
46 | + aliasesInfo.datasourceAliases[0] = { | |
47 | + aliasName: deviceName, | |
48 | + deviceId: deviceId | |
49 | + }; | |
50 | + theDashboard = itembuffer.addWidgetToDashboard(theDashboard, widget, aliasesInfo, 48, -1, -1); | |
93 | 51 | dashboardService.saveDashboard(theDashboard).then( |
94 | 52 | function success(dashboard) { |
95 | 53 | $mdDialog.hide(); |
... | ... | @@ -98,25 +56,6 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, |
98 | 56 | } |
99 | 57 | } |
100 | 58 | ); |
101 | - | |
102 | - } | |
103 | - | |
104 | - function createDeviceAliasName(deviceAliases, alias) { | |
105 | - var c = 0; | |
106 | - var newAlias = angular.copy(alias); | |
107 | - var unique = false; | |
108 | - while (!unique) { | |
109 | - unique = true; | |
110 | - for (var devAliasId in deviceAliases) { | |
111 | - var devAlias = deviceAliases[devAliasId]; | |
112 | - if (newAlias === devAlias.alias) { | |
113 | - c++; | |
114 | - newAlias = alias + c; | |
115 | - unique = false; | |
116 | - } | |
117 | - } | |
118 | - } | |
119 | - return newAlias; | |
120 | 59 | } |
121 | 60 | |
122 | 61 | } | ... | ... |
... | ... | @@ -239,6 +239,8 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS |
239 | 239 | index: 0 |
240 | 240 | } |
241 | 241 | scope.widgetsBundle = null; |
242 | + scope.firstBundle = true; | |
243 | + scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards; | |
242 | 244 | |
243 | 245 | scope.deviceAliases = {}; |
244 | 246 | scope.deviceAliases['1'] = {alias: scope.deviceName, deviceId: scope.deviceId}; |
... | ... | @@ -326,13 +328,6 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS |
326 | 328 | } |
327 | 329 | } |
328 | 330 | }); |
329 | - | |
330 | - widgetService.getWidgetsBundleByAlias(types.systemBundleAlias.cards).then( | |
331 | - function success(widgetsBundle) { | |
332 | - scope.firstBundle = true; | |
333 | - scope.widgetsBundle = widgetsBundle; | |
334 | - } | |
335 | - ); | |
336 | 331 | } |
337 | 332 | |
338 | 333 | scope.exitWidgetMode = function() { |
... | ... | @@ -344,6 +339,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS |
344 | 339 | scope.widgetsIndexWatch(); |
345 | 340 | scope.widgetsIndexWatch = null; |
346 | 341 | } |
342 | + scope.selectedWidgetsBundleAlias = null; | |
347 | 343 | scope.mode = 'default'; |
348 | 344 | scope.getDeviceAttributes(true); |
349 | 345 | } | ... | ... |
... | ... | @@ -105,7 +105,8 @@ |
105 | 105 | <tb-widgets-bundle-select flex-offset="5" |
106 | 106 | flex |
107 | 107 | ng-model="widgetsBundle" |
108 | - select-first-bundle="false"> | |
108 | + select-first-bundle="false" | |
109 | + select-bundle-alias="selectedWidgetsBundleAlias"> | |
109 | 110 | </tb-widgets-bundle-select> |
110 | 111 | </div> |
111 | 112 | <md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)"> | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<div class="tb-breadcrumb"> | |
18 | +<div flex class="tb-breadcrumb" layout="row"> | |
19 | 19 | <h1 flex hide-gt-sm>{{ steps[steps.length-1].ncyBreadcrumbLabel | breadcrumbLabel }}</h1> |
20 | 20 | <span hide-xs hide-sm ng-repeat="step in steps" ng-switch="$last || !!step.abstract"> |
21 | 21 | <a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}"> | ... | ... |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | |
40 | 40 | <div flex layout="column" tabIndex="-1" role="main"> |
41 | 41 | <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}"> |
42 | - <div flex class="md-toolbar-tools"> | |
42 | + <div layout="row" flex class="md-toolbar-tools"> | |
43 | 43 | <md-button id="main" hide-gt-sm |
44 | 44 | class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}"> |
45 | 45 | <md-icon aria-label="{{ 'home.menu' | translate }}" class="material-icons">menu</md-icon> |
... | ... | @@ -47,7 +47,7 @@ |
47 | 47 | <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="searchConfig.showSearch = !searchConfig.showSearch" ng-class="{'tb-invisible': !vm.displaySearchMode()}" > |
48 | 48 | <md-icon aria-label="{{ 'action.back' | translate }}" class="material-icons">arrow_back</md-icon> |
49 | 49 | </md-button> |
50 | - <div flex ng-show="!vm.displaySearchMode()" tb-no-animate flex class="md-toolbar-tools"> | |
50 | + <div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools"> | |
51 | 51 | <span ng-cloak ncy-breadcrumb></span> |
52 | 52 | </div> |
53 | 53 | <md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex> | ... | ... |
ui/src/app/services/item-buffer.service.js
0 → 100644
1 | +/* | |
2 | + * Copyright © 2016 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +import angularStorage from 'angular-storage'; | |
18 | + | |
19 | +export default angular.module('thingsboard.itembuffer', [angularStorage]) | |
20 | + .factory('itembuffer', ItemBuffer) | |
21 | + .factory('bufferStore', function(store) { | |
22 | + var newStore = store.getNamespacedStore('tbBufferStore', null, null, false); | |
23 | + return newStore; | |
24 | + }) | |
25 | + .name; | |
26 | + | |
27 | +/*@ngInject*/ | |
28 | +function ItemBuffer(bufferStore) { | |
29 | + | |
30 | + const WIDGET_ITEM = "widget_item"; | |
31 | + | |
32 | + var service = { | |
33 | + copyWidget: copyWidget, | |
34 | + hasWidget: hasWidget, | |
35 | + pasteWidget: pasteWidget, | |
36 | + addWidgetToDashboard: addWidgetToDashboard | |
37 | + } | |
38 | + | |
39 | + return service; | |
40 | + | |
41 | + /** | |
42 | + aliasesInfo { | |
43 | + datasourceAliases: { | |
44 | + datasourceIndex: { | |
45 | + aliasName: "...", | |
46 | + deviceId: "..." | |
47 | + } | |
48 | + } | |
49 | + targetDeviceAliases: { | |
50 | + targetDeviceAliasIndex: { | |
51 | + aliasName: "...", | |
52 | + deviceId: "..." | |
53 | + } | |
54 | + } | |
55 | + .... | |
56 | + } | |
57 | + **/ | |
58 | + | |
59 | + function copyWidget(widget, aliasesInfo, originalColumns) { | |
60 | + var widgetItem = { | |
61 | + widget: widget, | |
62 | + aliasesInfo: aliasesInfo, | |
63 | + originalColumns: originalColumns | |
64 | + } | |
65 | + bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem)); | |
66 | + } | |
67 | + | |
68 | + function hasWidget() { | |
69 | + return bufferStore.get(WIDGET_ITEM); | |
70 | + } | |
71 | + | |
72 | + function pasteWidget(targetDasgboard, position) { | |
73 | + var widgetItemJson = bufferStore.get(WIDGET_ITEM); | |
74 | + if (widgetItemJson) { | |
75 | + var widgetItem = angular.fromJson(widgetItemJson); | |
76 | + var widget = widgetItem.widget; | |
77 | + var aliasesInfo = widgetItem.aliasesInfo; | |
78 | + var originalColumns = widgetItem.originalColumns; | |
79 | + var targetRow = -1; | |
80 | + var targetColumn = -1; | |
81 | + if (position) { | |
82 | + targetRow = position.row; | |
83 | + targetColumn = position.column; | |
84 | + } | |
85 | + addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn); | |
86 | + } | |
87 | + } | |
88 | + | |
89 | + function addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, row, column) { | |
90 | + var theDashboard; | |
91 | + if (dashboard) { | |
92 | + theDashboard = dashboard; | |
93 | + } else { | |
94 | + theDashboard = {}; | |
95 | + } | |
96 | + if (!theDashboard.configuration) { | |
97 | + theDashboard.configuration = {}; | |
98 | + } | |
99 | + if (!theDashboard.configuration.deviceAliases) { | |
100 | + theDashboard.configuration.deviceAliases = {}; | |
101 | + } | |
102 | + updateAliases(theDashboard, widget, aliasesInfo); | |
103 | + | |
104 | + if (!theDashboard.configuration.widgets) { | |
105 | + theDashboard.configuration.widgets = []; | |
106 | + } | |
107 | + var targetColumns = 24; | |
108 | + if (theDashboard.configuration.gridSettings && | |
109 | + theDashboard.configuration.gridSettings.columns) { | |
110 | + targetColumns = theDashboard.configuration.gridSettings.columns; | |
111 | + } | |
112 | + if (targetColumns != originalColumns) { | |
113 | + var ratio = targetColumns / originalColumns; | |
114 | + widget.sizeX *= ratio; | |
115 | + widget.sizeY *= ratio; | |
116 | + } | |
117 | + if (row > -1 && column > - 1) { | |
118 | + widget.row = row; | |
119 | + widget.col = column; | |
120 | + } else { | |
121 | + row = 0; | |
122 | + for (var w in theDashboard.configuration.widgets) { | |
123 | + var existingWidget = theDashboard.configuration.widgets[w]; | |
124 | + var wRow = existingWidget.row ? existingWidget.row : 0; | |
125 | + var wSizeY = existingWidget.sizeY ? existingWidget.sizeY : 1; | |
126 | + var bottom = wRow + wSizeY; | |
127 | + row = Math.max(row, bottom); | |
128 | + } | |
129 | + widget.row = row; | |
130 | + widget.col = 0; | |
131 | + } | |
132 | + theDashboard.configuration.widgets.push(widget); | |
133 | + return theDashboard; | |
134 | + } | |
135 | + | |
136 | + function updateAliases(dashboard, widget, aliasesInfo) { | |
137 | + var deviceAliases = dashboard.configuration.deviceAliases; | |
138 | + var aliasInfo; | |
139 | + var newAliasId; | |
140 | + for (var datasourceIndex in aliasesInfo.datasourceAliases) { | |
141 | + aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex]; | |
142 | + newAliasId = getDeviceAliasId(deviceAliases, aliasInfo); | |
143 | + widget.config.datasources[datasourceIndex].deviceAliasId = newAliasId; | |
144 | + } | |
145 | + for (var targetDeviceAliasIndex in aliasesInfo.targetDeviceAliases) { | |
146 | + aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex]; | |
147 | + newAliasId = getDeviceAliasId(deviceAliases, aliasInfo); | |
148 | + widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId; | |
149 | + } | |
150 | + } | |
151 | + | |
152 | + function getDeviceAliasId(deviceAliases, aliasInfo) { | |
153 | + var newAliasId; | |
154 | + for (var aliasId in deviceAliases) { | |
155 | + if (deviceAliases[aliasId].deviceId === aliasInfo.deviceId) { | |
156 | + newAliasId = aliasId; | |
157 | + break; | |
158 | + } | |
159 | + } | |
160 | + if (!newAliasId) { | |
161 | + var newAliasName = createDeviceAliasName(deviceAliases, aliasInfo.aliasName); | |
162 | + newAliasId = 0; | |
163 | + for (aliasId in deviceAliases) { | |
164 | + newAliasId = Math.max(newAliasId, aliasId); | |
165 | + } | |
166 | + newAliasId++; | |
167 | + deviceAliases[newAliasId] = {alias: newAliasName, deviceId: aliasInfo.deviceId}; | |
168 | + } | |
169 | + return newAliasId; | |
170 | + } | |
171 | + | |
172 | + function createDeviceAliasName(deviceAliases, alias) { | |
173 | + var c = 0; | |
174 | + var newAlias = angular.copy(alias); | |
175 | + var unique = false; | |
176 | + while (!unique) { | |
177 | + unique = true; | |
178 | + for (var devAliasId in deviceAliases) { | |
179 | + var devAlias = deviceAliases[devAliasId]; | |
180 | + if (newAlias === devAlias.alias) { | |
181 | + c++; | |
182 | + newAlias = alias + c; | |
183 | + unique = false; | |
184 | + } | |
185 | + } | |
186 | + } | |
187 | + return newAlias; | |
188 | + } | |
189 | + | |
190 | + | |
191 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -38,7 +38,9 @@ |
38 | 38 | "create": "Create", |
39 | 39 | "drag": "Drag", |
40 | 40 | "refresh": "Refresh", |
41 | - "undo": "Undo" | |
41 | + "undo": "Undo", | |
42 | + "copy": "Copy", | |
43 | + "paste": "Paste" | |
42 | 44 | }, |
43 | 45 | "admin": { |
44 | 46 | "general": "General", |
... | ... | @@ -211,7 +213,8 @@ |
211 | 213 | "vertical-margin": "Vertical margin", |
212 | 214 | "vertical-margin-required": "Vertical margin value is required.", |
213 | 215 | "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", |
214 | - "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value." | |
216 | + "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", | |
217 | + "display-title": "Display dashboard title" | |
215 | 218 | }, |
216 | 219 | "datakey": { |
217 | 220 | "settings": "Settings", | ... | ... |