From f671d81ce431bb692990c844377943d6fab8e1d9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 15 Mar 2019 16:49:14 +0200 Subject: [PATCH] Add search to Entities Hierarchy widget. Improve widget advanced forms: add fullscreen button for area fields. --- .../json/system/widget_bundles/cards.json | 4 +- ui/src/app/components/json-form.directive.js | 9 +++ ui/src/app/components/json-form.tpl.html | 4 +- ui/src/app/components/nav-tree.directive.js | 43 ++++++++--- .../components/react/json-form-ace-editor.jsx | 36 ++++++++-- .../react/json-form-ace-editor.scss | 7 ++ .../app/components/react/json-form-array.jsx | 2 +- .../components/react/json-form-fieldset.jsx | 2 +- .../app/components/react/json-form-react.jsx | 3 +- .../react/json-form-schema-form.jsx | 18 +++-- ui/src/app/components/react/json-form.scss | 18 +++++ .../widget/lib/entities-hierarchy-widget.js | 71 +++++++++++++++++-- .../widget/lib/entities-hierarchy-widget.scss | 14 ++++ .../lib/entities-hierarchy-widget.tpl.html | 22 ++++++ 14 files changed, 221 insertions(+), 32 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 8833517a7b..e6fa1ab632 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -128,9 +128,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.hierarchyId = \"hierarchy-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-hierarchy-data-updated', self.ctx.$scope.hierarchyId);\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'nodeSelected': {\n name: 'widget-action.node-selected',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"widgetStyle\":{},\"actions\":{}}" } } ] diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js index 1b460b6238..80f8a6cdda 100644 --- a/ui/src/app/components/json-form.directive.js +++ b/ui/src/app/components/json-form.directive.js @@ -71,7 +71,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { $compile(element.contents())(childScope); } + scope.isFullscreen = false; + scope.formProps = { + isFullscreen: false, option: { formDefaults: { startEmpty: true @@ -86,6 +89,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { }, onColorClick: function(event, key, val) { scope.showColorPicker(event, val); + }, + onToggleFullscreen: function() { + scope.isFullscreen = !scope.isFullscreen; + scope.formProps.isFullscreen = scope.isFullscreen; } }; @@ -116,6 +123,8 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { }); } + scope.onFullscreenChanged = function() {} + scope.validate = function(){ if (scope.schema && scope.model) { var result = utils.validateBySchema(scope.schema, scope.model); diff --git a/ui/src/app/components/json-form.tpl.html b/ui/src/app/components/json-form.tpl.html index c15f3f5eb1..f5096c25b7 100644 --- a/ui/src/app/components/json-form.tpl.html +++ b/ui/src/app/components/json-form.tpl.html @@ -15,4 +15,6 @@ limitations under the License. --> - \ No newline at end of file +
+ +
diff --git a/ui/src/app/components/nav-tree.directive.js b/ui/src/app/components/nav-tree.directive.js index c179defa8e..119ef98529 100644 --- a/ui/src/app/components/nav-tree.directive.js +++ b/ui/src/app/components/nav-tree.directive.js @@ -33,8 +33,10 @@ function NavTree() { bindToController: { loadNodes: '=', editCallbacks: '=', + enableSearch: '@?', onNodeSelected: '&', - onNodesInserted: '&' + onNodesInserted: '&', + searchCallback: '&?' }, controller: NavTreeController, controllerAs: 'vm', @@ -55,17 +57,30 @@ function NavTreeController($scope, $element, types) { }); function initTree() { + var config = { + core: { + multiple: false, + check_callback: true, + themes: { name: 'proton', responsive: true }, + data: vm.loadNodes + } + }; + + if (vm.enableSearch) { + config.plugins = ["search"]; + config.search = { + case_sensitive: false, + show_only_matches: true, + show_only_matches_children: false, + search_leaves_only: false + }; + if (vm.searchCallback) { + config.search.search_callback = (searchText, node) => vm.searchCallback({searchText: searchText, node: node}); + } + } + vm.treeElement = angular.element('.tb-nav-tree-container', $element) - .jstree( - { - core: { - multiple: false, - check_callback: true, - themes: { name: 'proton', responsive: true }, - data: vm.loadNodes - } - } - ); + .jstree(config); vm.treeElement.on("changed.jstree", function (e, data) { if (vm.onNodeSelected) { @@ -180,6 +195,12 @@ function NavTreeController($scope, $element, types) { } } }; + vm.editCallbacks.search = (searchText) => { + vm.treeElement.jstree('search', searchText); + }; + vm.editCallbacks.clearSearch = () => { + vm.treeElement.jstree('clear_search'); + }; } } } diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx index a1f576bc43..44df6cd38d 100644 --- a/ui/src/app/components/react/json-form-ace-editor.jsx +++ b/ui/src/app/components/react/json-form-ace-editor.jsx @@ -34,8 +34,10 @@ class ThingsboardAceEditor extends React.Component { this.onFocus = this.onFocus.bind(this); this.onTidy = this.onTidy.bind(this); this.onLoad = this.onLoad.bind(this); + this.onToggleFull = this.onToggleFull.bind(this); var value = props.value ? props.value + '' : ''; this.state = { + isFull: false, value: value, focused: false }; @@ -76,9 +78,26 @@ class ThingsboardAceEditor extends React.Component { } onLoad(editor) { + this.aceEditor = editor; fixAceEditor(editor); } + onToggleFull() { + this.setState({ isFull: !this.state.isFull }); + this.props.onToggleFullscreen(); + this.updateAceEditorSize = true; + } + + componentDidUpdate() { + if (this.updateAceEditorSize) { + if (this.aceEditor) { + this.aceEditor.resize(); + this.aceEditor.renderer.updateFull(); + } + this.updateAceEditorSize = false; + } + } + render() { const styles = reactCSS({ @@ -108,18 +127,23 @@ class ThingsboardAceEditor extends React.Component { if (this.state.focused) { labelClass += " tb-focused"; } - + var containerClass = "tb-container"; + var style = this.props.form.style || {width: '100%'}; + if (this.state.isFull) { + containerClass += " fullscreen-form-field"; + } return ( -
+
+
+ style={style}/>
{this.props.error}
+ style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}
); } diff --git a/ui/src/app/components/react/json-form-ace-editor.scss b/ui/src/app/components/react/json-form-ace-editor.scss index 3ae4530b9c..fa32b74a07 100644 --- a/ui/src/app/components/react/json-form-ace-editor.scss +++ b/ui/src/app/components/react/json-form-ace-editor.scss @@ -13,6 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +.fullscreen-form-field { + .json-form-ace-editor { + height: calc(100% - 60px); + } +} + .json-form-ace-editor { position: relative; height: 100%; diff --git a/ui/src/app/components/react/json-form-array.jsx b/ui/src/app/components/react/json-form-array.jsx index b42bac55a8..46f6457b0f 100644 --- a/ui/src/app/components/react/json-form-array.jsx +++ b/ui/src/app/components/react/json-form-array.jsx @@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component { } let forms = this.props.form.items.map(function(form, index){ var copy = this.copyWithIndex(form, i); - return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder); + return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); }.bind(this)); arrays.push(
  • diff --git a/ui/src/app/components/react/json-form-fieldset.jsx b/ui/src/app/components/react/json-form-fieldset.jsx index 8434025c7b..5a0f94017c 100644 --- a/ui/src/app/components/react/json-form-fieldset.jsx +++ b/ui/src/app/components/react/json-form-fieldset.jsx @@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component { render() { let forms = this.props.form.items.map(function(form, index){ - return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder); + return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); }.bind(this)); return ( diff --git a/ui/src/app/components/react/json-form-react.jsx b/ui/src/app/components/react/json-form-react.jsx index 59d007532c..18b2de26ef 100644 --- a/ui/src/app/components/react/json-form-react.jsx +++ b/ui/src/app/components/react/json-form-react.jsx @@ -50,7 +50,8 @@ ReactSchemaForm.propTypes = { model: React.PropTypes.object, option: React.PropTypes.object, onModelChange: React.PropTypes.func, - onColorClick: React.PropTypes.func + onColorClick: React.PropTypes.func, + onToggleFullscreen: React.PropTypes.func } ReactSchemaForm.defaultProps = { diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx index 4c3aefe66e..47d20426b7 100644 --- a/ui/src/app/components/react/json-form-schema-form.jsx +++ b/ui/src/app/components/react/json-form-schema-form.jsx @@ -63,6 +63,7 @@ class ThingsboardSchemaForm extends React.Component { this.onChange = this.onChange.bind(this); this.onColorClick = this.onColorClick.bind(this); + this.onToggleFullscreen = this.onToggleFullscreen.bind(this); this.hasConditions = false; } @@ -78,7 +79,11 @@ class ThingsboardSchemaForm extends React.Component { this.props.onColorClick(event, key, val); } - builder(form, model, index, onChange, onColorClick, mapper) { + onToggleFullscreen() { + this.props.onToggleFullscreen(); + } + + builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) { var type = form.type; let Field = this.mapper[type]; if(!Field) { @@ -91,7 +96,7 @@ class ThingsboardSchemaForm extends React.Component { return null; } } - return + return } render() { @@ -101,11 +106,16 @@ class ThingsboardSchemaForm extends React.Component { mapper = _.merge(this.mapper, this.props.mapper); } let forms = merged.map(function(form, index) { - return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, mapper); + return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper); }.bind(this)); + let formClass = 'SchemaForm'; + if (this.props.isFullscreen) { + formClass += ' SchemaFormFullscreen'; + } + return ( -
    {forms}
    +
    {forms}
    ); } } diff --git a/ui/src/app/components/react/json-form.scss b/ui/src/app/components/react/json-form.scss index eabf2725ef..d36ec5c0e8 100644 --- a/ui/src/app/components/react/json-form.scss +++ b/ui/src/app/components/react/json-form.scss @@ -21,6 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; $input-label-float-offset: 6px !default; $input-label-float-scale: .75 !default; +.SchemaForm { + &.SchemaFormFullscreen { + position: relative; + width: 100%; + height: 100%; + + > div:not(.fullscreen-form-field) { + display: none; + } + + > div.fullscreen-form-field { + position: relative; + width: 100%; + height: 100%; + } + } +} + .json-form-error { position: relative; bottom: -5px; diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.js b/ui/src/app/widget/lib/entities-hierarchy-widget.js index 80a92d68f6..93b3912709 100644 --- a/ui/src/app/widget/lib/entities-hierarchy-widget.js +++ b/ui/src/app/widget/lib/entities-hierarchy-widget.js @@ -54,6 +54,25 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast vm.nodesMap = {}; vm.pendingUpdateNodeTasks = {}; + vm.query = { + search: null + }; + + vm.searchAction = { + name: 'action.search', + show: true, + onAction: function() { + vm.enterFilterMode(); + }, + icon: 'search' + }; + + vm.onNodesInserted = onNodesInserted; + vm.onNodeSelected = onNodeSelected; + vm.enterFilterMode = enterFilterMode; + vm.exitFilterMode = exitFilterMode; + vm.searchCallback = searchCallback; + $scope.$watch('vm.ctx', function() { if (vm.ctx && vm.ctx.defaultSubscription) { vm.settings = vm.ctx.settings; @@ -65,6 +84,12 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast } }); + $scope.$watch("vm.query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { + updateSearchNodes(); + } + }); + $scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) { if (vm.hierarchyId == hierarchyId) { if (vm.subscription) { @@ -73,12 +98,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast } }); - vm.onNodesInserted = onNodesInserted; - - vm.onNodeSelected = onNodeSelected; - function initializeConfig() { + vm.ctx.widgetActions = [ vm.searchAction ]; + var testNodeCtx = { entity: { id: { @@ -98,6 +121,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); + var nodeOpenedFunction = loadNodeCtxFunction(vm.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); var testNodeCtx2 = angular.copy(testNodeCtx); @@ -109,6 +133,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction; vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); vm.nodeDisabledFunction = nodeDisabledFunction || (() => false); + vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction; vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true); vm.nodesSortFunction = nodesSortFunction || defaultSortFunction; } @@ -129,10 +154,40 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast return nodeCtxFunction; } + function enterFilterMode () { + vm.query.search = ''; + vm.ctx.hideTitlePanel = true; + $timeout(()=>{ + angular.element(vm.ctx.$container).find('.searchInput').focus(); + }) + } + + function exitFilterMode () { + vm.query.search = null; + updateSearchNodes(); + vm.ctx.hideTitlePanel = false; + } + + function searchCallback (searchText, node) { + var theNode = vm.nodesMap[node.id]; + if (theNode && theNode.data.searchText) { + return theNode.data.searchText.includes(searchText.toLowerCase()); + } + return false; + } + function updateDatasources() { vm.loadNodes = loadNodes; } + function updateSearchNodes() { + if (vm.query.search != null) { + vm.nodeEditCallbacks.search(vm.query.search); + } else { + vm.nodeEditCallbacks.clearSearch(); + } + } + function onNodesInserted(nodes/*, parent*/) { if (nodes) { nodes.forEach((nodeId) => { @@ -222,6 +277,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast function prepareNodeText(node) { var nodeIcon = prepareNodeIcon(node.data.nodeCtx); var nodeText = vm.nodeTextFunction(node.data.nodeCtx); + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ""; return nodeIcon + nodeText; } @@ -298,7 +354,8 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast nodeCtx: nodeCtx }; node.state = { - disabled: vm.nodeDisabledFunction(node.data.nodeCtx) + disabled: vm.nodeDisabledFunction(node.data.nodeCtx), + opened: vm.nodeOpenedFunction(node.data.nodeCtx) }; node.text = prepareNodeText(node); node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); @@ -459,6 +516,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast }; } + function defaultNodeOpenedFunction(nodeCtx) { + return nodeCtx.level <= 4; + } + function defaultSortFunction(nodeCtx1, nodeCtx2) { var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); if (result === 0) { diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.scss b/ui/src/app/widget/lib/entities-hierarchy-widget.scss index 06ac2fc73a..d0f02a1392 100644 --- a/ui/src/app/widget/lib/entities-hierarchy-widget.scss +++ b/ui/src/app/widget/lib/entities-hierarchy-widget.scss @@ -14,7 +14,21 @@ * limitations under the License. */ +.tb-has-timewindow { + .tb-entities-hierarchy { + md-toolbar { + min-height: 60px; + max-height: 60px; + } + } +} + .tb-entities-hierarchy { + md-toolbar { + min-height: 39px; + max-height: 39px; + } + .tb-entities-nav-tree-panel { overflow-x: auto; overflow-y: auto; diff --git a/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html index a3ea257858..ab18748f0c 100644 --- a/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html +++ b/ui/src/app/widget/lib/entities-hierarchy-widget.tpl.html @@ -17,12 +17,34 @@ -->
    + +
    + + search + + {{'entity.search' | translate}} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
    +