Commit f671d81ce431bb692990c844377943d6fab8e1d9
1 parent
0d933cf0
Add search to Entities Hierarchy widget. Improve widget advanced forms: add full…
…screen button for area fields.
Showing
14 changed files
with
221 additions
and
32 deletions
... | ... | @@ -128,9 +128,9 @@ |
128 | 128 | "templateHtml": "<tb-entities-hierarchy-widget \n hierarchy-id=\"hierarchyId\"\n ctx=\"ctx\">\n</tb-entities-hierarchy-widget>", |
129 | 129 | "templateCss": "", |
130 | 130 | "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", |
131 | - "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}", | |
131 | + "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}", | |
132 | 132 | "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", |
133 | - "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 += \\\" <b>\\\"+ data['temperature'] +\\\" °C</b>\\\";\\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\":{}}" | |
133 | + "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 += \\\" <b>\\\"+ data['temperature'] +\\\" °C</b>\\\";\\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\":{}}" | |
134 | 134 | } |
135 | 135 | } |
136 | 136 | ] | ... | ... |
... | ... | @@ -71,7 +71,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { |
71 | 71 | $compile(element.contents())(childScope); |
72 | 72 | } |
73 | 73 | |
74 | + scope.isFullscreen = false; | |
75 | + | |
74 | 76 | scope.formProps = { |
77 | + isFullscreen: false, | |
75 | 78 | option: { |
76 | 79 | formDefaults: { |
77 | 80 | startEmpty: true |
... | ... | @@ -86,6 +89,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { |
86 | 89 | }, |
87 | 90 | onColorClick: function(event, key, val) { |
88 | 91 | scope.showColorPicker(event, val); |
92 | + }, | |
93 | + onToggleFullscreen: function() { | |
94 | + scope.isFullscreen = !scope.isFullscreen; | |
95 | + scope.formProps.isFullscreen = scope.isFullscreen; | |
89 | 96 | } |
90 | 97 | }; |
91 | 98 | |
... | ... | @@ -116,6 +123,8 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { |
116 | 123 | }); |
117 | 124 | } |
118 | 125 | |
126 | + scope.onFullscreenChanged = function() {} | |
127 | + | |
119 | 128 | scope.validate = function(){ |
120 | 129 | if (scope.schema && scope.model) { |
121 | 130 | var result = utils.validateBySchema(scope.schema, scope.model); | ... | ... |
... | ... | @@ -15,4 +15,6 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<react-component name="ReactSchemaForm" props="formProps" watch-depth="value"></react-component> | |
\ No newline at end of file | ||
18 | +<div style="background: #fff;" tb-expand-fullscreen="isFullscreen" hide-expand-button="true" fullscreen-zindex="100" on-fullscreen-changed="onFullscreenChanged()"> | |
19 | + <react-component name="ReactSchemaForm" props="formProps" watch-depth="value"></react-component> | |
20 | +</div> | ... | ... |
... | ... | @@ -33,8 +33,10 @@ function NavTree() { |
33 | 33 | bindToController: { |
34 | 34 | loadNodes: '=', |
35 | 35 | editCallbacks: '=', |
36 | + enableSearch: '@?', | |
36 | 37 | onNodeSelected: '&', |
37 | - onNodesInserted: '&' | |
38 | + onNodesInserted: '&', | |
39 | + searchCallback: '&?' | |
38 | 40 | }, |
39 | 41 | controller: NavTreeController, |
40 | 42 | controllerAs: 'vm', |
... | ... | @@ -55,17 +57,30 @@ function NavTreeController($scope, $element, types) { |
55 | 57 | }); |
56 | 58 | |
57 | 59 | function initTree() { |
60 | + var config = { | |
61 | + core: { | |
62 | + multiple: false, | |
63 | + check_callback: true, | |
64 | + themes: { name: 'proton', responsive: true }, | |
65 | + data: vm.loadNodes | |
66 | + } | |
67 | + }; | |
68 | + | |
69 | + if (vm.enableSearch) { | |
70 | + config.plugins = ["search"]; | |
71 | + config.search = { | |
72 | + case_sensitive: false, | |
73 | + show_only_matches: true, | |
74 | + show_only_matches_children: false, | |
75 | + search_leaves_only: false | |
76 | + }; | |
77 | + if (vm.searchCallback) { | |
78 | + config.search.search_callback = (searchText, node) => vm.searchCallback({searchText: searchText, node: node}); | |
79 | + } | |
80 | + } | |
81 | + | |
58 | 82 | vm.treeElement = angular.element('.tb-nav-tree-container', $element) |
59 | - .jstree( | |
60 | - { | |
61 | - core: { | |
62 | - multiple: false, | |
63 | - check_callback: true, | |
64 | - themes: { name: 'proton', responsive: true }, | |
65 | - data: vm.loadNodes | |
66 | - } | |
67 | - } | |
68 | - ); | |
83 | + .jstree(config); | |
69 | 84 | |
70 | 85 | vm.treeElement.on("changed.jstree", function (e, data) { |
71 | 86 | if (vm.onNodeSelected) { |
... | ... | @@ -180,6 +195,12 @@ function NavTreeController($scope, $element, types) { |
180 | 195 | } |
181 | 196 | } |
182 | 197 | }; |
198 | + vm.editCallbacks.search = (searchText) => { | |
199 | + vm.treeElement.jstree('search', searchText); | |
200 | + }; | |
201 | + vm.editCallbacks.clearSearch = () => { | |
202 | + vm.treeElement.jstree('clear_search'); | |
203 | + }; | |
183 | 204 | } |
184 | 205 | } |
185 | 206 | } | ... | ... |
... | ... | @@ -34,8 +34,10 @@ class ThingsboardAceEditor extends React.Component { |
34 | 34 | this.onFocus = this.onFocus.bind(this); |
35 | 35 | this.onTidy = this.onTidy.bind(this); |
36 | 36 | this.onLoad = this.onLoad.bind(this); |
37 | + this.onToggleFull = this.onToggleFull.bind(this); | |
37 | 38 | var value = props.value ? props.value + '' : ''; |
38 | 39 | this.state = { |
40 | + isFull: false, | |
39 | 41 | value: value, |
40 | 42 | focused: false |
41 | 43 | }; |
... | ... | @@ -76,9 +78,26 @@ class ThingsboardAceEditor extends React.Component { |
76 | 78 | } |
77 | 79 | |
78 | 80 | onLoad(editor) { |
81 | + this.aceEditor = editor; | |
79 | 82 | fixAceEditor(editor); |
80 | 83 | } |
81 | 84 | |
85 | + onToggleFull() { | |
86 | + this.setState({ isFull: !this.state.isFull }); | |
87 | + this.props.onToggleFullscreen(); | |
88 | + this.updateAceEditorSize = true; | |
89 | + } | |
90 | + | |
91 | + componentDidUpdate() { | |
92 | + if (this.updateAceEditorSize) { | |
93 | + if (this.aceEditor) { | |
94 | + this.aceEditor.resize(); | |
95 | + this.aceEditor.renderer.updateFull(); | |
96 | + } | |
97 | + this.updateAceEditorSize = false; | |
98 | + } | |
99 | + } | |
100 | + | |
82 | 101 | render() { |
83 | 102 | |
84 | 103 | const styles = reactCSS({ |
... | ... | @@ -108,18 +127,23 @@ class ThingsboardAceEditor extends React.Component { |
108 | 127 | if (this.state.focused) { |
109 | 128 | labelClass += " tb-focused"; |
110 | 129 | } |
111 | - | |
130 | + var containerClass = "tb-container"; | |
131 | + var style = this.props.form.style || {width: '100%'}; | |
132 | + if (this.state.isFull) { | |
133 | + containerClass += " fullscreen-form-field"; | |
134 | + } | |
112 | 135 | return ( |
113 | - <div className="tb-container"> | |
136 | + <div className={containerClass}> | |
114 | 137 | <label className={labelClass}>{this.props.form.title}</label> |
115 | 138 | <div className="json-form-ace-editor"> |
116 | 139 | <div className="title-panel"> |
117 | 140 | <label>{this.props.mode}</label> |
118 | 141 | <FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/> |
142 | + <FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={this.onToggleFull}/> | |
119 | 143 | </div> |
120 | 144 | <AceEditor mode={this.props.mode} |
121 | - height="150px" | |
122 | - width="300px" | |
145 | + height={this.state.isFull ? "100%" : "150px"} | |
146 | + width={this.state.isFull ? "100%" : "300px"} | |
123 | 147 | theme="github" |
124 | 148 | onChange={this.onValueChanged} |
125 | 149 | onFocus={this.onFocus} |
... | ... | @@ -132,10 +156,10 @@ class ThingsboardAceEditor extends React.Component { |
132 | 156 | enableBasicAutocompletion={true} |
133 | 157 | enableSnippets={true} |
134 | 158 | enableLiveAutocompletion={true} |
135 | - style={this.props.form.style || {width: '100%'}}/> | |
159 | + style={style}/> | |
136 | 160 | </div> |
137 | 161 | <div className="json-form-error" |
138 | - style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div> | |
162 | + style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div> | |
139 | 163 | </div> |
140 | 164 | ); |
141 | 165 | } | ... | ... |
... | ... | @@ -13,6 +13,13 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | + | |
17 | +.fullscreen-form-field { | |
18 | + .json-form-ace-editor { | |
19 | + height: calc(100% - 60px); | |
20 | + } | |
21 | +} | |
22 | + | |
16 | 23 | .json-form-ace-editor { |
17 | 24 | position: relative; |
18 | 25 | height: 100%; | ... | ... |
... | ... | @@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component { |
131 | 131 | } |
132 | 132 | let forms = this.props.form.items.map(function(form, index){ |
133 | 133 | var copy = this.copyWithIndex(form, i); |
134 | - return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder); | |
134 | + return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); | |
135 | 135 | }.bind(this)); |
136 | 136 | arrays.push( |
137 | 137 | <li key={keys[i]} className="list-group-item"> | ... | ... |
... | ... | @@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component { |
19 | 19 | |
20 | 20 | render() { |
21 | 21 | let forms = this.props.form.items.map(function(form, index){ |
22 | - return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.mapper, this.props.builder); | |
22 | + return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); | |
23 | 23 | }.bind(this)); |
24 | 24 | |
25 | 25 | return ( | ... | ... |
... | ... | @@ -50,7 +50,8 @@ ReactSchemaForm.propTypes = { |
50 | 50 | model: React.PropTypes.object, |
51 | 51 | option: React.PropTypes.object, |
52 | 52 | onModelChange: React.PropTypes.func, |
53 | - onColorClick: React.PropTypes.func | |
53 | + onColorClick: React.PropTypes.func, | |
54 | + onToggleFullscreen: React.PropTypes.func | |
54 | 55 | } |
55 | 56 | |
56 | 57 | ReactSchemaForm.defaultProps = { | ... | ... |
... | ... | @@ -63,6 +63,7 @@ class ThingsboardSchemaForm extends React.Component { |
63 | 63 | |
64 | 64 | this.onChange = this.onChange.bind(this); |
65 | 65 | this.onColorClick = this.onColorClick.bind(this); |
66 | + this.onToggleFullscreen = this.onToggleFullscreen.bind(this); | |
66 | 67 | this.hasConditions = false; |
67 | 68 | } |
68 | 69 | |
... | ... | @@ -78,7 +79,11 @@ class ThingsboardSchemaForm extends React.Component { |
78 | 79 | this.props.onColorClick(event, key, val); |
79 | 80 | } |
80 | 81 | |
81 | - builder(form, model, index, onChange, onColorClick, mapper) { | |
82 | + onToggleFullscreen() { | |
83 | + this.props.onToggleFullscreen(); | |
84 | + } | |
85 | + | |
86 | + builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) { | |
82 | 87 | var type = form.type; |
83 | 88 | let Field = this.mapper[type]; |
84 | 89 | if(!Field) { |
... | ... | @@ -91,7 +96,7 @@ class ThingsboardSchemaForm extends React.Component { |
91 | 96 | return null; |
92 | 97 | } |
93 | 98 | } |
94 | - return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} mapper={mapper} builder={this.builder}/> | |
99 | + return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/> | |
95 | 100 | } |
96 | 101 | |
97 | 102 | render() { |
... | ... | @@ -101,11 +106,16 @@ class ThingsboardSchemaForm extends React.Component { |
101 | 106 | mapper = _.merge(this.mapper, this.props.mapper); |
102 | 107 | } |
103 | 108 | let forms = merged.map(function(form, index) { |
104 | - return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, mapper); | |
109 | + return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper); | |
105 | 110 | }.bind(this)); |
106 | 111 | |
112 | + let formClass = 'SchemaForm'; | |
113 | + if (this.props.isFullscreen) { | |
114 | + formClass += ' SchemaFormFullscreen'; | |
115 | + } | |
116 | + | |
107 | 117 | return ( |
108 | - <div style={{width: '100%'}} className='SchemaForm'>{forms}</div> | |
118 | + <div style={{width: '100%'}} className={formClass}>{forms}</div> | |
109 | 119 | ); |
110 | 120 | } |
111 | 121 | } | ... | ... |
... | ... | @@ -21,6 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; |
21 | 21 | $input-label-float-offset: 6px !default; |
22 | 22 | $input-label-float-scale: .75 !default; |
23 | 23 | |
24 | +.SchemaForm { | |
25 | + &.SchemaFormFullscreen { | |
26 | + position: relative; | |
27 | + width: 100%; | |
28 | + height: 100%; | |
29 | + | |
30 | + > div:not(.fullscreen-form-field) { | |
31 | + display: none; | |
32 | + } | |
33 | + | |
34 | + > div.fullscreen-form-field { | |
35 | + position: relative; | |
36 | + width: 100%; | |
37 | + height: 100%; | |
38 | + } | |
39 | + } | |
40 | +} | |
41 | + | |
24 | 42 | .json-form-error { |
25 | 43 | position: relative; |
26 | 44 | bottom: -5px; | ... | ... |
... | ... | @@ -54,6 +54,25 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
54 | 54 | vm.nodesMap = {}; |
55 | 55 | vm.pendingUpdateNodeTasks = {}; |
56 | 56 | |
57 | + vm.query = { | |
58 | + search: null | |
59 | + }; | |
60 | + | |
61 | + vm.searchAction = { | |
62 | + name: 'action.search', | |
63 | + show: true, | |
64 | + onAction: function() { | |
65 | + vm.enterFilterMode(); | |
66 | + }, | |
67 | + icon: 'search' | |
68 | + }; | |
69 | + | |
70 | + vm.onNodesInserted = onNodesInserted; | |
71 | + vm.onNodeSelected = onNodeSelected; | |
72 | + vm.enterFilterMode = enterFilterMode; | |
73 | + vm.exitFilterMode = exitFilterMode; | |
74 | + vm.searchCallback = searchCallback; | |
75 | + | |
57 | 76 | $scope.$watch('vm.ctx', function() { |
58 | 77 | if (vm.ctx && vm.ctx.defaultSubscription) { |
59 | 78 | vm.settings = vm.ctx.settings; |
... | ... | @@ -65,6 +84,12 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
65 | 84 | } |
66 | 85 | }); |
67 | 86 | |
87 | + $scope.$watch("vm.query.search", function(newVal, prevVal) { | |
88 | + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { | |
89 | + updateSearchNodes(); | |
90 | + } | |
91 | + }); | |
92 | + | |
68 | 93 | $scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) { |
69 | 94 | if (vm.hierarchyId == hierarchyId) { |
70 | 95 | if (vm.subscription) { |
... | ... | @@ -73,12 +98,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
73 | 98 | } |
74 | 99 | }); |
75 | 100 | |
76 | - vm.onNodesInserted = onNodesInserted; | |
77 | - | |
78 | - vm.onNodeSelected = onNodeSelected; | |
79 | - | |
80 | 101 | function initializeConfig() { |
81 | 102 | |
103 | + vm.ctx.widgetActions = [ vm.searchAction ]; | |
104 | + | |
82 | 105 | var testNodeCtx = { |
83 | 106 | entity: { |
84 | 107 | id: { |
... | ... | @@ -98,6 +121,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
98 | 121 | var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); |
99 | 122 | var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); |
100 | 123 | var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); |
124 | + var nodeOpenedFunction = loadNodeCtxFunction(vm.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx); | |
101 | 125 | var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); |
102 | 126 | |
103 | 127 | var testNodeCtx2 = angular.copy(testNodeCtx); |
... | ... | @@ -109,6 +133,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
109 | 133 | vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction; |
110 | 134 | vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); |
111 | 135 | vm.nodeDisabledFunction = nodeDisabledFunction || (() => false); |
136 | + vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction; | |
112 | 137 | vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true); |
113 | 138 | vm.nodesSortFunction = nodesSortFunction || defaultSortFunction; |
114 | 139 | } |
... | ... | @@ -129,10 +154,40 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
129 | 154 | return nodeCtxFunction; |
130 | 155 | } |
131 | 156 | |
157 | + function enterFilterMode () { | |
158 | + vm.query.search = ''; | |
159 | + vm.ctx.hideTitlePanel = true; | |
160 | + $timeout(()=>{ | |
161 | + angular.element(vm.ctx.$container).find('.searchInput').focus(); | |
162 | + }) | |
163 | + } | |
164 | + | |
165 | + function exitFilterMode () { | |
166 | + vm.query.search = null; | |
167 | + updateSearchNodes(); | |
168 | + vm.ctx.hideTitlePanel = false; | |
169 | + } | |
170 | + | |
171 | + function searchCallback (searchText, node) { | |
172 | + var theNode = vm.nodesMap[node.id]; | |
173 | + if (theNode && theNode.data.searchText) { | |
174 | + return theNode.data.searchText.includes(searchText.toLowerCase()); | |
175 | + } | |
176 | + return false; | |
177 | + } | |
178 | + | |
132 | 179 | function updateDatasources() { |
133 | 180 | vm.loadNodes = loadNodes; |
134 | 181 | } |
135 | 182 | |
183 | + function updateSearchNodes() { | |
184 | + if (vm.query.search != null) { | |
185 | + vm.nodeEditCallbacks.search(vm.query.search); | |
186 | + } else { | |
187 | + vm.nodeEditCallbacks.clearSearch(); | |
188 | + } | |
189 | + } | |
190 | + | |
136 | 191 | function onNodesInserted(nodes/*, parent*/) { |
137 | 192 | if (nodes) { |
138 | 193 | nodes.forEach((nodeId) => { |
... | ... | @@ -222,6 +277,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
222 | 277 | function prepareNodeText(node) { |
223 | 278 | var nodeIcon = prepareNodeIcon(node.data.nodeCtx); |
224 | 279 | var nodeText = vm.nodeTextFunction(node.data.nodeCtx); |
280 | + node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : ""; | |
225 | 281 | return nodeIcon + nodeText; |
226 | 282 | } |
227 | 283 | |
... | ... | @@ -298,7 +354,8 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
298 | 354 | nodeCtx: nodeCtx |
299 | 355 | }; |
300 | 356 | node.state = { |
301 | - disabled: vm.nodeDisabledFunction(node.data.nodeCtx) | |
357 | + disabled: vm.nodeDisabledFunction(node.data.nodeCtx), | |
358 | + opened: vm.nodeOpenedFunction(node.data.nodeCtx) | |
302 | 359 | }; |
303 | 360 | node.text = prepareNodeText(node); |
304 | 361 | node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); |
... | ... | @@ -459,6 +516,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast |
459 | 516 | }; |
460 | 517 | } |
461 | 518 | |
519 | + function defaultNodeOpenedFunction(nodeCtx) { | |
520 | + return nodeCtx.level <= 4; | |
521 | + } | |
522 | + | |
462 | 523 | function defaultSortFunction(nodeCtx1, nodeCtx2) { |
463 | 524 | var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); |
464 | 525 | if (result === 0) { | ... | ... |
... | ... | @@ -14,7 +14,21 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | |
17 | +.tb-has-timewindow { | |
18 | + .tb-entities-hierarchy { | |
19 | + md-toolbar { | |
20 | + min-height: 60px; | |
21 | + max-height: 60px; | |
22 | + } | |
23 | + } | |
24 | +} | |
25 | + | |
17 | 26 | .tb-entities-hierarchy { |
27 | + md-toolbar { | |
28 | + min-height: 39px; | |
29 | + max-height: 39px; | |
30 | + } | |
31 | + | |
18 | 32 | .tb-entities-nav-tree-panel { |
19 | 33 | overflow-x: auto; |
20 | 34 | overflow-y: auto; | ... | ... |
... | ... | @@ -17,12 +17,34 @@ |
17 | 17 | --> |
18 | 18 | <div class="tb-absolute-fill tb-entities-hierarchy" layout="column"> |
19 | 19 | <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column"> |
20 | + <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null"> | |
21 | + <div class="md-toolbar-tools"> | |
22 | + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}"> | |
23 | + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon> | |
24 | + <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}"> | |
25 | + {{'entity.search' | translate}} | |
26 | + </md-tooltip> | |
27 | + </md-button> | |
28 | + <md-input-container flex> | |
29 | + <label> </label> | |
30 | + <input ng-model="vm.query.search" class="searchInput" placeholder="{{'entity.search' | translate}}"/> | |
31 | + </md-input-container> | |
32 | + <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()"> | |
33 | + <md-icon aria-label="Close" class="material-icons">close</md-icon> | |
34 | + <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}"> | |
35 | + {{ 'action.close' | translate }} | |
36 | + </md-tooltip> | |
37 | + </md-button> | |
38 | + </div> | |
39 | + </md-toolbar> | |
20 | 40 | <div flex class="tb-entities-nav-tree-panel"> |
21 | 41 | <tb-nav-tree |
22 | 42 | load-nodes="vm.loadNodes" |
23 | 43 | on-node-selected="vm.onNodeSelected(node, event)" |
24 | 44 | on-nodes-inserted="vm.onNodesInserted(nodes, parent)" |
25 | 45 | edit-callbacks="vm.nodeEditCallbacks" |
46 | + enable-search="true" | |
47 | + search-callback="vm.searchCallback(searchText, node)" | |
26 | 48 | ></tb-nav-tree> |
27 | 49 | </div> |
28 | 50 | </div> | ... | ... |