Browse Source

Merge remote-tracking branch 'origin/master' into feature/remote-js-executor-improvements

pull/7403/head
ViacheslavKlimov 4 years ago
parent
commit
100868d8e2
  1. 71
      application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java
  3. 19
      application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java
  4. 4
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  5. 3
      application/src/main/resources/thingsboard.yml
  6. 96
      application/src/test/java/org/thingsboard/server/service/script/JsInvokeServiceTest.java
  7. 2
      application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java
  8. 17
      msa/js-executor/api/jsInvokeMessageProcessor.ts
  9. 1
      msa/js-executor/config/custom-environment-variables.yml
  10. 1
      msa/js-executor/config/default.yml
  11. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  12. 18
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  13. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
  14. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts
  15. 9
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  16. 25
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  17. 19
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  18. 9
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  19. 33
      ui-ngx/src/app/shared/components/popover.component.ts
  20. 22
      ui-ngx/src/app/shared/components/popover.service.ts

71
application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java

@ -17,7 +17,10 @@ package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId;
@ -30,9 +33,10 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
/**
* Created by ashvayka on 26.09.18.
*/
@ -45,6 +49,16 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
protected Map<UUID, String> scriptIdToNameMap = new ConcurrentHashMap<>();
protected Map<UUID, DisableListInfo> disabledFunctions = new ConcurrentHashMap<>();
@Getter
@Value("${js.max_total_args_size:100000}")
private long maxTotalArgsSize;
@Getter
@Value("${js.max_result_size:300000}")
private long maxResultSize;
@Getter
@Value("${js.max_script_body_size:50000}")
private long maxScriptBodySize;
protected AbstractJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
this.apiUsageStateService = apiUsageStateService;
this.apiUsageClient = apiUsageClient;
@ -65,33 +79,45 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
@Override
public ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
if (scriptBodySizeExceeded(scriptBody)) {
return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()));
}
UUID scriptId = UUID.randomUUID();
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
return doEval(scriptId, functionName, jsScript);
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
return error("JS Execution is disabled due to API limits!");
}
}
@Override
public ListenableFuture<Object> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
public ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
String functionName = scriptIdToNameMap.get(scriptId);
if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
return error("No compiled script found for scriptId: [" + scriptId + "]!");
}
if (!isDisabled(scriptId)) {
if (argsSizeExceeded(args)) {
return scriptExecutionError(scriptId, format("Script input arguments exceed maximum allowed total args size of %s symbols", getMaxTotalArgsSize()));
}
apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return doInvokeFunction(scriptId, functionName, args);
return Futures.transformAsync(doInvokeFunction(scriptId, functionName, args), output -> {
String result = output.toString();
if (resultSizeExceeded(result)) {
return scriptExecutionError(scriptId, format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize()));
}
return Futures.immediateFuture(result);
}, MoreExecutors.directExecutor());
} else {
String message = "Script invocation is blocked due to maximum error count "
+ getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
return Futures.immediateFailedFuture(new RuntimeException(message));
return error(message);
}
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
return error("JS Execution is disabled due to API limits!");
}
}
@ -127,6 +153,27 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
disableListInfo.incrementAndGet();
}
private boolean scriptBodySizeExceeded(String scriptBody) {
if (getMaxScriptBodySize() <= 0) return false;
return scriptBody.length() > getMaxScriptBodySize();
}
private boolean argsSizeExceeded(Object[] args) {
if (getMaxTotalArgsSize() <= 0) return false;
long totalArgsSize = 0;
for (Object arg : args) {
if (arg instanceof CharSequence) {
totalArgsSize += ((CharSequence) arg).length();
}
}
return totalArgsSize > getMaxTotalArgsSize();
}
private boolean resultSizeExceeded(String result) {
if (getMaxResultSize() <= 0) return false;
return result.length() > getMaxResultSize();
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
if (scriptType == JsScriptType.RULE_NODE_SCRIPT) {
return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
@ -148,6 +195,16 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
}
}
private <T> ListenableFuture<T> error(String message) {
return Futures.immediateFailedFuture(new RuntimeException(message));
}
private <T> ListenableFuture<T> scriptExecutionError(UUID scriptId, String errorMsg) {
RuntimeException error = new RuntimeException(errorMsg);
onScriptExecutionError(scriptId, error, null);
return Futures.immediateFailedFuture(error);
}
private class DisableListInfo {
private final AtomicInteger counter;
private long expirationTime;

2
application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java

@ -25,7 +25,7 @@ public interface JsInvokeService {
ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<Object> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
ListenableFuture<Void> release(UUID scriptId);

19
application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.script;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -32,12 +33,15 @@ public class NashornJsInvokeService extends AbstractNashornJsInvokeService {
@Value("${js.local.use_js_sandbox}")
private boolean useJsSandbox;
@Getter
@Value("${js.local.monitor_thread_pool_size}")
private int monitorThreadPoolSize;
@Getter
@Value("${js.local.max_cpu_time}")
private long maxCpuTime;
@Getter
@Value("${js.local.max_errors}")
private int maxErrors;
@ -53,21 +57,6 @@ public class NashornJsInvokeService extends AbstractNashornJsInvokeService {
return useJsSandbox;
}
@Override
protected int getMonitorThreadPoolSize() {
return monitorThreadPoolSize;
}
@Override
protected long getMaxCpuTime() {
return maxCpuTime;
}
@Override
protected int getMaxErrors() {
return maxErrors;
}
@Override
protected long getMaxBlacklistDuration() {
return TimeUnit.SECONDS.toMillis(maxBlackListDurationSec);

4
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -210,11 +210,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
}
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, Object... args) {
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, String... args) {
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, customerId, this.scriptId, args),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o.toString()));
return Futures.immediateFuture(mapper.readTree(o));
} catch (Exception e) {
if (e.getCause() instanceof ScriptException) {
return Futures.immediateFailedFuture(e.getCause());

3
application/src/main/resources/thingsboard.yml

@ -595,6 +595,9 @@ state:
js:
evaluator: "${JS_EVALUATOR:local}" # local/remote
max_total_args_size: "${JS_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${JS_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${JS_MAX_SCRIPT_BODY_SIZE:50000}"
# Built-in JVM JavaScript environment properties
local:
# Use Sandboxed (secured) JVM JavaScript environment

96
application/src/test/java/org/thingsboard/server/service/script/JsInvokeServiceTest.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2022 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.server.service.script;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
@TestPropertySource(properties = {
"js.max_script_body_size=50",
"js.max_total_args_size=50",
"js.max_result_size=50",
"js.local.max_errors=2"
})
class JsInvokeServiceTest extends AbstractControllerTest {
@Autowired
private NashornJsInvokeService jsInvokeService;
@Value("${js.local.max_errors}")
private int maxJsErrors;
@Test
void givenTooBigScriptForEval_thenReturnError() {
String hugeScript = "var a = 'qwertyqwertywertyqwabababer'; return {a: a};";
assertThatThrownBy(() -> {
evalScript(hugeScript);
}).hasMessageContaining("body exceeds maximum allowed size");
}
@Test
void givenTooBigScriptInputArgs_thenReturnErrorAndReportScriptExecutionError() throws Exception {
String script = "return { msg: msg };";
String hugeMsg = "{\"input\":\"123456781234349\"}";
UUID scriptId = evalScript(script);
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, hugeMsg);
}).hasMessageContaining("input arguments exceed maximum");
}
assertThatScriptIsBlocked(scriptId);
}
@Test
void whenScriptInvocationResultIsTooBig_thenReturnErrorAndReportScriptExecutionError() throws Exception {
String script = "var s = new Array(50).join('a'); return { s: s};";
UUID scriptId = evalScript(script);
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
}).hasMessageContaining("result exceeds maximum allowed size");
}
assertThatScriptIsBlocked(scriptId);
}
private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
}).hasMessageContaining("invocation is blocked due to maximum error");
}
private UUID evalScript(String script) throws ExecutionException, InterruptedException {
return jsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, script).get();
}
private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException {
return jsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get();
}
}

2
application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java

@ -37,7 +37,7 @@ public class MockJsInvokeService implements JsInvokeService {
}
@Override
public ListenableFuture<Object> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
public ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
log.warn("invokeFunction {} {} {} {}", tenantId, customerId, scriptId, args);
return Futures.immediateFuture("{}");
}

17
msa/js-executor/api/jsInvokeMessageProcessor.ts

@ -44,6 +44,7 @@ const useSandbox = config.get('script.use_sandbox') === 'true';
const maxActiveScripts = Number(config.get('script.max_active_scripts'));
const slowQueryLogMs = Number(config.get('script.slow_query_log_ms'));
const slowQueryLogBody = config.get('script.slow_query_log_body') === 'true';
const maxResultSize = Number(config.get('js.max_result_size'));
export class JsInvokeMessageProcessor {
@ -165,9 +166,19 @@ export class JsInvokeMessageProcessor {
(script) => {
this.executor.executeScript(script, invokeRequest.args, invokeRequest.timeout).then(
(result) => {
const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse(result, true);
this.logger.debug('[%s] Sending success invoke response, scriptId: [%s]', requestId, scriptId);
this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse);
if (result.length <= maxResultSize) {
const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse(result, true);
this.logger.debug('[%s] Sending success invoke response, scriptId: [%s]', requestId, scriptId);
this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse);
} else {
let err = {
name: 'Error',
message: 'script invocation result exceeds maximum allowed size of ' + maxResultSize + ' symbols'
}
const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse("", false, RUNTIME_ERROR, err);
this.logger.debug('[%s] Script invocation result exceeds maximum allowed size of %s symbols, scriptId: [%s]', requestId, maxResultSize, scriptId);
this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse);
}
},
(err: any) => {
let errorCode;

1
msa/js-executor/config/custom-environment-variables.yml

@ -20,6 +20,7 @@ http_port: "HTTP_PORT" # /livenessProbe
js:
response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS"
max_result_size: "JS_MAX_RESULT_SIZE"
kafka:
bootstrap:

1
msa/js-executor/config/default.yml

@ -20,6 +20,7 @@ http_port: "8888" # /livenessProbe
js:
response_poll_interval: "25"
max_result_size: "300000"
kafka:
bootstrap:

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -185,7 +185,8 @@
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard">
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
</mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show"
@ -200,7 +201,8 @@
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard">
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
</mat-drawer-content>
</mat-drawer-container>

18
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -17,13 +17,18 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, ElementRef, EventEmitter, HostBinding,
Component,
ElementRef,
EventEmitter,
HostBinding,
Inject,
Injector,
Input,
NgZone,
OnDestroy,
OnInit, Optional, Renderer2,
OnInit,
Optional,
Renderer2,
StaticProvider,
ViewChild,
ViewContainerRef,
@ -48,7 +53,7 @@ import {
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, guid, hashCode, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils';
import { deepClone, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils';
import {
DashboardContext,
DashboardPageLayout,
@ -129,7 +134,8 @@ import { MobileService } from '@core/services/mobile.service';
import {
DashboardImageDialogComponent,
DashboardImageDialogData, DashboardImageDialogResult
DashboardImageDialogData,
DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import cssjs from '@core/css/css';
@ -139,6 +145,7 @@ import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service';
import { tap } from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component';
// @dynamic
@Component({
@ -184,6 +191,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@Input()
parentDashboard?: IDashboardComponent = null;
@Input()
popoverComponent?: TbPopoverComponent = null;
@Input()
parentAliasController?: IAliasController = null;

3
ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html

@ -58,6 +58,7 @@
[isRemoveActionEnabled]="isEdit && !widgetEditMode"
[callbacks]="this"
[ignoreLoading]="layoutCtx.ignoreLoading"
[parentDashboard]="parentDashboard">
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard>
</div>

4
ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts

@ -33,6 +33,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ItemBufferService } from '@app/core/services/item-buffer.service';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-dashboard-layout',
@ -81,6 +82,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
@Input()
parentDashboard?: IDashboardComponent = null;
@Input()
popoverComponent?: TbPopoverComponent = null;
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
private rxSubscriptions = new Array<Subscription>();

9
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -15,7 +15,9 @@
///
import {
AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef,
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
Input,
@ -56,6 +58,7 @@ import { distinct } from 'rxjs/operators';
import { ResizeObserver } from '@juggle/resize-observer';
import { UtilsService } from '@core/services/utils.service';
import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-dashboard',
@ -136,6 +139,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
parentDashboard?: IDashboardComponent = null;
@Input()
popoverComponent?: TbPopoverComponent = null;
dashboardTimewindowChangedSubject: Subject<Timewindow> = new ReplaySubject<Timewindow>();
dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable().pipe(
@ -191,6 +197,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngOnInit(): void {
this.dashboardWidgets.parentDashboard = this.parentDashboard;
this.dashboardWidgets.popoverComponent = this.popoverComponent;
if (!this.dashboardTimewindow) {
this.dashboardTimewindow = this.timeService.defaultTimewindow();
}

25
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -28,7 +28,8 @@ import {
NgZone,
OnChanges,
OnDestroy,
OnInit, Renderer2,
OnInit,
Renderer2,
SimpleChanges,
ViewChild,
ViewContainerRef,
@ -39,12 +40,14 @@ import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition, MobileActionResult,
LegendPosition,
Widget,
WidgetActionDescriptor,
widgetActionSources,
WidgetActionType,
WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType,
WidgetComparisonSettings,
WidgetMobileActionDescriptor,
WidgetMobileActionType,
WidgetResource,
widgetType,
WidgetTypeParameters
@ -65,7 +68,9 @@ import {
validateEntityId
} from '@core/utils';
import {
IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams,
IDynamicWidgetComponent,
ShowWidgetHeaderActionFunction,
updateEntityParams,
WidgetContext,
WidgetHeaderAction,
WidgetInfo,
@ -109,9 +114,7 @@ import { MobileService } from '@core/services/mobile.service';
import { DialogService } from '@core/services/dialog.service';
import { PopoverPlacement } from '@shared/components/popover.models';
import { TbPopoverService } from '@shared/components/popover.service';
import {
DASHBOARD_PAGE_COMPONENT_TOKEN
} from '@home/components/tokens';
import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens';
@Component({
selector: 'tb-widget',
@ -1378,8 +1381,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
]
});
const component = this.popoverService.displayPopover(trigger, this.renderer,
this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
const componentRef = this.popoverService.createPopoverRef(this.widgetContentContainer);
const component = this.popoverService.displayPopoverWithComponentRef(componentRef, trigger, this.renderer,
this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
injector,
{
embedded: true,
@ -1388,7 +1392,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
currentState: objToBase64([stateObject]),
dashboard,
parentDashboard: this.widgetContext.parentDashboard ?
this.widgetContext.parentDashboard : this.widgetContext.dashboard
this.widgetContext.parentDashboard : this.widgetContext.dashboard,
popoverComponent: componentRef.instance
},
{width: popoverWidth, height: popoverHeight},
popoverStyle,

19
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -16,8 +16,8 @@
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import {
Datasource,
datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
FormattedData,
Widget,
WidgetPosition,
@ -25,14 +25,14 @@ import {
} from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { AggregationType, Timewindow } from '@shared/models/time/time.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs';
import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
export interface WidgetsData {
widgets: Array<Widget>;
@ -109,6 +109,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
parentDashboard?: IDashboardComponent;
popoverComponent?: TbPopoverComponent;
[Symbol.iterator](): Iterator<DashboardWidget> {
return this.activeDashboardWidgets[Symbol.iterator]();
}
@ -174,7 +176,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
switch (record.operation) {
case 'add':
this.dashboardWidgets.push(
new DashboardWidget(this.dashboard, record.widget, record.widgetLayout, this.parentDashboard)
new DashboardWidget(this.dashboard, record.widget, record.widgetLayout, this.parentDashboard, this.popoverComponent)
);
break;
case 'remove':
@ -190,7 +192,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
if (!isEqual(prevDashboardWidget.widget, record.widget) ||
!isEqual(prevDashboardWidget.widgetLayout, record.widgetLayout)) {
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout,
this.parentDashboard);
this.parentDashboard, this.popoverComponent);
this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
this.dashboardWidgets[index].selected = prevDashboardWidget.selected;
} else {
@ -345,7 +347,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
customHeaderActions: Array<WidgetHeaderAction>;
widgetActions: Array<WidgetAction>;
widgetContext = new WidgetContext(this.dashboard, this, this.widget, this.parentDashboard);
widgetContext = new WidgetContext(this.dashboard, this, this.widget, this.parentDashboard, this.popoverComponent);
widgetId: string;
@ -388,7 +390,8 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private dashboard: IDashboardComponent,
public widget: Widget,
public widgetLayout?: WidgetLayout,
private parentDashboard?: IDashboardComponent) {
private parentDashboard?: IDashboardComponent,
private popoverComponent?: TbPopoverComponent) {
if (!widget.id) {
widget.id = guid();
}

9
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -18,7 +18,8 @@ import { IDashboardComponent } from '@home/models/dashboard-component.models';
import {
DataSet,
Datasource,
DatasourceData, FormattedData,
DatasourceData,
FormattedData,
JsonSettingsSchema,
Widget,
WidgetActionDescriptor,
@ -37,7 +38,8 @@ import {
IStateController,
IWidgetSubscription,
IWidgetUtils,
RpcApi, StateParams,
RpcApi,
StateParams,
SubscriptionEntityInfo,
TimewindowFunctions,
WidgetActionsApi,
@ -113,7 +115,8 @@ export class WidgetContext {
constructor(public dashboard: IDashboardComponent,
private dashboardWidget: IDashboardWidget,
private widget: Widget,
public parentDashboard?: IDashboardComponent) {}
public parentDashboard?: IDashboardComponent,
public popoverComponent?: TbPopoverComponent) {}
get stateController(): IStateController {
return this.parentDashboard ? this.parentDashboard.stateController : this.dashboard.stateController;

33
ui-ngx/src/app/shared/components/popover.component.ts

@ -57,6 +57,7 @@ import {
} from '@shared/components/popover.models';
import { distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
import { animate, AnimationBuilder, AnimationMetadata, style } from '@angular/animations';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@ -304,6 +305,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
<div #popoverRoot [@popoverMotion]="tbAnimationState"
(@popoverMotion.done)="animationDone()">
<div
#popover
class="tb-popover"
[class.tb-popover-rtl]="dir === 'rtl'"
[ngClass]="classMap"
@ -340,6 +342,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
tbContent: string | TemplateRef<void> | null = null;
tbComponentFactory: ComponentFactory<any> | null = null;
@ -442,6 +445,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
constructor(
public cdr: ChangeDetectorRef,
private renderer: Renderer2,
private animationBuilder: AnimationBuilder,
@Optional() private directionality: Directionality
) {}
@ -532,6 +536,35 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
});
}
resize(width: string, height: string, animationDurationMs?: number) {
if (animationDurationMs && animationDurationMs > 0) {
const prevWidth = this.popover.nativeElement.offsetWidth;
const prevHeight = this.popover.nativeElement.offsetHeight;
const animationMetadata: AnimationMetadata[] = [style({width: prevWidth + 'px', height: prevHeight + 'px'}),
animate(animationDurationMs + 'ms', style({width, height}))];
const factory = this.animationBuilder.build(animationMetadata);
const player = factory.create(this.popover.nativeElement);
player.play();
const resize$ = new ResizeObserver(() => {
this.updatePosition();
});
resize$.observe(this.popover.nativeElement);
player.onDone(() => {
player.destroy();
resize$.disconnect();
this.setSize(width, height);
});
} else {
this.setSize(width, height);
}
}
private setSize(width: string, height: string) {
this.renderer.setStyle(this.popover.nativeElement, 'width', width);
this.renderer.setStyle(this.popover.nativeElement, 'height', height);
this.updatePosition();
}
updatePosition(): void {
if (this.origin && this.overlay && this.overlay.overlayRef) {
this.overlay.overlayRef.updatePosition();

22
ui-ngx/src/app/shared/components/popover.service.ts

@ -16,8 +16,12 @@
import {
ComponentFactory,
ComponentFactoryResolver, ElementRef, Inject,
Injectable, Injector,
ComponentFactoryResolver,
ComponentRef,
ElementRef,
Inject,
Injectable,
Injector,
Renderer2,
Type,
ViewContainerRef
@ -53,11 +57,23 @@ export class TbPopoverService {
}
}
createPopoverRef(hostView: ViewContainerRef): ComponentRef<TbPopoverComponent> {
return hostView.createComponent(this.componentFactory);
}
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent {
const componentRef = hostView.createComponent(this.componentFactory);
const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton);
}
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent {
const component = componentRef.instance;
this.popoverWithTriggers.push({
trigger,

Loading…
Cancel
Save