Merge branch 'master' of https://github.com/thingsboard/thingsboard into UITests_AssetsProfiles

This commit is contained in:
Seraphym-Tuhai 2023-01-23 12:41:51 +02:00
commit e55f563b53
13 changed files with 159 additions and 69 deletions

View File

@ -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

View File

@ -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<>();
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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) {

View File

@ -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>

View File

@ -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],

View File

@ -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;

View File

@ -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;
}

View File

@ -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:**

View File

@ -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",

View File

@ -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 {
/*********************************