diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index 275e645ce2..e49e87339d 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -30,7 +30,9 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.script.ScriptException; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -116,14 +118,19 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S } @Override - public ListenableFuture executeUpdateAsync(TbMsg msg) { + public ListenableFuture> executeUpdateAsync(TbMsg msg) { ListenableFuture result = executeScriptAsync(msg); return Futures.transformAsync(result, json -> { - if (!json.isObject()) { + if (json.isObject()) { + return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); + } else if (json.isArray()){ + List res = new ArrayList<>(json.size()); + json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); + return Futures.immediateFuture(res); + } + else{ log.warn("Wrong result type: {}", json.getNodeType()); return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); - } else { - return Futures.immediateFuture(unbindMsg(json, msg)); } }, MoreExecutors.directExecutor()); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java index a85f519337..8b645f6d93 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java @@ -20,13 +20,14 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.msg.TbMsg; import javax.script.ScriptException; +import java.util.List; import java.util.Set; public interface ScriptEngine { TbMsg executeUpdate(TbMsg msg) throws ScriptException; - ListenableFuture executeUpdateAsync(TbMsg msg); + ListenableFuture> executeUpdateAsync(TbMsg msg); TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java new file mode 100644 index 0000000000..c841a5a3cc --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/MultipleTbMsgsCallbackWrapper.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.transform; + +import org.thingsboard.server.common.msg.queue.RuleEngineException; +import org.thingsboard.server.common.msg.queue.TbMsgCallback; + +import java.util.concurrent.atomic.AtomicInteger; + +public class MultipleTbMsgsCallbackWrapper implements TbMsgCallbackWrapper { + + private final AtomicInteger tbMsgsCallbackCount; + private final TbMsgCallback callback; + + public MultipleTbMsgsCallbackWrapper(int tbMsgsCallbackCount, TbMsgCallback callback) { + this.tbMsgsCallbackCount = new AtomicInteger(tbMsgsCallbackCount); + this.callback = callback; + } + + @Override + public void onSuccess() { + if (tbMsgsCallbackCount.decrementAndGet() <= 0) { + callback.onSuccess(); + } + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(new RuleEngineException(t.getMessage())); + } +} + diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java index 84a6b76920..0b9139a307 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java @@ -17,16 +17,19 @@ package org.thingsboard.rule.engine.transform; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.queue.RuleEngineException; +import org.thingsboard.server.common.msg.queue.TbMsgCallback; + +import java.util.List; import static org.thingsboard.common.util.DonAsynchron.withCallback; import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; /** * Created by ashvayka on 19.01.18. @@ -61,7 +64,30 @@ public abstract class TbAbstractTransformNode implements TbNode { } } - protected abstract ListenableFuture transform(TbContext ctx, TbMsg msg); + protected void transformSuccess(TbContext ctx, TbMsg msg, List msgs) { + if (msgs != null && !msgs.isEmpty()) { + if (msgs.size() == 1) { + ctx.tellSuccess(msgs.get(0)); + } else { + TbMsgCallbackWrapper wrapper = new MultipleTbMsgsCallbackWrapper(msgs.size(), new TbMsgCallback() { + @Override + public void onSuccess() { + ctx.ack(msg); + } + + @Override + public void onFailure(RuleEngineException e) { + ctx.tellFailure(msg, e); + } + }); + msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, "Success", wrapper::onSuccess, wrapper::onFailure)); + } + } else { + ctx.tellNext(msg, FAILURE); + } + } + + protected abstract ListenableFuture> transform(TbContext ctx, TbMsg msg); public void setConfig(TbTransformNodeConfiguration config) { this.config = config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 5d505f51b2..566b9bef08 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -15,16 +15,15 @@ */ package org.thingsboard.rule.engine.transform; -import com.google.common.base.Function; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesAlarmOriginatorIdAsyncLoader; import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; @@ -33,7 +32,9 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import java.util.Collections; import java.util.HashSet; +import java.util.List; @Slf4j @RuleNode( @@ -65,13 +66,13 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { } @Override - protected ListenableFuture transform(TbContext ctx, TbMsg msg) { + protected ListenableFuture> transform(TbContext ctx, TbMsg msg) { ListenableFuture newOriginator = getNewOriginator(ctx, msg.getOriginator()); - return Futures.transform(newOriginator, (Function) n -> { + return Futures.transform(newOriginator, n -> { if (n == null || n.isNullUid()) { return null; } - return ctx.transformMsg(msg, msg.getType(), n, msg.getMetaData(), msg.getData()); + return Collections.singletonList((ctx.transformMsg(msg, msg.getType(), n, msg.getMetaData(), msg.getData()))); }, ctx.getDbCallbackExecutor()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbMsgCallbackWrapper.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbMsgCallbackWrapper.java new file mode 100644 index 0000000000..d05da7ce5d --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbMsgCallbackWrapper.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.transform; + +public interface TbMsgCallbackWrapper { + + void onSuccess(); + + void onFailure(Throwable t); +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 56f15f42de..94b39af045 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -16,13 +16,16 @@ package org.thingsboard.rule.engine.transform; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; +import java.util.List; @RuleNode( type = ComponentType.TRANSFORMATION, @@ -51,7 +54,7 @@ public class TbTransformMsgNode extends TbAbstractTransformNode { } @Override - protected ListenableFuture transform(TbContext ctx, TbMsg msg) { + protected ListenableFuture> transform(TbContext ctx, TbMsg msg) { ctx.logJsEvalRequest(); return jsEngine.executeUpdateAsync(msg); } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index bc56815468..e2087fd607 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.script.ScriptException; +import java.util.Collections; import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; @@ -72,7 +73,7 @@ public class TbTransformMsgNodeTest { TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); TbMsg transformedMsg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); mockJsExecutor(); - when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(transformedMsg)); + when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(Collections.singletonList(transformedMsg))); node.onMsg(ctx, msg); verify(ctx).getDbCallbackExecutor(); diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index a9a2cac52f..3cb140c837 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -86,25 +86,9 @@ ] }, "scripts": [ - "node_modules/jquery/dist/jquery.min.js", - "node_modules/jquery.terminal/js/jquery.terminal.min.js", - "node_modules/flot/lib/jquery.colorhelpers.js", - "node_modules/flot/src/jquery.flot.js", - "node_modules/flot/src/plugins/jquery.flot.time.js", - "node_modules/flot/src/plugins/jquery.flot.selection.js", - "node_modules/flot/src/plugins/jquery.flot.pie.js", - "node_modules/flot/src/plugins/jquery.flot.crosshair.js", - "node_modules/flot/src/plugins/jquery.flot.stack.js", - "node_modules/flot/src/plugins/jquery.flot.symbol.js", - "node_modules/flot.curvedlines/curvedLines.js", "node_modules/tinycolor2/dist/tinycolor-min.js", - "node_modules/tooltipster/dist/js/tooltipster.bundle.min.js", - "node_modules/split.js/dist/split.js", - "node_modules/js-beautify/js/lib/beautify.js", - "node_modules/js-beautify/js/lib/beautify-css.js", - "node_modules/js-beautify/js/lib/beautify-html.js", - "node_modules/systemjs/dist/system.js", - "node_modules/jstree/dist/jstree.min.js" + "node_modules/split.js/dist/split.min.js", + "node_modules/systemjs/dist/system.js" ], "customWebpackConfig": { "path": "./extra-webpack.config.js" @@ -130,7 +114,11 @@ "tinycolor2", "json-schema-defaults", "leaflet-providers", - "lodash" + "lodash", + "jquery", + "jquery.terminal", + "tooltipster", + "jstree" ] }, "configurations": { diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js index f834952fb3..355d85551e 100644 --- a/ui-ngx/extra-webpack.config.js +++ b/ui-ngx/extra-webpack.config.js @@ -34,6 +34,13 @@ module.exports = (config, options) => { SUPPORTED_LANGS: JSON.stringify(langs), }) ); + config.plugins.push( + new webpack.ProvidePlugin( + { + $: "jquery" + } + ) + ); config.plugins.push( new CompressionPlugin({ filename: "[path][base].gz[query]", @@ -44,6 +51,9 @@ module.exports = (config, options) => { deleteOriginalAssets: false, }) ); + config.plugins.push( + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) + ); if (config.mode === 'production') { const index = config.plugins.findIndex(p => p instanceof AngularCompilerPlugin.AngularCompilerPlugin); diff --git a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts index 9ca56d53ef..b8232e7109 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-content-dialog.component.ts @@ -24,6 +24,9 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { ContentType, contentTypesMap } from '@shared/models/constants'; import { getAce } from '@shared/models/ace/ace.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { beautifyJs } from '@shared/models/beautify.models'; +import { of } from 'rxjs'; export interface EventContentDialogData { content: string; @@ -64,33 +67,42 @@ export class EventContentDialogComponent extends DialogComponent = null; if (this.contentType) { mode = contentTypesMap.get(this.contentType).code; if (this.contentType === ContentType.JSON && content) { - content = js_beautify(content, {indent_size: 4}); + content$ = beautifyJs(content, {indent_size: 4}); } } - let editorOptions: Partial = { - mode: `ace/mode/${mode}`, - theme: 'ace/theme/github', - showGutter: false, - showPrintMargin: false, - readOnly: true - }; + if (!content$) { + content$ = of(content); + } + + content$.subscribe( + (processedContent) => { + let editorOptions: Partial = { + mode: `ace/mode/${mode}`, + theme: 'ace/theme/github', + showGutter: false, + showPrintMargin: false, + readOnly: true + }; - const advancedOptions = { - enableSnippets: false, - enableBasicAutocompletion: false, - enableLiveAutocompletion: false - }; + const advancedOptions = { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }; - editorOptions = {...editorOptions, ...advancedOptions}; - getAce().subscribe( - (ace) => { - const editor = ace.edit(editorElement, editorOptions); - editor.session.setUseWrapMode(false); - editor.setValue(content, -1); - this.updateEditorSize(editorElement, content, editor); + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + const editor = ace.edit(editorElement, editorOptions); + editor.session.setUseWrapMode(false); + editor.setValue(processedContent, -1); + this.updateEditorSize(editorElement, processedContent, editor); + } + ); } ); } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html index ea4ad4097a..dc46e3f291 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html @@ -29,7 +29,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts index 65009dbd0b..cb3ac3dae4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -32,11 +32,14 @@ import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap, - dayOfWeekTranslations, getAlarmScheduleRangeText, timeOfDayToUTCTimestamp, utcTimestampToTimeOfDay + dayOfWeekTranslations, + getAlarmScheduleRangeText, + timeOfDayToUTCTimestamp, + utcTimestampToTimeOfDay } from '@shared/models/device.models'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; -import * as _moment from 'moment-timezone'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { getDefaultTimezone } from '@shared/models/time/time.models'; @Component({ selector: 'tb-alarm-schedule', @@ -58,8 +61,6 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, alarmScheduleForm: FormGroup; - defaultTimezone = _moment.tz.guess(); - alarmScheduleTypes = Object.keys(AlarmScheduleType); alarmScheduleType = AlarmScheduleType; alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; @@ -93,9 +94,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) }); this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { - this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false}); - this.updateValidators(type, true); - this.alarmScheduleForm.updateValueAndValidity(); + getDefaultTimezone().subscribe((defaultTimezone) => { + this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); + this.updateValidators(type, true); + this.alarmScheduleForm.updateValueAndValidity(); + }); }); this.alarmScheduleForm.valueChanges.subscribe(() => { this.updateModel(); diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts index 16c924b711..0463bd2ba7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.ts @@ -34,13 +34,13 @@ import { AppState } from '@core/core.state'; import { CustomActionDescriptor } from '@shared/models/widget.models'; import { Ace } from 'ace-builds'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; -import { css_beautify, html_beautify } from 'js-beautify'; import { ResizeObserver } from '@juggle/resize-observer'; import { CustomPrettyActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; import { Observable } from 'rxjs/internal/Observable'; import { forkJoin, from } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { getAce } from '@shared/models/ace/ace.models'; +import { beautifyCss, beautifyHtml } from '@shared/models/beautify.models'; @Component({ selector: 'tb-custom-action-pretty-resources-tabs', @@ -140,21 +140,27 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl } public beautifyCss(): void { - const res = css_beautify(this.action.customCss, {indent_size: 4}); - if (this.action.customCss !== res) { - this.action.customCss = res; - this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); - this.notifyActionUpdated(); - } + beautifyCss(this.action.customCss, {indent_size: 4}).subscribe( + (res) => { + if (this.action.customCss !== res) { + this.action.customCss = res; + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); + this.notifyActionUpdated(); + } + } + ); } public beautifyHtml(): void { - const res = html_beautify(this.action.customHtml, {indent_size: 4, wrap_line_length: 60}); - if (this.action.customHtml !== res) { - this.action.customHtml = res; - this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); - this.notifyActionUpdated(); - } + beautifyHtml(this.action.customHtml, {indent_size: 4, wrap_line_length: 60}).subscribe( + (res) => { + if (this.action.customHtml !== res) { + this.action.customHtml = res; + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); + this.notifyActionUpdated(); + } + } + ); } private initAceEditors(): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts index d3a8db18a7..8b519fe859 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-compass.ts @@ -30,6 +30,7 @@ import RadialGauge = CanvasGauges.RadialGauge; const analogueCompassSettingsSchemaValue = analogueCompassSettingsSchema; +// @dynamic export class TbAnalogueCompass extends TbBaseGauge { static get settingsSchema(): JsonSettingsSchema { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts index 8307069c86..8dbe37c3cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-linear-gauge.ts @@ -32,6 +32,7 @@ const tinycolor = tinycolor_; const analogueLinearGaugeSettingsSchemaValue = getAnalogueLinearGaugeSettingsSchema(); +// @dynamic export class TbAnalogueLinearGauge extends TbAnalogueGauge{ static get settingsSchema(): JsonSettingsSchema { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts index 05c1969657..658a639a2e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-radial-gauge.ts @@ -28,6 +28,7 @@ import BaseGauge = CanvasGauges.BaseGauge; const analogueRadialGaugeSettingsSchemaValue = getAnalogueRadialGaugeSettingsSchema(); +// @dynamic export class TbAnalogueRadialGauge extends TbAnalogueGauge{ static get settingsSchema(): JsonSettingsSchema { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index 11cc8dc732..8c6f0e56f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -46,6 +46,7 @@ const tinycolor = tinycolor_; const digitalGaugeSettingsSchemaValue = digitalGaugeSettingsSchema; +// @dynamic export class TbCanvasDigitalGauge { static get settingsSchema(): JsonSettingsSchema { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 01ebb4b39c..ff78ae2e3f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -111,11 +111,32 @@ export class WidgetComponentService { const initSubject = new ReplaySubject(); this.init$ = initSubject.asObservable(); - (window as any).tinycolor = tinycolor; - (window as any).cssjs = cssjs; - (window as any).moment = moment; + const w = (this.window as any); + + w.tinycolor = tinycolor; + w.cssjs = cssjs; + w.moment = moment; + w.$ = $; + w.jQuery = $; const widgetModulesTasks: Observable[] = []; + widgetModulesTasks.push(from(import('jquery.terminal'))); + + widgetModulesTasks.push(from(import('flot/src/jquery.flot.js')).pipe( + mergeMap(() => { + const flotJsPluginsTasks: Observable[] = []; + flotJsPluginsTasks.push(from(import('flot/lib/jquery.colorhelpers.js'))); + flotJsPluginsTasks.push(from(import('flot/src/plugins/jquery.flot.time.js'))); + flotJsPluginsTasks.push(from(import('flot/src/plugins/jquery.flot.selection.js'))); + flotJsPluginsTasks.push(from(import('flot/src/plugins/jquery.flot.pie.js'))); + flotJsPluginsTasks.push(from(import('flot/src/plugins/jquery.flot.crosshair.js'))); + flotJsPluginsTasks.push(from(import('flot/src/plugins/jquery.flot.stack.js'))); + flotJsPluginsTasks.push(from(import('flot/src/plugins/jquery.flot.symbol.js'))); + flotJsPluginsTasks.push(from(import('flot.curvedlines/curvedLines.js'))); + return forkJoin(flotJsPluginsTasks); + }) + )); + widgetModulesTasks.push(from(import('@home/components/widget/lib/flot-widget')).pipe( tap((mod) => { (window as any).TbFlot = mod.TbFlot; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts index 0431be2e14..e0c667a889 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts @@ -29,7 +29,7 @@ import { import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { Authority } from '@shared/models/authority.enum'; import { RuleChainsTableConfigResolver } from '@modules/home/pages/rulechain/rulechains-table-config.resolver'; -import { Observable } from 'rxjs'; +import { from, Observable } from 'rxjs'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { ResolvedRuleChainMetaData, RuleChain } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; @@ -75,6 +75,17 @@ export class RuleNodeComponentsResolver implements Resolve { + + constructor() { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + return from(import('tooltipster')); + } +} + @Injectable() export class RuleChainImportGuard implements CanActivate { @@ -144,7 +155,8 @@ const routes: Routes = [ resolve: { ruleChain: RuleChainResolver, ruleChainMetaData: ResolvedRuleChainMetaDataResolver, - ruleNodeComponents: RuleNodeComponentsResolver + ruleNodeComponents: RuleNodeComponentsResolver, + tooltipster: TooltipsterResolver } }, { @@ -162,7 +174,8 @@ const routes: Routes = [ import: true }, resolve: { - ruleNodeComponents: RuleNodeComponentsResolver + ruleNodeComponents: RuleNodeComponentsResolver, + tooltipster: TooltipsterResolver } } ] @@ -178,6 +191,7 @@ const routes: Routes = [ RuleChainResolver, ResolvedRuleChainMetaDataResolver, RuleNodeComponentsResolver, + TooltipsterResolver, RuleChainImportGuard ] }) diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 46338a2dfe..136e2133f6 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -34,7 +34,6 @@ import { TranslateService } from '@ngx-translate/core'; import { getCurrentIsLoading } from '@app/core/interceptors/load.selectors'; import { Ace } from 'ace-builds'; import { getAce, Range } from '@shared/models/ace/ace.models'; -import { css_beautify, html_beautify } from 'js-beautify'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { WINDOW } from '@core/services/window.service'; import { WindowMessage } from '@shared/models/window-message.model'; @@ -51,6 +50,7 @@ import Timeout = NodeJS.Timeout; import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; import { Observable } from 'rxjs/internal/Observable'; import { map, tap } from 'rxjs/operators'; +import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; // @dynamic @Component({ @@ -618,48 +618,63 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe } beautifyCss(): void { - const res = css_beautify(this.widget.templateCss, {indent_size: 4}); - if (this.widget.templateCss !== res) { - this.isDirty = true; - this.widget.templateCss = res; - this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1); - } + beautifyCss(this.widget.templateCss, {indent_size: 4}).subscribe( + (res) => { + if (this.widget.templateCss !== res) { + this.isDirty = true; + this.widget.templateCss = res; + this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1); + } + } + ); } beautifyHtml(): void { - const res = html_beautify(this.widget.templateHtml, {indent_size: 4, wrap_line_length: 60}); - if (this.widget.templateHtml !== res) { - this.isDirty = true; - this.widget.templateHtml = res; - this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1); - } + beautifyHtml(this.widget.templateHtml, {indent_size: 4, wrap_line_length: 60}).subscribe( + (res) => { + if (this.widget.templateHtml !== res) { + this.isDirty = true; + this.widget.templateHtml = res; + this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1); + } + } + ); } beautifyJson(): void { - const res = js_beautify(this.widget.settingsSchema, {indent_size: 4}); - if (this.widget.settingsSchema !== res) { - this.isDirty = true; - this.widget.settingsSchema = res; - this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1); - } + beautifyJs(this.widget.settingsSchema, {indent_size: 4}).subscribe( + (res) => { + if (this.widget.settingsSchema !== res) { + this.isDirty = true; + this.widget.settingsSchema = res; + this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1); + } + } + ); } beautifyDataKeyJson(): void { - const res = js_beautify(this.widget.dataKeySettingsSchema, {indent_size: 4}); - if (this.widget.dataKeySettingsSchema !== res) { - this.isDirty = true; - this.widget.dataKeySettingsSchema = res; - this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1); - } + beautifyJs(this.widget.dataKeySettingsSchema, {indent_size: 4}).subscribe( + (res) => { + if (this.widget.dataKeySettingsSchema !== res) { + this.isDirty = true; + this.widget.dataKeySettingsSchema = res; + this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1); + } + } + ); } beautifyJs(): void { - const res = js_beautify(this.widget.controllerScript, {indent_size: 4, wrap_line_length: 60}); - if (this.widget.controllerScript !== res) { - this.isDirty = true; - this.widget.controllerScript = res; - this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1); - } + beautifyJs(this.widget.controllerScript, {indent_size: 4, wrap_line_length: 60}).subscribe( + (res) => { + if (this.widget.controllerScript !== res) { + this.isDirty = true; + this.widget.controllerScript = res; + this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1); + } + } + ); } removeResource(index: number) { diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts index 007a96dbad..8202728131 100644 --- a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts @@ -41,6 +41,7 @@ import { TestScriptInputParams } from '@shared/models/rule-node.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { mergeMap } from 'rxjs/operators'; import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { beautifyJs } from '@shared/models/beautify.models'; export interface NodeScriptTestDialogData { script: string; @@ -110,12 +111,17 @@ export class NodeScriptTestDialogComponent extends DialogComponent { + this.nodeScriptTestFormGroup.get('payload').get('msg').patchValue(res, {emitEvent: false}); + } + ); } ngAfterViewInit(): void { @@ -166,7 +172,11 @@ export class NodeScriptTestDialogComponent extends DialogComponent { - this.nodeScriptTestFormGroup.get('output').setValue(js_beautify(output, {indent_size: 4})); + beautifyJs(output, {indent_size: 4}).subscribe( + (res) => { + this.nodeScriptTestFormGroup.get('output').setValue(res); + } + ); }); } diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 0fa827e4b7..93e7848072 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -37,6 +37,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { ResizeObserver } from '@juggle/resize-observer'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { beautifyJs } from '@shared/models/beautify.models'; @Component({ selector: 'tb-js-func', @@ -214,9 +215,12 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, } beautifyJs() { - const res = js_beautify(this.modelValue, {indent_size: 4, wrap_line_length: 60}); - this.jsEditor.setValue(res ? res : '', -1); - this.updateView(); + beautifyJs(this.modelValue, {indent_size: 4, wrap_line_length: 60}).subscribe( + (res) => { + this.jsEditor.setValue(res ? res : '', -1); + this.updateView(); + } + ); } validateOnSubmit(): void { diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts index cbe5368a8a..00a7745819 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.ts +++ b/ui-ngx/src/app/shared/components/json-content.component.ts @@ -36,6 +36,7 @@ import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { guid } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { getAce } from '@shared/models/ace/ace.models'; +import { beautifyJs } from '@shared/models/beautify.models'; @Component({ selector: 'tb-json-content', @@ -286,9 +287,12 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid } beautifyJSON() { - const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60}); - this.jsonEditor.setValue(res ? res : '', -1); - this.updateView(); + beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe( + (res) => { + this.jsonEditor.setValue(res ? res : '', -1); + this.updateView(); + } + ); } minifyJSON() { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx index ae2e112601..8e630582a3 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx @@ -22,6 +22,7 @@ import { IEditorProps } from 'react-ace/src/types'; import { map, mergeMap } from 'rxjs/operators'; import { loadAceDependencies } from '@shared/models/ace/ace.models'; import { from } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; const ReactAce = React.lazy(() => { return loadAceDependencies().pipe( @@ -33,7 +34,7 @@ const ReactAce = React.lazy(() => { interface ThingsboardAceEditorProps extends JsonFormFieldProps { mode: string; - onTidy: (value: string) => string; + onTidy: (value: string) => Observable; } interface ThingsboardAceEditorState extends JsonFormFieldState { @@ -84,15 +85,18 @@ class ThingsboardAceEditor extends React.Component { + this.setState({ + value: processedValue + }); + this.props.onChangeValidate({ + target: { + value: processedValue + } + }); + } + ); } } diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx index a5107ad4db..e8b3616b8b 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx @@ -16,7 +16,8 @@ import * as React from 'react'; import ThingsboardAceEditor from './json-form-ace-editor'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; -import { css_beautify } from 'js-beautify'; +import { Observable } from 'rxjs/internal/Observable'; +import { beautifyCss } from '@shared/models/beautify.models'; class ThingsboardCss extends React.Component { @@ -25,8 +26,8 @@ class ThingsboardCss extends React.Component { + return beautifyCss(css, {indent_size: 4}); } render() { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx index 4d7d0c956f..9e929b36aa 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx @@ -15,8 +15,9 @@ */ import * as React from 'react'; import ThingsboardAceEditor from './json-form-ace-editor'; -import { html_beautify } from 'js-beautify'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { beautifyHtml } from '@shared/models/beautify.models'; class ThingsboardHtml extends React.Component { @@ -25,8 +26,8 @@ class ThingsboardHtml extends React.Component { + return beautifyHtml(html, {indent_size: 4}); } render() { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx index 4530434b03..427c79ac65 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx @@ -16,6 +16,8 @@ import * as React from 'react'; import ThingsboardAceEditor from './json-form-ace-editor'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { beautifyJs } from '@shared/models/beautify.models'; class ThingsboardJavaScript extends React.Component { @@ -24,8 +26,8 @@ class ThingsboardJavaScript extends React.Component { + return beautifyJs(javascript, {indent_size: 4, wrap_line_length: 60}); } render() { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx index 5fc5f5d98f..0034b50422 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx @@ -16,6 +16,8 @@ import * as React from 'react'; import ThingsboardAceEditor from './json-form-ace-editor'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { beautifyJs } from '@shared/models/beautify.models'; class ThingsboardJson extends React.Component { @@ -24,8 +26,8 @@ class ThingsboardJson extends React.Component { + return beautifyJs(json, {indent_size: 4}); } render() { diff --git a/ui-ngx/src/app/shared/components/nav-tree.component.ts b/ui-ngx/src/app/shared/components/nav-tree.component.ts index a06a33d5c5..08b2256646 100644 --- a/ui-ngx/src/app/shared/components/nav-tree.component.ts +++ b/ui-ngx/src/app/shared/components/nav-tree.component.ts @@ -145,128 +145,131 @@ export class NavTreeComponent implements OnInit { }; } - this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config); + import('jstree').then(() => { - this.treeElement.on('changed.jstree', (e: any, data) => { - const node: NavTreeNode = data.instance.get_selected(true)[0]; - if (this.onNodeSelected) { - this.ngZone.run(() => this.onNodeSelected(node, e as Event)); - } - }); - - this.treeElement.on('model.jstree', (e: any, data) => { - if (this.onNodesInserted) { - this.ngZone.run(() => this.onNodesInserted(data.nodes, data.parent)); - } - }); + this.treeElement = $('.tb-nav-tree-container', this.elementRef.nativeElement).jstree(config); - if (this.editCallbacks) { - this.editCallbacks.selectNode = id => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - this.treeElement.jstree('deselect_all', true); - this.treeElement.jstree('select_node', node); - } - }; - this.editCallbacks.deselectAll = () => { - this.treeElement.jstree('deselect_all'); - }; - this.editCallbacks.getNode = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - return node; - }; - this.editCallbacks.getParentNodeId = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - return this.treeElement.jstree('get_parent', node); + this.treeElement.on('changed.jstree', (e: any, data) => { + const node: NavTreeNode = data.instance.get_selected(true)[0]; + if (this.onNodeSelected) { + this.ngZone.run(() => this.onNodeSelected(node, e as Event)); } - }; - this.editCallbacks.openNode = (id, cb) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - this.treeElement.jstree('open_node', node, cb); - } - }; - this.editCallbacks.nodeIsOpen = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - return this.treeElement.jstree('is_open', node); - } else { - return true; - } - }; - this.editCallbacks.nodeIsLoaded = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - return this.treeElement.jstree('is_loaded', node); - } else { - return true; + }); + + this.treeElement.on('model.jstree', (e: any, data) => { + if (this.onNodesInserted) { + this.ngZone.run(() => this.onNodesInserted(data.nodes, data.parent)); } - }; - this.editCallbacks.refreshNode = (id) => { - if (id === '#') { - this.treeElement.jstree('refresh'); - this.treeElement.jstree('redraw'); - } else { + }); + + if (this.editCallbacks) { + this.editCallbacks.selectNode = id => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('deselect_all', true); + this.treeElement.jstree('select_node', node); + } + }; + this.editCallbacks.deselectAll = () => { + this.treeElement.jstree('deselect_all'); + }; + this.editCallbacks.getNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + return node; + }; + this.editCallbacks.getParentNodeId = (id) => { const node: NavTreeNode = this.treeElement.jstree('get_node', id); if (node) { - const opened = this.treeElement.jstree('is_open', node); - this.treeElement.jstree('refresh_node', node); + return this.treeElement.jstree('get_parent', node); + } + }; + this.editCallbacks.openNode = (id, cb) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('open_node', node, cb); + } + }; + this.editCallbacks.nodeIsOpen = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + return this.treeElement.jstree('is_open', node); + } else { + return true; + } + }; + this.editCallbacks.nodeIsLoaded = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + return this.treeElement.jstree('is_loaded', node); + } else { + return true; + } + }; + this.editCallbacks.refreshNode = (id) => { + if (id === '#') { + this.treeElement.jstree('refresh'); this.treeElement.jstree('redraw'); - if (node.children && opened/* && !node.children.length*/) { - this.treeElement.jstree('open_node', node); + } else { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + const opened = this.treeElement.jstree('is_open', node); + this.treeElement.jstree('refresh_node', node); + this.treeElement.jstree('redraw'); + if (node.children && opened/* && !node.children.length*/) { + this.treeElement.jstree('open_node', node); + } } } - } - }; - this.editCallbacks.updateNode = (id, newName) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - this.treeElement.jstree('rename_node', node, newName); - } - }; - this.editCallbacks.createNode = (parentId, node, pos) => { - const parentNode: NavTreeNode = this.treeElement.jstree('get_node', parentId); - if (parentNode) { - this.treeElement.jstree('create_node', parentNode, node, pos); - } - }; - this.editCallbacks.deleteNode = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - this.treeElement.jstree('delete_node', node); - } - }; - this.editCallbacks.disableNode = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - this.treeElement.jstree('disable_node', node); - } - }; - this.editCallbacks.enableNode = (id) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - this.treeElement.jstree('enable_node', node); - } - }; - this.editCallbacks.setNodeHasChildren = (id, hasChildren) => { - const node: NavTreeNode = this.treeElement.jstree('get_node', id); - if (node) { - if (!node.children || (Array.isArray(node.children) && !node.children.length)) { - node.children = hasChildren; - node.state.loaded = !hasChildren; - node.state.opened = false; - this.treeElement.jstree('_node_changed', node.id); - this.treeElement.jstree('redraw'); + }; + this.editCallbacks.updateNode = (id, newName) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('rename_node', node, newName); } - } - }; - this.editCallbacks.search = (searchText) => { - this.treeElement.jstree('search', searchText); - }; - this.editCallbacks.clearSearch = () => { - this.treeElement.jstree('clear_search'); - }; - } + }; + this.editCallbacks.createNode = (parentId, node, pos) => { + const parentNode: NavTreeNode = this.treeElement.jstree('get_node', parentId); + if (parentNode) { + this.treeElement.jstree('create_node', parentNode, node, pos); + } + }; + this.editCallbacks.deleteNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('delete_node', node); + } + }; + this.editCallbacks.disableNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('disable_node', node); + } + }; + this.editCallbacks.enableNode = (id) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + this.treeElement.jstree('enable_node', node); + } + }; + this.editCallbacks.setNodeHasChildren = (id, hasChildren) => { + const node: NavTreeNode = this.treeElement.jstree('get_node', id); + if (node) { + if (!node.children || (Array.isArray(node.children) && !node.children.length)) { + node.children = hasChildren; + node.state.loaded = !hasChildren; + node.state.opened = false; + this.treeElement.jstree('_node_changed', node.id); + this.treeElement.jstree('redraw'); + } + } + }; + this.editCallbacks.search = (searchText) => { + this.treeElement.jstree('search', searchText); + }; + this.editCallbacks.clearSearch = () => { + this.treeElement.jstree('clear_search'); + }; + } + }); } } diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts index 7dc89df7da..f96c80b4c1 100644 --- a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts @@ -16,21 +16,14 @@ import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { map, mergeMap, share, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import * as _moment from 'moment-timezone'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; - -interface TimezoneInfo { - id: string; - name: string; - offset: string; - nOffset: number; -} +import { getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models'; @Component({ selector: 'tb-timezone-select', @@ -50,28 +43,14 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af defaultTimezoneId: string = null; - defaultTimezoneInfo: TimezoneInfo = null; - - timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => { - const tz = _moment.tz(zoneName); - return { - id: zoneName, - name: zoneName.replace(/_/g, ' '), - offset: `UTC${tz.format('Z')}`, - nOffset: tz.utcOffset() - } - }); + timezones$ = getTimezones().pipe( + share() + ); @Input() set defaultTimezone(timezone: string) { if (this.defaultTimezoneId !== timezone) { this.defaultTimezoneId = timezone; - if (this.defaultTimezoneId) { - this.defaultTimezoneInfo = - this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId); - } else { - this.defaultTimezoneInfo = null; - } } } @@ -84,6 +63,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af this.requiredValue = coerceBooleanProperty(value); } + private userTimezoneByDefaultValue: boolean; + get userTimezoneByDefault(): boolean { + return this.userTimezoneByDefaultValue; + } + @Input() + set userTimezoneByDefault(value: boolean) { + this.userTimezoneByDefaultValue = coerceBooleanProperty(value); + } + @Input() disabled: boolean; @@ -150,24 +138,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af writeValue(value: string | null): void { this.searchText = ''; - let foundTimezone: TimezoneInfo = null; - if (value !== null) { - foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value); - } - if (foundTimezone !== null) { - this.modelValue = value; - this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); - } else { - if (this.defaultTimezoneInfo) { - this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false}); - setTimeout(() => { - this.updateView(this.defaultTimezoneInfo.id); - }, 0); - } else { - this.modelValue = null; - this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); + getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe( + (foundTimezone) => { + if (foundTimezone !== null) { + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); + if (foundTimezone.id !== value) { + setTimeout(() => { + this.updateView(foundTimezone.id); + }, 0); + } else { + this.modelValue = value; + } + } else { + this.modelValue = null; + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); + } } - } + ); this.dirty = true; } @@ -182,10 +169,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af if (this.ignoreClosePanel) { this.ignoreClosePanel = false; } else { - if (!this.modelValue && this.defaultTimezoneInfo) { - this.ngZone.run(() => { - this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true}); - }); + if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) { + getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe( + (defaultTimezoneInfo) => { + if (defaultTimezoneInfo !== null) { + this.ngZone.run(() => { + this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true}); + }); + } + }); } } } @@ -203,12 +195,13 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af fetchTimezones(searchText?: string): Observable> { this.searchText = searchText; - let result = this.timezones; if (searchText && searchText.length) { - result = this.timezones.filter((timezoneInfo) => - timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())); + return getTimezones().pipe( + map((timezones) => timezones.filter((timezoneInfo) => + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase()))) + ); } - return of(result); + return getTimezones(); } clear() { diff --git a/ui-ngx/src/app/shared/models/beautify.models.ts b/ui-ngx/src/app/shared/models/beautify.models.ts new file mode 100644 index 0000000000..d28d8e00c4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/beautify.models.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Observable } from 'rxjs/internal/Observable'; +import { from, of } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; + +let jsBeautifyModule: any; +let htmlBeautifyModule: any; +let cssBeautifyModule: any; + +function loadJsBeautify(): Observable { + if (jsBeautifyModule) { + return of(jsBeautifyModule); + } else { + return from(import('js-beautify/js/lib/beautify.js')).pipe( + tap((module) => { + jsBeautifyModule = module; + }) + ); + } +} + +function loadHtmlBeautify(): Observable { + if (htmlBeautifyModule) { + return of(htmlBeautifyModule); + } else { + return from(import('js-beautify/js/lib/beautify-html.js')).pipe( + tap((module) => { + htmlBeautifyModule = module; + }) + ); + } +} + +function loadCssBeautify(): Observable { + if (cssBeautifyModule) { + return of(cssBeautifyModule); + } else { + return from(import('js-beautify/js/lib/beautify-css.js')).pipe( + tap((module) => { + cssBeautifyModule = module; + }) + ); + } +} + +export function beautifyJs(source: string, options?: any): Observable { + return loadJsBeautify().pipe( + map((mod) => { + return mod.js_beautify(source, options); + }) + ); +} + +export function beautifyCss(source: string, options?: any): Observable { + return loadCssBeautify().pipe( + map((mod) => mod.css_beautify(source, options)) + ); +} + +export function beautifyHtml(source: string, options?: any): Observable { + return loadHtmlBeautify().pipe( + map((mod) => mod.html_beautify(source, options)) + ); +} diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index ffeae2f56d..e4d240d0d5 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -25,7 +25,7 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { EntityInfoData } from '@shared/models/entity.models'; import { KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; -import * as _moment from 'moment-timezone'; +import * as _moment from 'moment'; import { AbstractControl, ValidationErrors } from '@angular/forms'; export enum DeviceProfileType { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index b81952dbde..314a6f2e69 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -17,6 +17,9 @@ import { TimeService } from '@core/services/time.service'; import { deepClone, isDefined, isUndefined } from '@app/core/utils'; import * as moment_ from 'moment'; +import { Observable } from 'rxjs/internal/Observable'; +import { from, of } from 'rxjs'; +import { map, mergeMap, tap } from 'rxjs/operators'; const moment = moment_; @@ -481,3 +484,71 @@ export const timeUnitTranslationMap = new Map( [TimeUnit.DAYS, 'timeunit.days'] ] ); + +export interface TimezoneInfo { + id: string; + name: string; + offset: string; + nOffset: number; +} + +let timezones: TimezoneInfo[] = null; +let defaultTimezone: string = null; + +export function getTimezones(): Observable { + if (timezones) { + return of(timezones); + } else { + return from(import('moment-timezone')).pipe( + map((monentTz) => { + return monentTz.tz.names().map((zoneName) => { + const tz = monentTz.tz(zoneName); + return { + id: zoneName, + name: zoneName.replace(/_/g, ' '), + offset: `UTC${tz.format('Z')}`, + nOffset: tz.utcOffset() + }; + }); + }), + tap((zones) => { + timezones = zones; + }) + ); + } +} + +export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): Observable { + return getTimezones().pipe( + mergeMap((timezoneList) => { + let foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId); + if (!foundTimezone) { + if (userTimezoneByDefault) { + return getDefaultTimezone().pipe( + map((userTimezone) => { + return timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone); + }) + ); + } else if (defaultTimezoneId) { + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId); + } + } + return of(foundTimezone); + }) + ); +} + +export function getDefaultTimezone(): Observable { + if (defaultTimezone) { + return of(defaultTimezone); + } else { + return from(import('moment-timezone')).pipe( + map((monentTz) => { + return monentTz.tz.guess(); + }), + tap((zone) => { + defaultTimezone = zone; + }) + ); + } +} diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index baaa5cdb3a..1fb19bb630 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -37,6 +37,18 @@ ], "ace": [ "node_modules/ace-builds/src-noconflict/ace.js" + ], + "jquery": [ + "node_modules/jquery/dist/jquery.min.js" + ], + "jquery.terminal": [ + "node_modules/jquery.terminal/js/jquery.terminal.js" + ], + "tooltipster": [ + "node_modules/tooltipster/dist/js/tooltipster.bundle.min.js" + ], + "jstree": [ + "node_modules/jstree/dist/jstree.min.js" ] }, "lib": [