diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json new file mode 100644 index 0000000000..c963835ef9 --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -0,0 +1,25 @@ +{ + "widgetsBundle": { + "alias": "gateway_widgets", + "title": "Gateway widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "extension_configuration_widget", + "name": "Extensions table", + "descriptor": { + "type": "latest", + "sizeX": 9, + "sizeY": 6.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "#container {\n overflow: auto;\n}", + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1\n };\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"ExtensionTableSettings\",\n \"properties\": {\n \"extensionsTitle\": {\n \"title\": \"Extension table title\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"extensionsTitle\"\n ]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Extensions table\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } + } + ] +} \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index cb8fdbd913..81f457417a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,6 +30,7 @@ "angular-material": "1.1.1", "angular-material-data-table": "^0.10.9", "angular-material-icons": "^0.7.1", + "angular-material-expansion-panel": "^0.7.2", "angular-messages": "1.5.8", "angular-route": "1.5.8", "angular-sanitize": "1.5.8", diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 0df17b74c7..3c74ecbb4c 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; +import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; import thingsboardRpcWidgets from '../widget/lib/rpc'; @@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, - thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils]) + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 4a6f3f0a8d..5b1908928e 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router'; import angularJwt from 'angular-jwt'; import 'angular-drag-and-drop-lists'; import mdDataTable from 'angular-material-data-table'; +import 'angular-material-expansion-panel'; import ngTouch from 'angular-touch'; import 'angular-carousel'; import 'clipboard'; @@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css'; import 'mdPickers/dist/mdPickers.min.css'; import 'angular-hotkeys/build/hotkeys.min.css'; import 'angular-carousel/dist/angular-carousel.min.css'; +import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css'; import '../scss/main.scss'; import AppConfig from './app.config'; @@ -103,6 +105,7 @@ angular.module('thingsboard', [ angularJwt, 'dndLists', mdDataTable, + 'material.components.expansionPanels', ngTouch, 'angular-carousel', 'ngclipboard', diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 7bac29dff9..5b3c9e4ac4 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', []) name: "event.type-stats" } }, + extensionType: { + http: "HTTP", + mqtt: "MQTT", + opc: "OPC UA" + }, + extensionValueType: { + string: 'value.string', + long: 'value.long', + double: 'value.double', + boolean: 'value.boolean' + }, + extensionTransformerType: { + toDouble: 'extension.to-double', + custom: 'extension.custom' + }, + mqttConverterTypes: { + json: 'extension.converter-json', + custom: 'extension.custom' + }, + mqttCredentialTypes: { + anonymous: { + value: "anonymous", + name: "extension.anonymous" + }, + basic: { + value: "basic", + name: "extension.basic" + }, + pem: { + value: "cert.PEM", + name: "extension.pem" + } + }, + extensionOpcSecurityTypes: { + Basic128Rsa15: "Basic128Rsa15", + Basic256: "Basic256", + Basic256Sha256: "Basic256Sha256", + None: "None" + }, + extensionIdentityType: { + anonymous: "extension.anonymous", + username: "extension.username" + }, + extensionKeystoreType: { + PKCS12: "PKCS12", + JKS: "JKS" + }, latestTelemetry: { value: "LATEST_TELEMETRY", name: "attribute.scope-latest-telemetry", diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index b8394c7958..1ec0134444 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -67,4 +67,11 @@ entity-type="{{vm.types.entityType.device}}"> + + + + diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html index 6f862ba71f..3b19e9d8d7 100644 --- a/ui/src/app/entity/attribute/attribute-table.tpl.html +++ b/ui/src/app/entity/attribute/attribute-table.tpl.html @@ -16,7 +16,7 @@ --> -
+
@@ -26,7 +26,7 @@
-
+
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js new file mode 100644 index 0000000000..fe5825c8ee --- /dev/null +++ b/ui/src/app/extension/extension-dialog.controller.js @@ -0,0 +1,298 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import beautify from 'js-beautify'; + +const js_beautify = beautify.js; + +/*@ngInject*/ +export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) { + + var vm = this; + + vm.types = types; + vm.isAdd = isAdd; + vm.entityType = entityType; + vm.entityId = entityId; + vm.allExtensions = allExtensions; + + + if (extension) { + vm.extension = angular.copy(extension); + editTransformers(vm.extension); + } else { + vm.extension = {}; + } + + + vm.extensionTypeChange = function () { + + if (vm.extension.type === "HTTP") { + vm.extension.configuration = { + "converterConfigurations": [] + }; + } + if (vm.extension.type === "MQTT") { + vm.extension.configuration = { + "brokers": [] + }; + } + if (vm.extension.type === "OPC UA") { + vm.extension.configuration = { + "servers": [] + }; + } + }; + + vm.cancel = cancel; + function cancel() { + $mdDialog.cancel(); + } + + vm.save = save; + function save() { + let $errorElement = angular.element('[name=theForm]').find('.ng-invalid'); + + if ($errorElement.length) { + + let $mdDialogScroll = angular.element('md-dialog-content').scrollTop(); + let $mdDialogTop = angular.element('md-dialog-content').offset().top; + let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top; + + + if ($errorElementTop !== $mdDialogTop) { + angular.element('md-dialog-content').animate({ + scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50 + }, 500); + $errorElement.eq(0).focus(); + } + } else { + + if(vm.isAdd) { + vm.allExtensions.push(vm.extension); + } else { + var index = vm.allExtensions.indexOf(extension); + if(index > -1) { + vm.allExtensions[index] = vm.extension; + } + } + + $mdDialog.hide(); + saveTransformers(); + + var editedValue = angular.toJson(vm.allExtensions); + + attributeService + .saveEntityAttributes( + vm.entityType, + vm.entityId, + types.attributesScope.shared.value, + [{key:"configuration", value:editedValue}] + ) + .then(function success() { + }); + + } + } + + vm.validateId = function() { + var coincidenceArray = vm.allExtensions.filter(function(ext) { + return ext.id == vm.extension.id; + }); + if(coincidenceArray.length) { + if(!vm.isAdd) { + if(coincidenceArray[0].id == extension.id) { + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true); + } else { + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false); + } + } else { + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false); + } + } else { + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true); + } + }; + + function saveTransformers() { + if(vm.extension.type == types.extensionType.http) { + var config = vm.extension.configuration.converterConfigurations; + if(config && config.length > 0) { + for(let i=0;i 0) { + for(let i=0;i 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j + +
+ +
+

{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}

+ + + + +
+
+ + + + + + +
+ +
+
+ + + +
+
extension.field-required
+
extension.unique-id-required
+
+
+ + + + + + + {{value}} + + + +
+
extension.field-required
+
+
+
+
+
+
+
+
+
+
+ + + + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} + + + {{ 'action.cancel' | translate }} + + +
+
\ No newline at end of file diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js new file mode 100644 index 0000000000..5e5b036a13 --- /dev/null +++ b/ui/src/app/extension/extension-table.directive.js @@ -0,0 +1,397 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'angular-material-data-table/dist/md-data-table.min.css'; +import './extension-table.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import extensionTableTemplate from './extension-table.tpl.html'; +import extensionDialogTemplate from './extension-dialog.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +import ExtensionDialogController from './extension-dialog.controller' +import $ from 'jquery'; + +/*@ngInject*/ +export default function ExtensionTableDirective() { + return { + restrict: "E", + scope: true, + bindToController: { + entityId: '=', + entityType: '@', + inWidget: '@?', + ctx: '=?', + entityName: '=' + }, + controller: ExtensionTableController, + controllerAs: 'vm', + templateUrl: extensionTableTemplate + }; +} + +/*@ngInject*/ +function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) { + + let vm = this; + + vm.extensions = []; + vm.allExtensions = []; + vm.selectedExtensions = []; + vm.extensionsCount = 0; + + vm.query = { + order: 'id', + limit: 5, + page: 1, + search: null + }; + + vm.enterFilterMode = enterFilterMode; + vm.exitFilterMode = exitFilterMode; + vm.onReorder = onReorder; + vm.onPaginate = onPaginate; + vm.addExtension = addExtension; + vm.editExtension = editExtension; + vm.deleteExtension = deleteExtension; + vm.deleteExtensions = deleteExtensions; + vm.reloadExtensions = reloadExtensions; + vm.updateExtensions = updateExtensions; + + $scope.$watch("vm.entityId", function(newVal) { + if (newVal) { + if ($scope.subscriber) { + telemetryWebsocketService.unsubscribe($scope.subscriber); + } + + vm.subscribed = false; + vm.syncLastTime = $translate.instant('extension.sync.not-available'); + + subscribeForClientAttributes(); + + reloadExtensions(); + } + }); + + $scope.$watch("vm.query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { + updateExtensions(); + } + }); + + $scope.$watch('vm.selectedExtensions.length', function (newLength) { + var selectionMode = newLength ? true : false; + if (vm.ctx) { + if (selectionMode) { + vm.ctx.hideTitlePanel = true; + $scope.$emit("selectedExtensions", true); + } else if (vm.query.search == null) { + vm.ctx.hideTitlePanel = false; + $scope.$emit("selectedExtensions", false); + } + } + }); + + $scope.$on("showSearch", function($event, source) { + if(source.entityId == vm.entityId) { + enterFilterMode(); + $scope.$emit("filterMode", true); + } + }); + $scope.$on("refreshExtensions", function($event, source) { + if(source.entityId == vm.entityId) { + reloadExtensions(); + } + }); + $scope.$on("addExtension", function($event, source) { + if(source.entityId == vm.entityId) { + addExtension(); + } + }); + $scope.$on("exportExtensions", function($event, source) { + if(source.entityId == vm.entityId) { + vm.exportExtensions(source.entityName); + } + }); + $scope.$on("importExtensions", function($event, source) { + if(source.entityId == vm.entityId) { + vm.importExtensions(); + } + }); + + function enterFilterMode() { + vm.query.search = ''; + if(vm.inWidget) { + vm.ctx.hideTitlePanel = true; + } + } + + function exitFilterMode() { + vm.query.search = null; + updateExtensions(); + if(vm.inWidget) { + vm.ctx.hideTitlePanel = false; + $scope.$emit("filterMode", false); + } + } + + function onReorder() { + updateExtensions(); + } + + function onPaginate() { + updateExtensions(); + } + + function addExtension($event) { + if ($event) { + $event.stopPropagation(); + } + openExtensionDialog($event); + } + + function editExtension($event, extension) { + if ($event) { + $event.stopPropagation(); + } + openExtensionDialog($event, extension); + } + + function openExtensionDialog($event, extension) { + if ($event) { + $event.stopPropagation(); + } + var isAdd = false; + if(!extension) { + isAdd = true; + } + $mdDialog.show({ + controller: ExtensionDialogController, + controllerAs: 'vm', + templateUrl: extensionDialogTemplate, + parent: angular.element($document[0].body), + locals: { + isAdd: isAdd, + allExtensions: vm.allExtensions, + entityId: vm.entityId, + entityType: vm.entityType, + extension: extension + }, + bindToController: true, + targetEvent: $event, + fullscreen: true, + skipHide: true + }).then(function() { + reloadExtensions(); + }, function () { + }); + } + + function deleteExtension($event, extension) { + if ($event) { + $event.stopPropagation(); + } + if(extension) { + var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id}); + var content = $translate.instant('extension.delete-extension-text'); + + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(title) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function() { + var editedExtensions = vm.allExtensions.filter(function(ext) { + return ext.id !== extension.id; + }); + var editedValue = angular.toJson(editedExtensions); + attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then( + function success() { + reloadExtensions(); + } + ); + }); + } + } + + function deleteExtensions($event) { + if ($event) { + $event.stopPropagation(); + } + if (vm.selectedExtensions && vm.selectedExtensions.length > 0) { + var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat'); + var content = $translate.instant('extension.delete-extensions-text'); + + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(title) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + var editedExtensions = angular.copy(vm.allExtensions); + for (var i = 0; i < vm.selectedExtensions.length; i++) { + editedExtensions = editedExtensions.filter(function (ext) { + return ext.id !== vm.selectedExtensions[i].id; + }); + } + var editedValue = angular.toJson(editedExtensions); + attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then( + function success() { + reloadExtensions(); + } + ); + }); + } + } + + function reloadExtensions() { + vm.subscribed = false; + vm.allExtensions.length = 0; + vm.extensions.length = 0; + vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]); + vm.extensionsPromise.then( + function success(data) { + if (data.length) { + vm.allExtensions = angular.fromJson(data[0].value); + } else { + vm.allExtensions = []; + } + + vm.selectedExtensions = []; + updateExtensions(); + vm.extensionsPromise = null; + }, + function fail() { + vm.extensions = []; + vm.selectedExtensions = []; + updateExtensions(); + vm.extensionsPromise = null; + } + ); + } + + function updateExtensions() { + vm.selectedExtensions = []; + var result = $filter('orderBy')(vm.allExtensions, vm.query.order); + if (vm.query.search != null) { + result = $filter('filter')(result, function(extension) { + if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) { + return true; + } + return false; + }); + } + vm.extensionsCount = result.length; + var startIndex = vm.query.limit * (vm.query.page - 1); + vm.extensions = result.slice(startIndex, startIndex + vm.query.limit); + + vm.extensionsJSON = angular.toJson(vm.extensions); + checkForSync(); + } + + function subscribeForClientAttributes() { + if (!vm.subscribed) { + if (vm.entityId && vm.entityType) { + $scope.subscriber = { + subscriptionCommands: [{ + entityType: vm.entityType, + entityId: vm.entityId, + scope: 'CLIENT_SCOPE' + }], + type: 'attribute', + onData: function (data) { + if (data.data) { + onSubscriptionData(data.data); + } + vm.subscribed = true; + } + }; + } + telemetryWebsocketService.subscribe($scope.subscriber); + + } + } + function onSubscriptionData(data) { + + if ($.isEmptyObject(data)) { + vm.appliedConfiguration = undefined; + } else { + if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) { + vm.appliedConfiguration = data.appliedConfiguration[0][1]; + } + } + + updateExtensions(); + $scope.$digest(); + } + + + function checkForSync() { + if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) { + vm.syncStatus = $translate.instant('extension.sync.sync'); + vm.syncLastTime = formatDate(); + $scope.isSync = true; + } else { + vm.syncStatus = $translate.instant('extension.sync.not-sync'); + + $scope.isSync = false; + } + } + + function formatDate(date) { + let d; + if (date) { + d = date; + } else { + d = new Date(); + } + + d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds()); + return d; + + function addZero(num) { + if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) { + num = '0' + num; + } + return num; + } + } + + vm.importExtensions = function($event) { + importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions}); + }; + vm.exportExtensions = function(widgetSourceEntityName) { + if(vm.inWidget) { + importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json'); + } else { + importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json'); + } + }; + + /*change function for widget implementing, like vm.exportExtensions*/ + vm.exportExtension = function($event, extension) { + if ($event) { + $event.stopPropagation(); + } + importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json'); + }; +} \ No newline at end of file diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss new file mode 100644 index 0000000000..7c0a0d8578 --- /dev/null +++ b/ui/src/app/extension/extension-table.scss @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../scss/constants'; + + +.extension-table { + + md-input-container .md-errors-spacer { + min-height: 0; + } + + /*&.tb-data-table table.md-table tbody tr td.tb-action-cell, + &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell { + width: 114px; + }*/ + .sync-widget { + max-height: 90px; + overflow: hidden; + } + .toolbar-widget { + min-height: 39px; + max-height: 39px; + } +} + +.extension__syncStatus--black { + color: #000000!important; +} +.extension__syncStatus--green { + color: #228634!important; +} +.extension__syncStatus--red { + color: #862222!important; +} \ No newline at end of file diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html new file mode 100644 index 0000000000..b86061cdce --- /dev/null +++ b/ui/src/app/extension/extension-table.tpl.html @@ -0,0 +1,159 @@ + + + +
+ +
+ {{ 'extension.extensions' }} + + + + file_upload + + {{ 'extension.import-extensions-configuration' | translate }} + + + + file_download + + {{ 'extension.export-extensions-configuration' | translate }} + + + + add + + {{ 'action.add' | translate }} + + + + search + + {{ 'action.search' | translate }} + + + + refresh + + {{ 'action.refresh' | translate }} + + +
+
+ +
+ + search + + {{ 'action.search' | translate }} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ +
+ extension.selected-extensions + + + delete + + {{ 'action.delete' | translate }} + + +
+
+ +
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
extension.idextension.type 
{{ extension.id }}{{ extension.type }} + + + + + edit + + {{ 'extension.edit' | translate }} + + + + delete + + {{ 'extension.delete' | translate }} + + +
+ +
+ + +
+ +
\ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js new file mode 100644 index 0000000000..37a94443f7 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js @@ -0,0 +1,147 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'brace/ext/language_tools'; +import 'brace/mode/json'; +import 'brace/theme/github'; + +import './extension-form.scss'; + +/* eslint-disable angular/log */ + +import extensionFormHttpTemplate from './extension-form-http.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) { + + var linker = function(scope, element) { + + var template = $templateCache.get(extensionFormHttpTemplate); + element.html(template); + + scope.types = types; + scope.theForm = scope.$parent.theForm; + + scope.extensionCustomTransformerOptions = { + useWrapMode: false, + mode: 'json', + showGutter: true, + showPrintMargin: true, + theme: 'github', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function(_ace) { + _ace.$blockScrolling = 1; + } + }; + + + scope.addConverterConfig = function() { + var newConverterConfig = {converterId:"", converters:[]}; + scope.converterConfigs.push(newConverterConfig); + + scope.converterConfigs[scope.converterConfigs.length - 1].converters = []; + scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters); + }; + + scope.removeConverterConfig = function(config) { + var index = scope.converterConfigs.indexOf(config); + if (index > -1) { + scope.converterConfigs.splice(index, 1); + } + }; + + scope.addConverter = function(converters) { + var newConverter = { + deviceNameJsonExpression:"", + deviceTypeJsonExpression:"", + attributes:[], + timeseries:[] + }; + converters.push(newConverter); + }; + + scope.removeConverter = function(converter, converters) { + var index = converters.indexOf(converter); + if (index > -1) { + converters.splice(index, 1); + } + }; + + scope.addAttribute = function(attributes) { + var newAttribute = {type:"", key:"", value:""}; + attributes.push(newAttribute); + }; + + scope.removeAttribute = function(attribute, attributes) { + var index = attributes.indexOf(attribute); + if (index > -1) { + attributes.splice(index, 1); + } + }; + + + if(scope.isAdd) { + scope.converterConfigs = scope.config.converterConfigurations; + scope.addConverterConfig(); + } else { + scope.converterConfigs = scope.config.converterConfigurations; + } + + scope.transformerTypeChange = function(attribute) { + attribute.transformer = ""; + }; + + scope.validateTransformer = function (model, editorName) { + if(model && model.length) { + try { + angular.fromJson(model); + scope.theForm[editorName].$setValidity('transformerJSON', true); + } catch(e) { + scope.theForm[editorName].$setValidity('transformerJSON', false); + } + } + }; + + scope.collapseValidation = function(index, id) { + var invalidState = angular.element('#'+id+':has(.ng-invalid)'); + if(invalidState.length) { + invalidState.addClass('inner-invalid'); + } + }; + + scope.expandValidation = function (index, id) { + var invalidState = angular.element('#'+id); + invalidState.removeClass('inner-invalid'); + }; + + $compile(element.contents())(scope); + }; + + return { + restrict: "A", + link: linker, + scope: { + config: "=", + isAdd: "=" + } + } +} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html new file mode 100644 index 0000000000..b39794a725 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html @@ -0,0 +1,311 @@ + + + + + extension.configuration + + + + + + + {{ 'extension.converter-configurations' | translate }} + + +
+
    +
  1. + + + + {{ 'action.remove' | translate }} + + + + + + + + +
    +
    extension.field-required
    +
    +
    + + + + + + + + {{ 'extension.converters' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + + + {{ 'extension.attributes' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.field-required
        +
        +
        +
        +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + + + {{value | translate}} + + + +
        + +
        +
        extension.transformer-json
        +
        +
        +
        +
        + +
        + + +
        +
        +
      2. +
      +
      +
      + + add + extension.add-attribute + +
      +
      +
      +
      + + + + + + {{ 'extension.timeseries' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.field-required
        +
        +
        +
        +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + + + {{value | translate}} + + + +
        + +
        +
        extension.transformer-json
        +
        +
        +
        +
        + +
        + + +
        +
        +
      2. +
      +
      +
      + + add + extension.add-timeseries + +
      +
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + add + extension.add-converter + +
    +
    +
    +
    + +
    +
    +
  2. +
+
+
+ + add + extension.add-config + +
+
+
+
+ +
+
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js new file mode 100644 index 0000000000..39a1eb8fcc --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js @@ -0,0 +1,347 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './extension-form.scss'; + +/* eslint-disable angular/log */ + +import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) { + + var linker = function(scope, element) { + + var template = $templateCache.get(extensionFormMqttTemplate); + element.html(template); + + scope.types = types; + scope.theForm = scope.$parent.theForm; + + scope.deviceNameExpressions = { + deviceNameJsonExpression: "extension.converter-json", + deviceNameTopicExpression: "extension.topic" + }; + scope.deviceTypeExpressions = { + deviceTypeJsonExpression: "extension.converter-json", + deviceTypeTopicExpression: "extension.topic" + }; + scope.attributeKeyExpressions = { + attributeKeyJsonExpression: "extension.converter-json", + attributeKeyTopicExpression: "extension.topic" + }; + scope.requestIdExpressions = { + requestIdJsonExpression: "extension.converter-json", + requestIdTopicExpression: "extension.topic" + } + + scope.extensionCustomConverterOptions = { + useWrapMode: false, + mode: 'json', + showGutter: true, + showPrintMargin: true, + theme: 'github', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function(_ace) { + _ace.$blockScrolling = 1; + } + }; + + scope.updateValidity = function () { + if(scope.brokers.length) { + for(let i=0;i -1) { + scope.brokers.splice(index, 1); + } + }; + + if(scope.isAdd) { + scope.brokers = []; + scope.config.brokers = scope.brokers; + scope.addBroker(); + } else { + scope.brokers = scope.config.brokers; + } + + scope.addMap = function(mapping) { + var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}}; + + mapping.push(newMap); + }; + + scope.removeMap = function(map, mapping) { + var index = mapping.indexOf(map); + if (index > -1) { + mapping.splice(index, 1); + } + }; + + scope.addAttribute = function(attributes) { + var newAttribute = {type:"", key:"", value:""}; + attributes.push(newAttribute); + }; + + scope.removeAttribute = function(attribute, attributes) { + var index = attributes.indexOf(attribute); + if (index > -1) { + attributes.splice(index, 1); + } + }; + + scope.addConnectRequest = function(requests, type) { + var newRequest = {}; + if(type == "connect") { + newRequest.topicFilter = "sensors/connect"; + } else { + newRequest.topicFilter = "sensors/disconnect"; + } + requests.push(newRequest); + }; + + scope.addAttributeRequest = function(requests) { + var newRequest = { + topicFilter: "sensors/attributes", + clientScope: false, + responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}", + valueExpression: "${attributeValue}" + }; + requests.push(newRequest); + }; + + scope.addAttributeUpdate = function(updates) { + var newUpdate = { + deviceNameFilter: ".*", + attributeFilter: ".*", + topicExpression: "sensor/${deviceName}/${attributeKey}", + valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}" + } + updates.push(newUpdate); + }; + + scope.addServerSideRpc = function(rpcRequests) { + var newRpc = { + deviceNameFilter: ".*", + methodFilter: "echo", + requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}", + responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}", + responseTimeout: 10000, + valueExpression: "${params}" + }; + rpcRequests.push(newRpc); + }; + + scope.changeCredentials = function(broker) { + var type = broker.credentials.type; + broker.credentials = {}; + broker.credentials.type = type; + }; + + scope.changeConverterType = function(map) { + if(map.converterType == "custom"){ + map.converter = ""; + } + if(map.converterType == "json") { + map.converter = {attributes:[],timeseries:[]}; + } + }; + + scope.changeNameExpression = function(element, type) { + if(element.nameExp == "deviceNameJsonExpression") { + if(element.deviceNameTopicExpression) { + delete element.deviceNameTopicExpression; + } + if(type) { + element.deviceNameJsonExpression = "${$.serialNumber}"; + } + } + if(element.nameExp == "deviceNameTopicExpression") { + if(element.deviceNameJsonExpression) { + delete element.deviceNameJsonExpression; + } + if(type && type == "connect") { + element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)"; + } + if(type && type == "disconnect") { + element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)"; + } + if(type && type == "attribute") { + element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)"; + } + } + }; + + scope.changeTypeExpression = function(converter) { + if(converter.typeExp == "deviceTypeJsonExpression") { + if(converter.deviceTypeTopicExpression) { + delete converter.deviceTypeTopicExpression; + } + } + if(converter.typeExp == "deviceTypeTopicExpression") { + if(converter.deviceTypeJsonExpression) { + delete converter.deviceTypeJsonExpression; + } + } + }; + + scope.changeAttrKeyExpression = function(request) { + if(request.attrKey == "attributeKeyJsonExpression") { + if(request.attributeKeyTopicExpression) { + delete request.attributeKeyTopicExpression; + } + request.attributeKeyJsonExpression = "${$.key}"; + } + if(request.attrKey == "attributeKeyTopicExpression") { + if(request.attributeKeyJsonExpression) { + delete request.attributeKeyJsonExpression; + } + request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)"; + } + }; + + scope.changeRequestIdExpression = function(request) { + if(request.requestId == "requestIdJsonExpression") { + if(request.requestIdTopicExpression) { + delete request.requestIdTopicExpression; + } + request.requestIdJsonExpression = "${$.requestId}"; + } + if(request.requestId == "requestIdTopicExpression") { + if(request.requestIdJsonExpression) { + delete request.requestIdJsonExpression; + } + request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)"; + } + }; + + scope.validateCustomConverter = function(model, editorName) { + if(model && model.length) { + try { + angular.fromJson(model); + scope.theForm[editorName].$setValidity('converterJSON', true); + } catch(e) { + scope.theForm[editorName].$setValidity('converterJSON', false); + } + } + }; + + scope.fileAdded = function($file, broker, fileType) { + var reader = new FileReader(); + reader.onload = function(event) { + scope.$apply(function() { + if(event.target.result) { + scope.theForm.$setDirty(); + var addedFile = event.target.result; + if (addedFile && addedFile.length > 0) { + if(fileType == "caCert") { + broker.credentials.caCertFileName = $file.name; + broker.credentials.caCert = addedFile.replace(/^data.*base64,/, ""); + } + if(fileType == "privateKey") { + broker.credentials.privateKeyFileName = $file.name; + broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, ""); + } + if(fileType == "Cert") { + broker.credentials.certFileName = $file.name; + broker.credentials.cert = addedFile.replace(/^data.*base64,/, ""); + } + } + } + }); + }; + reader.readAsDataURL($file.file); + }; + + scope.clearFile = function(broker, fileType) { + scope.theForm.$setDirty(); + if(fileType == "caCert") { + broker.credentials.caCertFileName = null; + broker.credentials.caCert = null; + } + if(fileType == "privateKey") { + broker.credentials.privateKeyFileName = null; + broker.credentials.privateKey = null; + } + if(fileType == "Cert") { + broker.credentials.certFileName = null; + broker.credentials.cert = null; + } + }; + + scope.collapseValidation = function(index, id) { + var invalidState = angular.element('#'+id+':has(.ng-invalid)'); + if(invalidState.length) { + invalidState.addClass('inner-invalid'); + } + }; + + scope.expandValidation = function (index, id) { + var invalidState = angular.element('#'+id); + invalidState.removeClass('inner-invalid'); + }; + + $compile(element.contents())(scope); + }; + + return { + restrict: "A", + link: linker, + scope: { + config: "=", + isAdd: "=" + } + } +} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html new file mode 100644 index 0000000000..f665fb0ef0 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html @@ -0,0 +1,860 @@ + + + + + extension.configuration + + + + + + + {{ 'extension.brokers' | translate }} + + +
+
    +
  1. + + + + {{ 'action.remove' | translate }} + + + + +
    + + + +
    +
    extension.field-required
    +
    extension.port-range
    +
    extension.port-range
    +
    +
    + + + +
    +
    extension.field-required
    +
    +
    +
    +
    + + + +
    +
    extension.field-required
    +
    +
    + + + + + {{credentialsValue.name | translate}} + + + + + {{ 'extension.ssl' | translate }} + + +
    +
    + + + +
    +
    extension.field-required
    +
    +
    + + + +
    +
    extension.field-required
    +
    +
    +
    + + + + + + {{ 'extension.mapping' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      + +
      +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      +
      + + + + + + + +
      +
      extension.field-required
      +
      +
      +
      +
      + +
      +
      extension.transformer-json
      +
      +
      +
      +
      + +
      + + + + + {{ 'extension.attributes' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.field-required
        +
        +
        +
        + + + +
        +
        extension.field-required
        +
        +
        +
        +
        +
      2. +
      +
      +
      + + add + extension.add-attribute + +
      +
      +
      +
      + + + + + {{ 'extension.timeseries' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.field-required
        +
        +
        +
        + + + +
        +
        extension.field-required
        +
        +
        +
        +
        +
      2. +
      +
      +
      + + add + extension.add-timeseries + +
      +
      +
      +
      + +
      +
      +
    2. +
    +
    +
    + + add + extension.add-map + +
    +
    +
    +
    + + + + + {{ 'extension.connect-requests' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + + + + +
      +
      extension.field-required
      +
      +
      +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + add + extension.add-connect-request + +
    +
    +
    +
    + + + + + {{ 'extension.disconnect-requests' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + + + + +
      +
      extension.field-required
      +
      +
      +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + add + extension.add-disconnect-request + +
    +
    +
    +
    + + + + + {{ 'extension.attribute-requests' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + +
      + + + +
      +
      extension.field-required
      +
      +
      + + {{ 'extension.client-scope' | translate }} + + +
      +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      + +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      + +
      + + + + + {{value | translate}} + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      + + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + add + extension.add-attribute-request + +
    +
    +
    +
    + + + + + {{ 'extension.attribute-updates' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + add + extension.add-attribute-update + +
    +
    +
    +
    + + + + + {{ 'extension.server-side-rpc' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + +
      + + + +
      +
      extension.field-required
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      +
      + + + +
      +
      extension.field-required
      +
      +
      + + + + +
      + + + + + + + +
      +
      extension.field-required
      +
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + add + extension.add-server-side-rpc-request + +
    +
    +
    +
    + +
    +
    +
  2. +
+
+ +
+ + add + extension.add-broker + +
+
+
+
+ +
+
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js new file mode 100644 index 0000000000..fc69b68f9b --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js @@ -0,0 +1,173 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'brace/ext/language_tools'; +import 'brace/mode/json'; +import 'brace/theme/github'; + +import './extension-form.scss'; + +/* eslint-disable angular/log */ + +import extensionFormOpcTemplate from './extension-form-opc.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) { + + + var linker = function(scope, element) { + + + function Server() { + this.applicationName = "Thingsboard OPC-UA client"; + this.applicationUri = ""; + this.host = "localhost"; + this.port = 49320; + this.scanPeriodInSeconds = 10; + this.timeoutInMillis = 5000; + this.security = "Basic128Rsa15"; + this.identity = { + "type": "anonymous" + }; + this.keystore = { + "type": "PKCS12", + "location": "example.pfx", + "password": "secret", + "alias": "gateway", + "keyPassword": "secret" + }; + this.mapping = [] + } + + function Map() { + this.deviceNodePattern = "Channel1\\.Device\\d+$"; + this.deviceNamePattern = "Device ${_System._DeviceId}"; + this.attributes = []; + this.timeseries = []; + } + + function Attribute() { + this.key = "Tag1"; + this.type = "string"; + this.value = "${Tag1}"; + } + + function Timeseries() { + this.key = "Tag2"; + this.type = "long"; + this.value = "${Tag2}"; + } + + + var template = $templateCache.get(extensionFormOpcTemplate); + element.html(template); + + scope.types = types; + scope.theForm = scope.$parent.theForm; + + + if (!scope.configuration.servers.length) { + scope.configuration.servers.push(new Server()); + } + + scope.addServer = function(serversList) { + serversList.push(new Server()); + // scope.addMap(serversList[serversList.length-1].mapping); + + scope.theForm.$setDirty(); + }; + + scope.addMap = function(mappingList) { + mappingList.push(new Map()); + scope.theForm.$setDirty(); + }; + + scope.addNewAttribute = function(attributesList) { + attributesList.push(new Attribute()); + scope.theForm.$setDirty(); + }; + + scope.addNewTimeseries = function(timeseriesList) { + timeseriesList.push(new Timeseries()); + scope.theForm.$setDirty(); + }; + + + scope.removeItem = (item, itemList) => { + var index = itemList.indexOf(item); + if (index > -1) { + itemList.splice(index, 1); + } + scope.theForm.$setDirty(); + }; + + + $compile(element.contents())(scope); + + + scope.fileAdded = function($file, model, options) { + let reader = new FileReader(); + reader.onload = function(event) { + scope.$apply(function() { + if(event.target.result) { + scope.theForm.$setDirty(); + let addedFile = event.target.result; + + if (addedFile && addedFile.length > 0) { + model[options.fileName] = $file.name; + model[options.file] = addedFile.replace(/^data.*base64,/, ""); + + } + } + }); + }; + reader.readAsDataURL($file.file); + + }; + + scope.clearFile = function(model, options) { + scope.theForm.$setDirty(); + + model[options.fileName] = null; + model[options.file] = null; + + }; + + scope.collapseValidation = function(index, id) { + var invalidState = angular.element('#'+id+':has(.ng-invalid)'); + if(invalidState.length) { + invalidState.addClass('inner-invalid'); + } + }; + + scope.expandValidation = function (index, id) { + var invalidState = angular.element('#'+id); + invalidState.removeClass('inner-invalid'); + }; + + }; + + return { + restrict: "A", + link: linker, + scope: { + configuration: "=", + isAdd: "=" + } + } +} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html new file mode 100644 index 0000000000..01b4f86355 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html @@ -0,0 +1,557 @@ + + + + + extension.configuration + + + + + + + + {{ 'extension.opc-server' | translate }} + + + +
+ extension.opc-add-server-prompt +
+ +
+
    +
  1. + + + + {{ 'action.remove' | translate }} + + + + + + +
    + + + +
    +
    extension.field-required
    +
    +
    + + + + + +
    +
    extension.field-required
    +
    +
    +
    + + +
    + + + +
    +
    extension.field-required
    +
    +
    + + + + +
    +
    extension.field-required
    +
    Port should be in a range from 1 to 65535
    +
    Port should be in a range from 1 to 65535
    +
    +
    +
    + +
    + + + +
    +
    extension.field-required
    +
    +
    + + + + +
    +
    extension.field-required
    +
    +
    +
    + +
    + + + + + + +
    +
    extension.field-required
    +
    +
    + + + + + + +
    +
    extension.field-required
    +
    +
    +
    + +
    + +
    +
    + + + +
    +
    extension.field-required
    +
    +
    + + + + +
    +
    extension.field-required
    +
    +
    +
    + + + + + {{ 'extension.opc-keystore' | translate }} + + + + + + + + +
    +
    extension.field-required
    +
    +
    + + + + +
    + + + +
    +
    extension.field-required
    +
    +
    + + + + +
    +
    extension.field-required
    +
    +
    +
    + + + + +
    +
    extension.field-required
    +
    +
    + +
    +
    +
    + + + + + + {{ 'extension.mapping' | translate }} + + +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + + +
      + + + +
      +
      extension.field-required
      +
      +
      + + + + +
      +
      extension.field-required
      +
      +
      +
      + + + + + + {{ 'extension.attributes' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + + +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.required-type
        +
        +
        +
        + +
        + + + +
        +
        extension.field-required
        +
        +
        + +
        + + +
        +
        +
      2. +
      +
      +
      + + add + add-attribute + +
      +
      +
      +
      + + + + + {{ 'extension.timeseries' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.field-required
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.field-required
        +
        +
        +
        +
        + + + +
        +
        extension.required-value
        +
        +
        +
        +
        +
        +
      2. +
      +
      +
      + + add + extension.add-timeseries + +
      +
      +
      +
      + + +
      +
      +
    2. +
    +
    +
    + + add + extension.add-map + +
    +
    +
    +
    + +
    +
    +
  2. +
+ +
+ + add + extension.opc-add-server + +
+ +
+
+
+
+ +
+
\ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss new file mode 100644 index 0000000000..97ac7174c4 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form.scss @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.extension-form { + li > .md-button { + color: rgba(0, 0, 0, 0.7); + margin: 0; + } + .vAccordion--default { + margin-top: 0; + padding-left: 3px; + } + .tb-container { + width:100%; + } + .dropdown-messages { + .tb-error-message { + padding: 5px 0 0 0; + } + } + .dropdown-section { + margin-bottom: 30px; + } + v-pane.inner-invalid > v-pane-header { + border-bottom: 2px solid rgb(221,44,0); + } +} + +.extension-form.extension-mqtt { + md-checkbox{ + margin-left: 10px; + } +} + +.tb-extension-custom-transformer-panel { + margin-left: 15px; + border: 1px solid #C0C0C0; + height: 100%; + .tb-extension-custom-transformer { + min-width: 600px; + min-height: 200px; + width: 100%; + height: 100%; + } + .ace_text-input { + position:absolute!important + } +} + +.extensionDialog { + min-width: 1000px; +} + +.tb-container-for-select { + height: 58px; +} + +.tb-drop-file-input-hide { + height: 200%; + display: block; + position: absolute; + bottom: 0; + width: 100%; +} \ No newline at end of file diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js new file mode 100644 index 0000000000..acb97be60d --- /dev/null +++ b/ui/src/app/extension/index.js @@ -0,0 +1,29 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ExtensionTableDirective from './extension-table.directive'; +import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive'; +import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive' +import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive'; +import {ParseToNull} from './extension-dialog.controller'; + +export default angular.module('thingsboard.extension', []) + .directive('tbExtensionTable', ExtensionTableDirective) + .directive('tbExtensionFormHttp', ExtensionFormHttpDirective) + .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective) + .directive('tbExtensionFormOpc', ExtensionFormOpcDirective) + .directive('parseToNull', ParseToNull) + .name; \ No newline at end of file diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index 86d5240953..62e1bb3e2d 100644 --- a/ui/src/app/import-export/import-export.service.js +++ b/ui/src/app/import-export/import-export.service.js @@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; /* eslint-disable no-undef, angular/window-service, angular/document-service */ /*@ngInject*/ -export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils, - entityService, dashboardService, pluginService, ruleService, widgetService, toast) { +export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, + dashboardUtils, entityService, dashboardService, pluginService, ruleService, + widgetService, toast, attributeService) { var service = { @@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, exportWidgetType: exportWidgetType, importWidgetType: importWidgetType, exportWidgetsBundle: exportWidgetsBundle, - importWidgetsBundle: importWidgetsBundle - } + importWidgetsBundle: importWidgetsBundle, + exportExtension: exportExtension, + importExtension: importExtension, + exportToPc: exportToPc + }; return service; @@ -614,6 +618,84 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return true; } + + + function exportExtension(extensionId) { + + getExtension(extensionId) + .then( + function success(extension) { + var name = extension.title; + name = name.toLowerCase().replace(/\W/g,"_"); + exportToPc(prepareExport(extension), name + '.json'); + }, + function fail(rejection) { + var message = rejection; + if (!message) { + message = $translate.instant('error.unknown-error'); + } + toast.showError($translate.instant('extension.export-failed-error', {error: message})); + } + ); + + function getExtension(extensionId) { + var deferred = $q.defer(); + var url = '/api/plugins/telemetry/DEVICE/' + extensionId; + $http.get(url, null) + .then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + } + + function importExtension($event, options) { + var deferred = $q.defer(); + openImportDialog($event, 'extension.import-extensions', 'extension.file') + .then( + function success(extension) { + if (!validateImportedExtension(extension)) { + toast.showError($translate.instant('extension.invalid-file-error')); + deferred.reject(); + } else { + attributeService + .saveEntityAttributes( + options.entityType, + options.entityId, + types.attributesScope.shared.value, + [{ + key: "configuration", + value: angular.toJson(extension) + }] + ) + .then(function success() { + options.successFunc(); + }); + } + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + function validateImportedExtension(configuration) { + if (configuration.length) { + for (let i = 0; i < configuration.length; i++) { + if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) { + return false; + } + } + } else { + return false; + } + return true; + } + function processEntityAliases(entityAliases, aliasIds) { var deferred = $q.defer(); var missingEntityAliases = {}; diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 9d3e1d16c6..c23b0086a6 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive'; import thingsboardEntity from '../entity'; import thingsboardEvent from '../event'; import thingsboardAlarm from '../alarm'; +import thingsboardExtension from '../extension'; import thingsboardTenant from '../tenant'; import thingsboardCustomer from '../customer'; import thingsboardUser from '../user'; @@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [ thingsboardEntity, thingsboardEvent, thingsboardAlarm, + thingsboardExtension, thingsboardTenant, thingsboardCustomer, thingsboardUser, diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index bd1b9e99b0..ab672de30d 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -729,6 +729,133 @@ export default angular.module('thingsboard.locale', []) "messages-processed": "Messages processed", "errors-occurred": "Errors occurred" }, + "extension": { + "extensions": "Extensions", + "selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected", + "type": "Type", + "key": "Key", + "value": "Value", + "id": "Id", + "extension-id": "Extension id", + "extension-type": "Extension type", + "transformer-json": "JSON *", + "unique-id-required": "Current extension id already exists.", + "delete": "Delete extension", + "add": "Add extension", + "edit": "Edit extension", + "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", + "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", + "delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?", + "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", + "converters": "Converters", + "converter-id": "Converter id", + "configuration": "Configuration", + "converter-configurations": "Converter configurations", + "token": "Security token", + "add-converter": "Add converter", + "add-config": "Add converter configuration", + "device-name-expression": "Device name expression", + "device-type-expression": "Device type expression", + "custom": "Custom", + "to-double": "To Double", + "transformer": "Transformer", + "json-required": "Transformer json is required.", + "json-parse": "Unable to parse transformer json.", + "attributes": "Attributes", + "add-attribute": "Add attribute", + "add-map": "Add mapping element", + "timeseries": "Timeseries", + "add-timeseries": "Add timeseries", + "field-required": "Field is required", + "brokers": "Brokers", + "add-broker": "Add broker", + "host": "Host", + "port": "Port", + "port-range": "Port should be in a range from 1 to 65535.", + "ssl": "Ssl", + "credentials": "Credentials", + "username": "Username", + "password": "Password", + "retry-interval": "Retry interval in milliseconds", + "anonymous": "Anonymous", + "basic": "Basic", + "pem": "PEM", + "ca-cert": "CA certificate file *", + "private-key": "Private key file *", + "cert": "Certificate file *", + "no-file": "No file selected.", + "drop-file": "Drop a file or click to select a file to upload.", + "mapping": "Mapping", + "topic-filter": "Topic filter", + "converter-type": "Converter type", + "converter-json": "Json", + "json-name-expression": "Device name json expression", + "topic-name-expression": "Device name topic expression", + "json-type-expression": "Device type json expression", + "topic-type-expression": "Device type topic expression", + "attribute-key-expression": "Attribute key expression", + "attr-json-key-expression": "Attribute key json expression", + "attr-topic-key-expression": "Attribute key topic expression", + "request-id-expression": "Request id expression", + "request-id-json-expression": "Request id json expression", + "request-id-topic-expression": "Request id topic expression", + "response-topic-expression": "Response topic expression", + "value-expression": "Value expression", + "topic": "Topic", + "timeout": "Timeout in milliseconds", + "converter-json-required": "Converter json is required.", + "converter-json-parse": "Unable to parse converter json.", + "filter-expression": "Filter expression", + "connect-requests": "Connect requests", + "add-connect-request": "Add connect request", + "disconnect-requests": "Disconnect requests", + "add-disconnect-request": "Add disconnect request", + "attribute-requests": "Attribute requests", + "add-attribute-request": "Add attribute request", + "attribute-updates": "Attribute updates", + "add-attribute-update": "Add attribute update", + "server-side-rpc": "Server side RPC", + "add-server-side-rpc-request": "Add server-side RPC request", + "device-name-filter": "Device name filter", + "attribute-filter": "Attribute filter", + "method-filter": "Method filter", + "request-topic-expression": "Request topic expression", + "response-timeout": "Response timeout in milliseconds", + "topic-expression": "Topic expression", + "client-scope": "Client scope", + "opc-server": "Servers", + "opc-add-server": "Add server", + "opc-application-name": "Application name", + "opc-application-uri": "Application uri", + "opc-scan-period-in-seconds": "Scan period in seconds", + "opc-security": "Security", + "opc-identity": "Identity", + "opc-keystore": "Keystore", + "opc-type": "Type", + "opc-keystore-type":"Type", + "opc-keystore-location":"Location *", + "opc-keystore-password":"Password", + "opc-keystore-alias":"Alias", + "opc-keystore-key-password":"Key password", + "opc-device-node-pattern":"Device node pattern", + "opc-device-name-pattern":"Device name pattern", + + "sync": { + "status": "Status", + "sync": "Sync", + "not-sync": "Not sync", + "last-sync-time": "Last sync time", + "not-available": "Not available" + }, + + "export-extensions-configuration":"Export extensions configuration", + "import-extensions-configuration":"Import extensions configuration", + "import-extensions": "Import extensions", + "import-extension": "Import extension", + "export-extension": "Export extension", + "file": "Extensions file", + "invalid-file-error": "Invalid extension file" + }, "fullscreen": { "expand": "Expand to fullscreen", "exit": "Exit fullscreen", @@ -851,7 +978,6 @@ export default angular.module('thingsboard.locale', []) "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.", "copyId": "Copy plugin Id", "idCopiedMessage": "Plugin Id has been copied to clipboard" - }, "position": { "top": "Top", @@ -1071,7 +1197,8 @@ export default angular.module('thingsboard.locale', []) "boolean": "Boolean", "boolean-value": "Boolean value", "false": "False", - "true": "True" + "true": "True", + "long": "Long" }, "widget": { "widget-library": "Widgets Library", diff --git a/ui/src/app/widget/lib/extensions-table-widget.js b/ui/src/app/widget/lib/extensions-table-widget.js new file mode 100644 index 0000000000..3e5ac61741 --- /dev/null +++ b/ui/src/app/widget/lib/extensions-table-widget.js @@ -0,0 +1,135 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './extensions-table-widget.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.widgets.extensionsTableWidget', []) + .directive('tbExtensionsTableWidget', ExtensionsTableWidget) + .name; + +/*@ngInject*/ +function ExtensionsTableWidget() { + return { + restrict: "E", + scope: true, + bindToController: { + ctx: '=' + }, + controller: ExtensionsTableWidgetController, + controllerAs: 'vm', + templateUrl: extensionsTableWidgetTemplate + }; +} + +/*@ngInject*/ +function ExtensionsTableWidgetController($scope, $translate, utils) { + var vm = this; + + vm.datasources = null; + vm.tabsHidden = false; + + $scope.$watch('vm.ctx', function() { + if (vm.ctx && vm.ctx.defaultSubscription) { + vm.settings = vm.ctx.settings; + vm.subscription = vm.ctx.defaultSubscription; + vm.datasources = vm.subscription.datasources; + initializeConfig(); + updateDatasources(); + } + }); + + function initializeConfig() { + + if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) { + vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle); + } else { + vm.extensionsTitle = $translate.instant('extension.extensions'); + } + vm.ctx.widgetTitle = vm.extensionsTitle; + + vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction]; + } + + function updateDatasources() { + + var datasource = vm.datasources[0]; + vm.selectedSource = vm.datasources[0]; + vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle); + } + + vm.changeSelectedSource = function(source) { + vm.selectedSource = source; + }; + + vm.searchAction = { + name: "action.search", + show: true, + onAction: function() { + $scope.$broadcast("showSearch", vm.selectedSource); + }, + icon: "search" + }; + + vm.refreshAction = { + name: "action.refresh", + show: true, + onAction: function() { + $scope.$broadcast("refreshExtensions", vm.selectedSource); + }, + icon: "refresh" + }; + + vm.addAction = { + name: "action.add", + show: true, + onAction: function() { + $scope.$broadcast("addExtension", vm.selectedSource); + }, + icon: "add" + }; + + vm.exportExtensionsAction = { + name: "extension.export-extensions-configuration", + show: true, + onAction: function() { + $scope.$broadcast("exportExtensions", vm.selectedSource); + }, + icon: "file_download" + }; + + vm.importExtensionsAction = { + name: "extension.import-extensions-configuration", + show: true, + onAction: function() { + $scope.$broadcast("importExtensions", vm.selectedSource); + }, + icon: "file_upload" + }; + + $scope.$on("filterMode", function($event, mode) { + vm.tabsHidden = mode; + }); + + $scope.$on("selectedExtensions", function($event, mode) { + vm.tabsHidden = mode; + }); +} \ No newline at end of file diff --git a/ui/src/app/widget/lib/extensions-table-widget.scss b/ui/src/app/widget/lib/extensions-table-widget.scss new file mode 100644 index 0000000000..26ab5d95dc --- /dev/null +++ b/ui/src/app/widget/lib/extensions-table-widget.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +tb-extension-table { + md-content { + background-color: #fff; + } +} +md-tabs.hide-tabs-menu { + md-tabs-wrapper { + display: none; + } + md-tabs-content-wrapper { + top: 0 !important; + } +} \ No newline at end of file diff --git a/ui/src/app/widget/lib/extensions-table-widget.tpl.html b/ui/src/app/widget/lib/extensions-table-widget.tpl.html new file mode 100644 index 0000000000..067193890c --- /dev/null +++ b/ui/src/app/widget/lib/extensions-table-widget.tpl.html @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file