Commit f671d81ce431bb692990c844377943d6fab8e1d9

Authored by Igor Kulikov
1 parent 0d933cf0

Add search to Entities Hierarchy widget. Improve widget advanced forms: add full…

…screen button for area fields.
... ... @@ -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>&nbsp;</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>
... ...