Merge branch 'master' of https://github.com/thingsboard/thingsboard into UITests_AssetsProfiles
This commit is contained in:
		
						commit
						e55f563b53
					
				@ -30,6 +30,7 @@ import org.springframework.security.config.annotation.authentication.builders.Au
 | 
			
		||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 | 
			
		||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
			
		||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 | 
			
		||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
 | 
			
		||||
import org.springframework.security.config.http.SessionCreationPolicy;
 | 
			
		||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 | 
			
		||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
 | 
			
		||||
@ -181,15 +182,8 @@ public class ThingsboardSecurityConfiguration {
 | 
			
		||||
    private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    @Order(0)
 | 
			
		||||
    SecurityFilterChain resources(HttpSecurity http) throws Exception {
 | 
			
		||||
        http
 | 
			
		||||
                .requestMatchers((matchers) -> matchers.antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"))
 | 
			
		||||
                .authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
 | 
			
		||||
                .requestCache().disable()
 | 
			
		||||
                .securityContext().disable()
 | 
			
		||||
                .sessionManagement().disable();
 | 
			
		||||
        return http.build();
 | 
			
		||||
    public WebSecurityCustomizer webSecurityCustomizer() {
 | 
			
		||||
        return (web) -> web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,7 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
 | 
			
		||||
    protected Object[] prepareArgs(TbMsg msg) {
 | 
			
		||||
        Object[] args = new Object[3];
 | 
			
		||||
        if (msg.getData() != null) {
 | 
			
		||||
            args[0] = JacksonUtil.fromString(msg.getData(), Map.class);
 | 
			
		||||
            args[0] = JacksonUtil.fromString(msg.getData(), Object.class);
 | 
			
		||||
        } else {
 | 
			
		||||
            args[0] = new HashMap<>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.rule.engine.deduplication;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbMsg;
 | 
			
		||||
 | 
			
		||||
import java.util.LinkedList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@Data
 | 
			
		||||
public class DeduplicationData {
 | 
			
		||||
 | 
			
		||||
    private final List<TbMsg> msgList;
 | 
			
		||||
    private boolean tickScheduled;
 | 
			
		||||
 | 
			
		||||
    public DeduplicationData() {
 | 
			
		||||
        msgList = new LinkedList<>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int size() {
 | 
			
		||||
        return msgList.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void add(TbMsg msg) {
 | 
			
		||||
        msgList.add(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isEmpty() {
 | 
			
		||||
        return msgList.isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.deduplication;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ArrayNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.data.util.Pair;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.rule.engine.api.RuleNode;
 | 
			
		||||
import org.thingsboard.rule.engine.api.TbContext;
 | 
			
		||||
@ -37,7 +36,6 @@ import java.util.ArrayList;
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.LinkedList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
@ -61,14 +59,14 @@ import java.util.concurrent.TimeUnit;
 | 
			
		||||
public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
 | 
			
		||||
    private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg";
 | 
			
		||||
    private static final int TB_MSG_DEDUPLICATION_TIMEOUT = 5000;
 | 
			
		||||
    public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10;
 | 
			
		||||
    private static final String EMPTY_DATA = "";
 | 
			
		||||
    private static final TbMsgMetaData EMPTY_META_DATA = new TbMsgMetaData();
 | 
			
		||||
 | 
			
		||||
    private TbMsgDeduplicationNodeConfiguration config;
 | 
			
		||||
 | 
			
		||||
    private final Map<EntityId, List<TbMsg>> deduplicationMap;
 | 
			
		||||
    private final Map<EntityId, DeduplicationData> deduplicationMap;
 | 
			
		||||
    private long deduplicationInterval;
 | 
			
		||||
    private long lastScheduledTs;
 | 
			
		||||
    private DeduplicationId deduplicationId;
 | 
			
		||||
 | 
			
		||||
    public TbMsgDeduplicationNode() {
 | 
			
		||||
@ -80,17 +78,12 @@ public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
        this.config = TbNodeUtils.convert(configuration, TbMsgDeduplicationNodeConfiguration.class);
 | 
			
		||||
        this.deduplicationInterval = TimeUnit.SECONDS.toMillis(config.getInterval());
 | 
			
		||||
        this.deduplicationId = config.getId();
 | 
			
		||||
        scheduleTickMsg(ctx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
 | 
			
		||||
        if (TB_MSG_DEDUPLICATION_TIMEOUT_MSG.equals(msg.getType())) {
 | 
			
		||||
            try {
 | 
			
		||||
                processDeduplication(ctx);
 | 
			
		||||
            } finally {
 | 
			
		||||
                scheduleTickMsg(ctx);
 | 
			
		||||
            }
 | 
			
		||||
            processDeduplication(ctx, msg.getOriginator());
 | 
			
		||||
        } else {
 | 
			
		||||
            processOnRegularMsg(ctx, msg);
 | 
			
		||||
        }
 | 
			
		||||
@ -103,11 +96,12 @@ public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
 | 
			
		||||
    private void processOnRegularMsg(TbContext ctx, TbMsg msg) {
 | 
			
		||||
        EntityId id = getDeduplicationId(ctx, msg);
 | 
			
		||||
        List<TbMsg> deduplicationMsgs = deduplicationMap.computeIfAbsent(id, k -> new LinkedList<>());
 | 
			
		||||
        DeduplicationData deduplicationMsgs = deduplicationMap.computeIfAbsent(id, k -> new DeduplicationData());
 | 
			
		||||
        if (deduplicationMsgs.size() < config.getMaxPendingMsgs()) {
 | 
			
		||||
            log.trace("[{}][{}] Adding msg: [{}][{}] to the pending msgs map ...", ctx.getSelfId(), id, msg.getId(), msg.getMetaDataTs());
 | 
			
		||||
            deduplicationMsgs.add(msg);
 | 
			
		||||
            ctx.ack(msg);
 | 
			
		||||
            scheduleTickMsg(ctx, id, deduplicationMsgs);
 | 
			
		||||
        } else {
 | 
			
		||||
            log.trace("[{}] Max limit of pending messages reached for deduplication id: [{}]", ctx.getSelfId(), id);
 | 
			
		||||
            ctx.tellFailure(msg, new RuntimeException("[" + ctx.getSelfId() + "] Max limit of pending messages reached for deduplication id: [" + id + "]"));
 | 
			
		||||
@ -127,22 +121,25 @@ public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void processDeduplication(TbContext ctx) {
 | 
			
		||||
        if (deduplicationMap.isEmpty()) {
 | 
			
		||||
    private void processDeduplication(TbContext ctx, EntityId deduplicationId) {
 | 
			
		||||
        DeduplicationData data = deduplicationMap.get(deduplicationId);
 | 
			
		||||
        if (data == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        data.setTickScheduled(false);
 | 
			
		||||
        if (data.isEmpty()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        List<TbMsg> deduplicationResults = new ArrayList<>();
 | 
			
		||||
        long deduplicationTimeoutMs = System.currentTimeMillis();
 | 
			
		||||
        deduplicationMap.forEach((entityId, tbMsgs) -> {
 | 
			
		||||
            if (tbMsgs.isEmpty()) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            Optional<TbPair<Long, Long>> packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs);
 | 
			
		||||
        try {
 | 
			
		||||
            List<TbMsg> deduplicationResults = new ArrayList<>();
 | 
			
		||||
            List<TbMsg> msgList = data.getMsgList();
 | 
			
		||||
            Optional<TbPair<Long, Long>> packBoundsOpt = findValidPack(msgList, deduplicationTimeoutMs);
 | 
			
		||||
            while (packBoundsOpt.isPresent()) {
 | 
			
		||||
                TbPair<Long, Long> packBounds = packBoundsOpt.get();
 | 
			
		||||
                if (DeduplicationStrategy.ALL.equals(config.getStrategy())) {
 | 
			
		||||
                    List<TbMsg> pack = new ArrayList<>();
 | 
			
		||||
                    for (Iterator<TbMsg> iterator = tbMsgs.iterator(); iterator.hasNext(); ) {
 | 
			
		||||
                    for (Iterator<TbMsg> iterator = msgList.iterator(); iterator.hasNext(); ) {
 | 
			
		||||
                        TbMsg msg = iterator.next();
 | 
			
		||||
                        long msgTs = msg.getMetaDataTs();
 | 
			
		||||
                        if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) {
 | 
			
		||||
@ -153,13 +150,13 @@ public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
                    deduplicationResults.add(TbMsg.newMsg(
 | 
			
		||||
                            config.getQueueName(),
 | 
			
		||||
                            config.getOutMsgType(),
 | 
			
		||||
                            entityId,
 | 
			
		||||
                            deduplicationId,
 | 
			
		||||
                            getMetadata(),
 | 
			
		||||
                            getMergedData(pack)));
 | 
			
		||||
                } else {
 | 
			
		||||
                    TbMsg resultMsg = null;
 | 
			
		||||
                    boolean searchMin = DeduplicationStrategy.FIRST.equals(config.getStrategy());
 | 
			
		||||
                    for (Iterator<TbMsg> iterator = tbMsgs.iterator(); iterator.hasNext(); ) {
 | 
			
		||||
                    for (Iterator<TbMsg> iterator = msgList.iterator(); iterator.hasNext(); ) {
 | 
			
		||||
                        TbMsg msg = iterator.next();
 | 
			
		||||
                        long msgTs = msg.getMetaDataTs();
 | 
			
		||||
                        if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) {
 | 
			
		||||
@ -173,10 +170,21 @@ public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
                    }
 | 
			
		||||
                    deduplicationResults.add(resultMsg);
 | 
			
		||||
                }
 | 
			
		||||
                packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs);
 | 
			
		||||
                packBoundsOpt = findValidPack(msgList, deduplicationTimeoutMs);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        deduplicationResults.forEach(outMsg -> enqueueForTellNextWithRetry(ctx, outMsg, 0));
 | 
			
		||||
            deduplicationResults.forEach(outMsg -> enqueueForTellNextWithRetry(ctx, outMsg, 0));
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (!data.isEmpty()) {
 | 
			
		||||
                scheduleTickMsg(ctx, deduplicationId, data);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleTickMsg(TbContext ctx, EntityId deduplicationId, DeduplicationData data) {
 | 
			
		||||
        if (!data.isTickScheduled()) {
 | 
			
		||||
            scheduleTickMsg(ctx, deduplicationId);
 | 
			
		||||
            data.setTickScheduled(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<TbPair<Long, Long>> findValidPack(List<TbMsg> msgs, long deduplicationTimeoutMs) {
 | 
			
		||||
@ -206,15 +214,8 @@ public class TbMsgDeduplicationNode implements TbNode {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleTickMsg(TbContext ctx) {
 | 
			
		||||
        long curTs = System.currentTimeMillis();
 | 
			
		||||
        if (lastScheduledTs == 0L) {
 | 
			
		||||
            lastScheduledTs = curTs;
 | 
			
		||||
        }
 | 
			
		||||
        lastScheduledTs += TB_MSG_DEDUPLICATION_TIMEOUT;
 | 
			
		||||
        long curDelay = Math.max(0L, (lastScheduledTs - curTs));
 | 
			
		||||
        TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
 | 
			
		||||
        ctx.tellSelf(tickMsg, curDelay);
 | 
			
		||||
    private void scheduleTickMsg(TbContext ctx, EntityId deduplicationId) {
 | 
			
		||||
        ctx.tellSelf(ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, deduplicationId, EMPTY_META_DATA, EMPTY_DATA), deduplicationInterval + 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getMergedData(List<TbMsg> msgs) {
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ import { TranslateService } from '@ngx-translate/core';
 | 
			
		||||
import { DomSanitizer } from '@angular/platform-browser';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { TbInject } from '@shared/decorators/tb-inject';
 | 
			
		||||
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
 | 
			
		||||
 | 
			
		||||
@Directive()
 | 
			
		||||
// tslint:disable-next-line:directive-class-suffix
 | 
			
		||||
@ -83,6 +84,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
 | 
			
		||||
    this.ctx.resourceService = $injector.get(ResourceService);
 | 
			
		||||
    this.ctx.telemetryWsService = $injector.get(TelemetryWebsocketService);
 | 
			
		||||
    this.ctx.date = $injector.get(DatePipe);
 | 
			
		||||
    this.ctx.milliSecondsToTimeString = $injector.get(MillisecondsToTimeStringPipe);
 | 
			
		||||
    this.ctx.translate = $injector.get(TranslateService);
 | 
			
		||||
    this.ctx.http = $injector.get(HttpClient);
 | 
			
		||||
    this.ctx.sanitizer = $injector.get(DomSanitizer);
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ interface MarkdownWidgetSettings {
 | 
			
		||||
  markdownCss: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MarkdownTextFunction = (data: FormattedData[]) => string;
 | 
			
		||||
type MarkdownTextFunction = (data: FormattedData[], ctx: WidgetContext) => string;
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-markdown-widget ',
 | 
			
		||||
@ -72,7 +72,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.ctx.$scope.markdownWidget = this;
 | 
			
		||||
    this.settings = this.ctx.settings;
 | 
			
		||||
    this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data']) : null;
 | 
			
		||||
    this.markdownTextFunction = this.settings.useMarkdownTextFunction ?
 | 
			
		||||
      parseFunction(this.settings.markdownTextFunction, ['data', 'ctx']) : null;
 | 
			
		||||
    this.markdownClass = 'markdown-widget';
 | 
			
		||||
    const cssString = this.settings.markdownCss;
 | 
			
		||||
    if (isNotEmptyStr(cssString)) {
 | 
			
		||||
@ -117,7 +118,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
 | 
			
		||||
    }
 | 
			
		||||
    const data = formattedDataFormDatasourceData(initialData);
 | 
			
		||||
    let markdownText = this.settings.useMarkdownTextFunction ?
 | 
			
		||||
      safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern;
 | 
			
		||||
      safeExecute(this.markdownTextFunction, [data, this.ctx]) : this.settings.markdownTextPattern;
 | 
			
		||||
    const allData: FormattedData = flatDataWithoutOverride(data);
 | 
			
		||||
    markdownText = createLabelFromPattern(markdownText, allData);
 | 
			
		||||
    if (this.markdownText !== markdownText) {
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
  <tb-js-func [fxShow]="markdownWidgetSettingsForm.get('useMarkdownTextFunction').value"
 | 
			
		||||
              formControlName="markdownTextFunction"
 | 
			
		||||
              [globalVariables]="functionScopeVariables"
 | 
			
		||||
              [functionArgs]="['data']"
 | 
			
		||||
              [functionArgs]="['data', 'ctx']"
 | 
			
		||||
              functionTitle="{{ 'widgets.markdown.markdown-text-function' | translate }}"
 | 
			
		||||
              helpId="widget/lib/markdown/markdown_text_fn">
 | 
			
		||||
  </tb-js-func>
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ import { AuthService } from '@core/auth/auth.service';
 | 
			
		||||
import { ResourceService } from '@core/http/resource.service';
 | 
			
		||||
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
 | 
			
		||||
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
 | 
			
		||||
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
 | 
			
		||||
 | 
			
		||||
export const ServicesMap = new Map<string, Type<any>>(
 | 
			
		||||
  [
 | 
			
		||||
@ -57,6 +58,7 @@ export const ServicesMap = new Map<string, Type<any>>(
 | 
			
		||||
   ['dialogs', DialogService],
 | 
			
		||||
   ['customDialog', CustomDialogService],
 | 
			
		||||
   ['date', DatePipe],
 | 
			
		||||
   ['milliSecondsToTimeString', MillisecondsToTimeStringPipe],
 | 
			
		||||
   ['utils', UtilsService],
 | 
			
		||||
   ['translate', TranslateService],
 | 
			
		||||
   ['http', HttpClient],
 | 
			
		||||
 | 
			
		||||
@ -88,7 +88,7 @@ import * as RxJSOperators from 'rxjs/operators';
 | 
			
		||||
import { TbPopoverComponent } from '@shared/components/popover.component';
 | 
			
		||||
import { EntityId } from '@shared/models/id/entity-id';
 | 
			
		||||
import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models';
 | 
			
		||||
import { TelemetrySubscriber } from '@app/shared/public-api';
 | 
			
		||||
import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
 | 
			
		||||
 | 
			
		||||
export interface IWidgetAction {
 | 
			
		||||
  name: string;
 | 
			
		||||
@ -182,6 +182,7 @@ export class WidgetContext {
 | 
			
		||||
  telemetryWsService: TelemetryWebsocketService;
 | 
			
		||||
  telemetrySubscribers?: TelemetrySubscriber[];
 | 
			
		||||
  date: DatePipe;
 | 
			
		||||
  milliSecondsToTimeString: MillisecondsToTimeStringPipe;
 | 
			
		||||
  translate: TranslateService;
 | 
			
		||||
  http: HttpClient;
 | 
			
		||||
  sanitizer: DomSanitizer;
 | 
			
		||||
 | 
			
		||||
@ -25,33 +25,51 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
 | 
			
		||||
  constructor(private translate: TranslateService) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  transform(millseconds: number, args?: any): string {
 | 
			
		||||
  transform(millseconds: number, shortFormat = false): string {
 | 
			
		||||
    let seconds = Math.floor(millseconds / 1000);
 | 
			
		||||
    const days = Math.floor(seconds / 86400);
 | 
			
		||||
    let hours = Math.floor((seconds % 86400) / 3600);
 | 
			
		||||
    let minutes = Math.floor(((seconds % 86400) % 3600) / 60);
 | 
			
		||||
    seconds = seconds % 60;
 | 
			
		||||
    let timeString = '';
 | 
			
		||||
    if (days > 0) {
 | 
			
		||||
      timeString += this.translate.instant('timewindow.days', {days});
 | 
			
		||||
    }
 | 
			
		||||
    if (hours > 0) {
 | 
			
		||||
      if (timeString.length === 0 && hours === 1) {
 | 
			
		||||
        hours = 0;
 | 
			
		||||
    if (shortFormat) {
 | 
			
		||||
      if (days > 0) {
 | 
			
		||||
        timeString += this.translate.instant('timewindow.short.days', {days});
 | 
			
		||||
      }
 | 
			
		||||
      timeString += this.translate.instant('timewindow.hours', {hours});
 | 
			
		||||
    }
 | 
			
		||||
    if (minutes > 0) {
 | 
			
		||||
      if (timeString.length === 0 && minutes === 1) {
 | 
			
		||||
        minutes = 0;
 | 
			
		||||
      if (hours > 0) {
 | 
			
		||||
        timeString += this.translate.instant('timewindow.short.hours', {hours});
 | 
			
		||||
      }
 | 
			
		||||
      timeString += this.translate.instant('timewindow.minutes', {minutes});
 | 
			
		||||
    }
 | 
			
		||||
    if (seconds > 0) {
 | 
			
		||||
      if (timeString.length === 0 && seconds === 1) {
 | 
			
		||||
        seconds = 0;
 | 
			
		||||
      if (minutes > 0) {
 | 
			
		||||
        timeString += this.translate.instant('timewindow.short.minutes', {minutes});
 | 
			
		||||
      }
 | 
			
		||||
      if (seconds > 0) {
 | 
			
		||||
        timeString += this.translate.instant('timewindow.short.seconds', {seconds});
 | 
			
		||||
      }
 | 
			
		||||
      if (!timeString.length) {
 | 
			
		||||
        timeString += this.translate.instant('timewindow.short.seconds', {seconds: 0});
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (days > 0) {
 | 
			
		||||
        timeString += this.translate.instant('timewindow.days', {days});
 | 
			
		||||
      }
 | 
			
		||||
      if (hours > 0) {
 | 
			
		||||
        if (timeString.length === 0 && hours === 1) {
 | 
			
		||||
          hours = 0;
 | 
			
		||||
        }
 | 
			
		||||
        timeString += this.translate.instant('timewindow.hours', {hours});
 | 
			
		||||
      }
 | 
			
		||||
      if (minutes > 0) {
 | 
			
		||||
        if (timeString.length === 0 && minutes === 1) {
 | 
			
		||||
          minutes = 0;
 | 
			
		||||
        }
 | 
			
		||||
        timeString += this.translate.instant('timewindow.minutes', {minutes});
 | 
			
		||||
      }
 | 
			
		||||
      if (seconds > 0) {
 | 
			
		||||
        if (timeString.length === 0 && seconds === 1) {
 | 
			
		||||
          seconds = 0;
 | 
			
		||||
        }
 | 
			
		||||
        timeString += this.translate.instant('timewindow.seconds', {seconds});
 | 
			
		||||
      }
 | 
			
		||||
      timeString += this.translate.instant('timewindow.seconds', {seconds});
 | 
			
		||||
    }
 | 
			
		||||
    return timeString;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
<div class="divider"></div>
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
*function (data): string*
 | 
			
		||||
*function (data, ctx): string*
 | 
			
		||||
 | 
			
		||||
A JavaScript function used to calculate markdown or HTML content.
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,9 @@ A JavaScript function used to calculate markdown or HTML content.
 | 
			
		||||
  <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[]</a></code> - An array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects resolved from configured datasources.<br/>
 | 
			
		||||
     Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration.
 | 
			
		||||
  </li>
 | 
			
		||||
  <li><b>ctx:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API 
 | 
			
		||||
     and data used by widget instance.
 | 
			
		||||
  </li>
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
**Returns:**
 | 
			
		||||
 | 
			
		||||
@ -3379,6 +3379,12 @@
 | 
			
		||||
        "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }",
 | 
			
		||||
        "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }",
 | 
			
		||||
        "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }",
 | 
			
		||||
        "short": {
 | 
			
		||||
            "days": "{ days, plural, 1 {1 day } other {# days } }",
 | 
			
		||||
            "hours": "{ hours, plural, 1 {1 hour } other {# hours } }",
 | 
			
		||||
            "minutes": "{{minutes}} min ",
 | 
			
		||||
            "seconds": "{{seconds}} sec "
 | 
			
		||||
        },
 | 
			
		||||
        "realtime": "Realtime",
 | 
			
		||||
        "history": "History",
 | 
			
		||||
        "last-prefix": "last",
 | 
			
		||||
 | 
			
		||||
@ -850,6 +850,23 @@ mat-label {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tooltipster
 | 
			
		||||
 | 
			
		||||
.tooltipster-sidetip.tooltipster-tb {
 | 
			
		||||
  .tooltipster-box {
 | 
			
		||||
    background: rgba(3, 8, 40, 0.64);
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    .tooltipster-content {
 | 
			
		||||
      padding: 4px 8px;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      line-height: 16px;
 | 
			
		||||
      font-weight: 500;
 | 
			
		||||
      color: #ffffff;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tb-default, .tb-dark {
 | 
			
		||||
 | 
			
		||||
  /*********************************
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user