Merge branch 'master' of github.com:thingsboard/thingsboard

This commit is contained in:
ShvaykaD 2020-12-04 17:48:09 +02:00
commit 409fb1f2bc
25 changed files with 353 additions and 209 deletions

View File

@ -61,6 +61,7 @@ import org.thingsboard.server.service.telemetry.InternalTelemetryService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -73,6 +74,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
@ -347,16 +349,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
myTenantStates.values().forEach(state -> { myTenantStates.values().forEach(state -> {
if ((state.getNextCycleTs() > now) && (state.getNextCycleTs() - now < TimeUnit.HOURS.toMillis(1))) { if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
TenantId tenantId = state.getTenantId(); TenantId tenantId = state.getTenantId();
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
ToUsageStatsServiceMsg.Builder msg = ToUsageStatsServiceMsg.newBuilder(); saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
msg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); updateTenantState(state, tenantProfileCache.get(tenantId));
msg.setTenantIdLSB(tenantId.getId().getLeastSignificantBits());
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
msg.addValues(UsageStatsKVProto.newBuilder().setKey(key.name()).setValue(0).build());
}
process(new TbProtoQueueMsg<>(UUID.randomUUID(), msg.build()), TbCallback.EMPTY);
} }
}); });
} finally { } finally {
@ -364,6 +361,14 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
} }
} }
private void saveNewCounts(TenantApiUsageState state, List<ApiUsageRecordKey> keys) {
List<TsKvEntry> counts = keys.stream()
.map(key -> new BasicTsKvEntry(state.getCurrentCycleTs(), new LongDataEntry(key.getApiCountKey(), 0L)))
.collect(Collectors.toList());
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK);
}
private TenantApiUsageState getOrFetchState(TenantId tenantId) { private TenantApiUsageState getOrFetchState(TenantId tenantId) {
TenantApiUsageState tenantState = myTenantStates.get(tenantId); TenantApiUsageState tenantState = myTenantStates.get(tenantId);
if (tenantState == null) { if (tenantState == null) {
@ -377,6 +382,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
} }
TenantProfile tenantProfile = tenantProfileCache.get(tenantId); TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity); tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity);
List<ApiUsageRecordKey> newCounts = new ArrayList<>();
try { try {
List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get(); List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get();
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
@ -385,7 +391,13 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
for (TsKvEntry tsKvEntry : dbValues) { for (TsKvEntry tsKvEntry : dbValues) {
if (tsKvEntry.getKey().equals(key.getApiCountKey())) { if (tsKvEntry.getKey().equals(key.getApiCountKey())) {
cycleEntryFound = true; cycleEntryFound = true;
tenantState.put(key, tsKvEntry.getTs() == tenantState.getCurrentCycleTs() ? tsKvEntry.getLongValue().get() : 0L);
boolean oldCount = tsKvEntry.getTs() == tenantState.getCurrentCycleTs();
tenantState.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L);
if (!oldCount) {
newCounts.add(key);
}
} else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) { } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) {
hourlyEntryFound = true; hourlyEntryFound = true;
tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
@ -397,6 +409,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
} }
log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity); log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity);
myTenantStates.put(tenantId, tenantState); myTenantStates.put(tenantId, tenantState);
saveNewCounts(tenantState, newCounts);
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e); log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e);
} }

View File

@ -17,22 +17,19 @@ package org.thingsboard.server.common.msg.tools;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters; import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoField.DAY_OF_MONTH; import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoUnit.MONTHS; import static java.time.temporal.ChronoUnit.MONTHS;
public class SchedulerUtils { public class SchedulerUtils {
private final static ZoneId UTC = ZoneId.of("UTC");
private static final ConcurrentMap<String, ZoneId> tzMap = new ConcurrentHashMap<>(); private static final ConcurrentMap<String, ZoneId> tzMap = new ConcurrentHashMap<>();
public static ZoneId getZoneId(String tz) { public static ZoneId getZoneId(String tz) {
@ -44,7 +41,7 @@ public class SchedulerUtils {
} }
public static long getStartOfCurrentHour(ZoneId zoneId) { public static long getStartOfCurrentHour(ZoneId zoneId) {
return LocalDateTime.now(ZoneOffset.UTC).atZone(zoneId).truncatedTo(ChronoUnit.HOURS).toInstant().toEpochMilli(); return LocalDateTime.now(UTC).atZone(zoneId).truncatedTo(ChronoUnit.HOURS).toInstant().toEpochMilli();
} }
public static long getStartOfCurrentMonth() { public static long getStartOfCurrentMonth() {
@ -52,7 +49,7 @@ public class SchedulerUtils {
} }
public static long getStartOfCurrentMonth(ZoneId zoneId) { public static long getStartOfCurrentMonth(ZoneId zoneId) {
return LocalDate.now().withDayOfMonth(1).atStartOfDay(zoneId).toInstant().toEpochMilli(); return LocalDate.now(UTC).withDayOfMonth(1).atStartOfDay(zoneId).toInstant().toEpochMilli();
} }
public static long getStartOfNextMonth() { public static long getStartOfNextMonth() {
@ -60,7 +57,7 @@ public class SchedulerUtils {
} }
public static long getStartOfNextMonth(ZoneId zoneId) { public static long getStartOfNextMonth(ZoneId zoneId) {
return LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
} }
public static long getStartOfNextNextMonth() { public static long getStartOfNextNextMonth() {
@ -68,7 +65,7 @@ public class SchedulerUtils {
} }
public static long getStartOfNextNextMonth(ZoneId zoneId) { public static long getStartOfNextNextMonth(ZoneId zoneId) {
return LocalDate.now().with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
} }
public static TemporalAdjuster firstDayOfNextNextMonth() { public static TemporalAdjuster firstDayOfNextNextMonth() {

View File

@ -17,4 +17,4 @@
--> -->
<!--The content below is only a placeholder and can be replaced.--> <!--The content below is only a placeholder and can be replaced.-->
<router-outlet></router-outlet> <router-outlet (activate)="onActivateComponent($event)"></router-outlet>

View File

@ -62,7 +62,7 @@ export class AppComponent implements OnInit {
this.matIconRegistry.addSvgIconLiteral( this.matIconRegistry.addSvgIconLiteral(
'alpha-e-circle-outline', 'alpha-e-circle-outline',
this.domSanitizer.bypassSecurityTrustHtml( this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 24 24"><path d="M9,7H15V9H11V11H15V13H11V15H15V17H9V7M12,2A10,10 0 0,'+ '<svg viewBox="0 0 24 24"><path d="M9,7H15V9H11V11H15V13H11V15H15V17H9V7M12,2A10,10 0 0,' +
'1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 ' + '1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 ' +
'0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>' '0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>'
) )
@ -124,5 +124,11 @@ export class AppComponent implements OnInit {
ngOnInit() { ngOnInit() {
} }
} onActivateComponent($event: any) {
const loadingElement = $('div#tb-loading-spinner');
if (loadingElement.length) {
loadingElement.remove();
}
}
}

View File

@ -198,7 +198,7 @@ export class WidgetSubscriptionContext {
export type SubscriptionMessageSeverity = 'info' | 'warn' | 'error' | 'success'; export type SubscriptionMessageSeverity = 'info' | 'warn' | 'error' | 'success';
export interface SubscriptionMessage { export interface SubscriptionMessage {
severity: SubscriptionMessageSeverity, severity: SubscriptionMessageSeverity;
message: string; message: string;
} }

View File

@ -90,7 +90,7 @@
matTooltipPosition="above" matTooltipPosition="above"
class="mat-subheading-2 title"> class="mat-subheading-2 title">
<mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon> <mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
{{widget.title}} {{widget.customTranslatedTitle}}
</span> </span>
<tb-timewindow *ngIf="widget.hasTimewindow" <tb-timewindow *ngIf="widget.hasTimewindow"
#timewindowComponent #timewindowComponent

View File

@ -54,6 +54,7 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { SafeStyle } from '@angular/platform-browser'; import { SafeStyle } from '@angular/platform-browser';
import { distinct } from 'rxjs/operators'; import { distinct } from 'rxjs/operators';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { UtilsService } from '@core/services/utils.service';
@Component({ @Component({
selector: 'tb-dashboard', selector: 'tb-dashboard',
@ -168,6 +169,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
private gridsterResize$: ResizeObserver; private gridsterResize$: ResizeObserver;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
public utils: UtilsService,
private timeService: TimeService, private timeService: TimeService,
private dialogService: DialogService, private dialogService: DialogService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,

View File

@ -17,28 +17,28 @@
--> -->
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateValueFormGroup"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateValueFormGroup">
<div fxFlex fxLayout="column" [fxShow]="!dynamicMode"> <div fxFlex fxLayout="column" [fxShow]="!dynamicMode">
<div fxFlex fxLayout="column" [ngSwitch]="valueType"> <div fxLayout="column" [ngSwitch]="valueType">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING"> <ng-template [ngSwitchCase]="valueTypeEnum.STRING">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<input matInput formControlName="defaultValue" placeholder="{{'filter.value' | translate}}"> <input matInput formControlName="defaultValue" placeholder="{{'filter.value' | translate}}">
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC"> <ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<input required type="number" matInput formControlName="defaultValue" <input required type="number" matInput formControlName="defaultValue"
placeholder="{{'filter.value' | translate}}"> placeholder="{{'filter.value' | translate}}">
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME"> <ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<tb-datetime fxFlex formControlName="defaultValue" <tb-datetime formControlName="defaultValue"
dateText="filter.date" dateText="filter.date"
timeText="filter.time" timeText="filter.time"
required [showLabel]="false"></tb-datetime> required [showLabel]="false"></tb-datetime>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN"> <ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">
<mat-checkbox fxFlex formControlName="defaultValue"> <mat-checkbox formControlName="defaultValue">
{{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox> </mat-checkbox>
</ng-template> </ng-template>
@ -46,9 +46,9 @@
<div class="tb-hint" translate>filter.default-value</div> <div class="tb-hint" translate>filter.default-value</div>
</div> </div>
<div fxFlex fxLayout="column" [fxShow]="dynamicMode"> <div fxFlex fxLayout="column" [fxShow]="dynamicMode">
<div fxFlex formGroupName="dynamicValue" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div formGroupName="dynamicValue" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}"> <mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="null"> <mat-option [value]="null">
@ -62,7 +62,7 @@
<div class="tb-hint" translate>filter.dynamic-source-type</div> <div class="tb-hint" translate>filter.dynamic-source-type</div>
</div> </div>
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}"> <input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}">
</mat-form-field> </mat-form-field>

View File

@ -27,10 +27,10 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> <fieldset [disabled]="isLoading$ | async" fxLayout="column">
<mat-checkbox fxFlex formControlName="editable" style="margin-bottom: 16px;"> <mat-checkbox formControlName="editable" style="margin-bottom: 16px;">
{{ 'filter.editable' | translate }} {{ 'filter.editable' | translate }}
</mat-checkbox> </mat-checkbox>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.display-label</mat-label> <mat-label translate>filter.display-label</mat-label>
<input matInput formControlName="label"> <input matInput formControlName="label">
@ -39,7 +39,7 @@
{{ 'filter.autogenerated-label' | translate }} {{ 'filter.autogenerated-label' | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<mat-form-field fxFlex class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>filter.order-priority</mat-label> <mat-label translate>filter.order-priority</mat-label>
<input matInput type="number" formControlName="order"> <input matInput type="number" formControlName="order">
</mat-form-field> </mat-form-field>

View File

@ -16,8 +16,8 @@
--> -->
<div fxLayout="column" class="mat-content mat-padding"> <div fxLayout="column" class="mat-content mat-padding">
<div fxLayout="column" *ngFor="let filter of filtersInfo | keyvalue; let last = last"> <div *ngFor="let filter of filtersInfo | keyvalue; let last = last">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutAlign="start center">
<mat-label fxFlex>{{filter.value.filter}}</mat-label> <mat-label fxFlex>{{filter.value.filter}}</mat-label>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
style="min-width: 40px;" style="min-width: 40px;"

View File

@ -41,7 +41,7 @@
<span *ngIf="$index > 0" translate>filter.operation.and</span> <span *ngIf="$index > 0" translate>filter.operation.and</span>
</div> </div>
<div fxLayout="column" fxFlex="92"> <div fxLayout="column" fxFlex="92">
<div fxLayout="row" fxLayoutAlign="start center" fxFlex> <div fxLayout="row" fxLayoutAlign="start center">
<div fxFlex>{{ keyFilterControl.value.key.key }}</div> <div fxFlex>{{ keyFilterControl.value.key.key }}</div>
<div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div> <div fxFlex translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"

View File

@ -29,37 +29,33 @@
</mat-progress-bar> </mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column"> <div formArrayName="userInputs"
<div fxFlex fxLayout="row" fxLayoutAlign="start center" *ngFor="let userInputControl of userInputsFormArray().controls; let $index = index">
formArrayName="userInputs" <div [ngSwitch]="userInputControl.get('valueType').value">
*ngFor="let userInputControl of userInputsFormArray().controls; let $index = index"> <ng-template [ngSwitchCase]="valueTypeEnum.STRING">
<div fxFlex fxLayout="column" <mat-form-field class="mat-block">
[ngSwitch]="userInputControl.get('valueType').value"> <mat-label>{{ userInputControl.get('label').value }}</mat-label>
<ng-template [ngSwitchCase]="valueTypeEnum.STRING"> <input matInput [formControl]="userInputControl.get('value')">
<mat-form-field fxFlex class="mat-block"> </mat-form-field>
<mat-label>{{ userInputControl.get('label').value }}</mat-label> </ng-template>
<input matInput [formControl]="userInputControl.get('value')"> <ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
</mat-form-field> <mat-form-field class="mat-block">
</ng-template> <mat-label>{{ userInputControl.get('label').value }}</mat-label>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC"> <input required type="number" matInput [formControl]="userInputControl.get('value')">
<mat-form-field fxFlex class="mat-block"> </mat-form-field>
<mat-label>{{ userInputControl.get('label').value }}</mat-label> </ng-template>
<input required type="number" matInput [formControl]="userInputControl.get('value')"> <ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
</mat-form-field> <label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label>
</ng-template> <tb-datetime [formControl]="userInputControl.get('value')"
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME"> dateText="filter.date"
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label> timeText="filter.time"
<tb-datetime fxFlex [formControl]="userInputControl.get('value')" required [showLabel]="false"></tb-datetime>
dateText="filter.date" </ng-template>
timeText="filter.time" <ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">
required [showLabel]="false"></tb-datetime> <mat-checkbox labelPosition="before" [formControl]="userInputControl.get('value')">
</ng-template> {{ userInputControl.get('label').value }}
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN"> </mat-checkbox>
<mat-checkbox labelPosition="before" fxFlex [formControl]="userInputControl.get('value')"> </ng-template>
{{ userInputControl.get('label').value }}
</mat-checkbox>
</ng-template>
</div>
</div> </div>
</div> </div>
</fieldset> </fieldset>

View File

@ -51,7 +51,7 @@
<span translate fxLayoutAlign="center center" style="margin: 16px 0" <span translate fxLayoutAlign="center center" style="margin: 16px 0"
class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span> class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span>
</div> </div>
<div fxLayout="row" *ngIf="!disabled"> <div *ngIf="!disabled">
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
type="button" type="button"
(click)="addCreateAlarmRule()" (click)="addCreateAlarmRule()"

View File

@ -83,9 +83,9 @@
<tb-create-alarm-rules formControlName="createRules" style="padding-bottom: 16px;"> <tb-create-alarm-rules formControlName="createRules" style="padding-bottom: 16px;">
</tb-create-alarm-rules> </tb-create-alarm-rules>
<div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div> <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div>
<div fxFlex fxLayout="row" <div fxLayout="row" fxLayoutGap="8px;" fxLayoutAlign="start center"
[fxShow]="alarmFormGroup.get('clearRule').value" [fxShow]="alarmFormGroup.get('clearRule').value"
fxLayoutGap="8px;" fxLayoutAlign="start center" style="padding-bottom: 8px;"> style="padding-bottom: 8px;">
<div class="clear-alarm-rule" fxFlex fxLayout="row"> <div class="clear-alarm-rule" fxFlex fxLayout="row">
<tb-alarm-rule formControlName="clearRule" fxFlex> <tb-alarm-rule formControlName="clearRule" fxFlex>
</tb-alarm-rule> </tb-alarm-rule>
@ -103,8 +103,7 @@
<span translate fxLayoutAlign="center center" style="margin: 16px 0" <span translate fxLayoutAlign="center center" style="margin: 16px 0"
class="tb-prompt">device-profile.no-clear-alarm-rule</span> class="tb-prompt">device-profile.no-clear-alarm-rule</span>
</div> </div>
<div fxLayout="row" *ngIf="!disabled" <div *ngIf="!disabled" [fxShow]="!alarmFormGroup.get('clearRule').value">
[fxShow]="!alarmFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
type="button" type="button"
(click)="addClearAlarmRule()" (click)="addClearAlarmRule()"

View File

@ -30,8 +30,7 @@
<span translate fxLayoutAlign="center center" <span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-alarm-rules</span> class="tb-prompt">device-profile.no-alarm-rules</span>
</div> </div>
<div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center" <div *ngIf="!disabled" style="padding-top: 16px;">
style="padding-top: 16px;">
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="button" type="button"
(click)="addAlarm()" (click)="addAlarm()"

View File

@ -39,7 +39,7 @@
</button> </button>
</div> </div>
</div> </div>
<div class="mat-padding" fxLayout="column"> <div [ngClass]="{'mat-padding': !standalone}" fxLayout="column">
<form [formGroup]="entityForm"> <form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit" style="min-width: 0;"> <fieldset [disabled]="(isLoading$ | async) || !isEdit" style="min-width: 0;">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">

View File

@ -24,6 +24,7 @@ import { guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core'; import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable'; import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
export interface WidgetsData { export interface WidgetsData {
widgets: Array<Widget>; widgets: Array<Widget>;
@ -56,6 +57,7 @@ export interface DashboardCallbacks {
} }
export interface IDashboardComponent { export interface IDashboardComponent {
utils: UtilsService;
gridsterOpts: GridsterConfig; gridsterOpts: GridsterConfig;
gridster: GridsterComponent; gridster: GridsterComponent;
dashboardWidgets: DashboardWidgets; dashboardWidgets: DashboardWidgets;
@ -295,6 +297,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
margin: string; margin: string;
title: string; title: string;
customTranslatedTitle: string;
titleTooltip: string; titleTooltip: string;
showTitle: boolean; showTitle: boolean;
titleStyle: {[klass: string]: any}; titleStyle: {[klass: string]: any};
@ -358,8 +361,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.title = isDefined(this.widgetContext.widgetTitle) this.title = isDefined(this.widgetContext.widgetTitle)
&& this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title; && this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title;
this.customTranslatedTitle = this.dashboard.utils.customTranslation(this.title, this.title);
this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip) this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip)
&& this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip; && this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip;
this.titleTooltip = this.dashboard.utils.customTranslation(this.titleTooltip, this.titleTooltip);
this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true; this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true;
this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {}; this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {};

View File

@ -61,8 +61,8 @@
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> <div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px">
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<div fxLayout="column" fxFlex.sm="60" fxFlex.gt-sm="50"> <div fxLayout="column" fxFlex.sm="60" fxFlex.gt-sm="50">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayout.md="column" fxLayoutGap="8px" fxLayoutGap.md="0px">
<mat-form-field fxFlex="30" fxFlex.xs class="mat-block"> <mat-form-field fxFlex="30" fxFlex.md fxFlex.xs class="mat-block">
<mat-label translate>admin.oauth2.protocol</mat-label> <mat-label translate>admin.oauth2.protocol</mat-label>
<mat-select formControlName="scheme"> <mat-select formControlName="scheme">
<mat-option *ngFor="let protocol of protocols" [value]="protocol"> <mat-option *ngFor="let protocol of protocols" [value]="protocol">

View File

@ -18,7 +18,12 @@
:host { :host {
mat-card.settings-card { mat-card.settings-card {
margin: 8px; margin: 8px;
@media #{$mat-gt-sm} {
@media #{$mat-md} {
width: 80%;
}
@media #{$mat-gt-md} {
width: 60%; width: 60%;
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -21,48 +21,46 @@
{{'rulechain.open-rulechain' | translate }} {{'rulechain.open-rulechain' | translate }}
</button> </button>
</div> </div>
<div class="mat-padding" fxLayout="column"> <form [formGroup]="ruleNodeFormGroup" class="mat-padding">
<form [formGroup]="ruleNodeFormGroup"> <fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly">
<fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly"> <section *ngIf="ruleNode.component.type !== ruleNodeType.RULE_CHAIN">
<section *ngIf="ruleNode.component.type !== ruleNodeType.RULE_CHAIN"> <section fxLayout="column" fxLayout.gt-sm="row">
<section fxLayout="column" fxLayout.gt-sm="row"> <mat-form-field fxFlex class="mat-block">
<mat-form-field fxFlex class="mat-block"> <mat-label translate>rulenode.name</mat-label>
<mat-label translate>rulenode.name</mat-label> <input matInput formControlName="name" required>
<input matInput formControlName="name" required> <mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required') || ruleNodeFormGroup.get('name').hasError('pattern')">
|| ruleNodeFormGroup.get('name').hasError('pattern')"> {{ 'rulenode.name-required' | translate }}
{{ 'rulenode.name-required' | translate }} </mat-error>
</mat-error> </mat-form-field>
</mat-form-field> <mat-checkbox formControlName="debugMode">
<mat-checkbox formControlName="debugMode"> {{ 'rulenode.debug-mode' | translate }}
{{ 'rulenode.debug-mode' | translate }} </mat-checkbox>
</mat-checkbox>
</section>
<tb-rule-node-config #ruleNodeConfigComponent
formControlName="configuration"
[ruleNodeId]="ruleNode.ruleNodeId?.id"
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition">
</tb-rule-node-config>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>rulenode.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</section> </section>
<section *ngIf="ruleNode.component.type === ruleNodeType.RULE_CHAIN"> <tb-rule-node-config #ruleNodeConfigComponent
<tb-entity-autocomplete required formControlName="configuration"
[excludeEntityIds]="[ruleChainId]" [ruleNodeId]="ruleNode.ruleNodeId?.id"
[entityType]="entityType.RULE_CHAIN" [nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition">
formControlName="targetRuleChainId"> </tb-rule-node-config>
</tb-entity-autocomplete> <div formGroupName="additionalInfo" fxLayout="column">
<div formGroupName="additionalInfo" fxLayout="column"> <mat-form-field class="mat-block">
<mat-form-field class="mat-block"> <mat-label translate>rulenode.description</mat-label>
<mat-label translate>rulenode.description</mat-label> <textarea matInput formControlName="description" rows="2"></textarea>
<textarea matInput formControlName="description" rows="2"></textarea> </mat-form-field>
</mat-form-field> </div>
</div> </section>
</section> <section *ngIf="ruleNode.component.type === ruleNodeType.RULE_CHAIN">
</fieldset> <tb-entity-autocomplete required
</form> [excludeEntityIds]="[ruleChainId]"
</div> [entityType]="entityType.RULE_CHAIN"
formControlName="targetRuleChainId">
</tb-entity-autocomplete>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>rulenode.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</section>
</fieldset>
</form>

View File

@ -21,7 +21,7 @@
.tb-login-content { .tb-login-content {
margin-top: 36px; margin-top: 36px;
margin-bottom: 76px; margin-bottom: 76px;
background-color: rgb(250, 250, 250); background-color: #eee;
.tb-login-form { .tb-login-form {
@media #{$mat-gt-xs} { @media #{$mat-gt-xs} {
width: 550px !important; width: 550px !important;

View File

@ -433,7 +433,57 @@
"no-telemetry-text": "No telemetry found" "no-telemetry-text": "No telemetry found"
}, },
"api-usage": { "api-usage": {
"api-usage": "Api Usage" "api-usage": "Api Usage",
"data-points": "Data points",
"data-points-storage-days": "Data points storage days",
"email": "Email",
"email-messages": "Email messages",
"email-messages-daily-activity": "Email messages daily activity",
"email-messages-hourly-activity": "Email messages hourly activity",
"email-messages-monthly-activity": "Email messages monthly activity",
"exceptions": "Exceptions",
"executions": "Executions",
"javascript": "JavaScript",
"javascript-executions": "JavaScript executions",
"javascript-functions": "JavaScript functions",
"javascript-functions-daily-activity": "JavaScript functions daily activity",
"javascript-functions-hourly-activity": "JavaScript functions hourly activity",
"javascript-functions-monthly-activity": "JavaScript functions monthly activity",
"latest-error": "Latest Error",
"messages": "Messages",
"permanent-failures": "${entityName} Permanent Failures",
"permanent-timeouts": "${entityName} Permanent Timeouts",
"processing-failures": "${entityName} Processing Failures",
"processing-failures-and-timeouts": "Processing Failures and Timeouts",
"processing-timeouts": "${entityName} Processing Timeouts",
"queue-stats": "Queue Stats",
"rule-chain": "Rule Chain",
"rule-engine": "Rule Engine",
"rule-engine-daily-activity": "Rule Engine daily activity",
"rule-engine-executions": "Rule Engine executions",
"rule-engine-hourly-activity": "Rule Engine hourly activity",
"rule-engine-monthly-activity": "Rule Engine monthly activity",
"rule-engine-statistics": "Rule Engine Statistics",
"rule-node": "Rule Node",
"sms": "SMS",
"sms-messages": "SMS messages",
"sms-messages-daily-activity": "SMS messages daily activity",
"sms-messages-hourly-activity": "SMS messages hourly activity",
"sms-messages-monthly-activity": "SMS messages monthly activity",
"successful": "${entityName} Successful",
"telemetry": "Telemetry",
"telemetry-persistence": "Telemetry persistence",
"telemetry-persistence-daily-activity": "Telemetry persistence daily activity",
"telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity",
"telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity",
"transport": "Transport",
"transport-daily-activity": "Transport daily activity",
"transport-data-points": "Transport data points",
"transport-hourly-activity": "Transport hourly activity",
"transport-messages": "Transport messages",
"transport-monthly-activity": "Transport monthly activity",
"view-details": "View details",
"view-statistics": "View statistics"
}, },
"audit-log": { "audit-log": {
"audit": "Audit", "audit": "Audit",

View File

@ -16,7 +16,7 @@
--> -->
<!doctype html> <!doctype html>
<html lang="en" style="width: 100%;"> <html lang="en" style="width: 100%; height: 100%;">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>ThingsBoard</title> <title>ThingsBoard</title>
@ -24,8 +24,82 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="thingsboard.ico"> <link rel="icon" type="image/x-icon" href="thingsboard.ico">
<style type="text/css">
body, html {
height: 100%;
overflow: hidden;
background-color: #eee;
}
.tb-loading-spinner {
margin: auto;
z-index: 1;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 136px;
height: 30px;
text-align: center;
}
.tb-loading-spinner > div {
width: 30px;
height: 30px;
margin-right: 10px;
background-color: rgb(43,160,199);
border-radius: 100%;
display: inline-block;
-webkit-animation: tb-bouncedelay 1.4s infinite ease-in-out both;
-moz-animation: tb-bouncedelay 1.4s infinite ease-in-out both;
animation: tb-bouncedelay 1.4s infinite ease-in-out both;
}
.tb-loading-spinner .tb-bounce1 {
-webkit-animation-delay: -0.32s;
-moz-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.tb-loading-spinner .tb-bounce2 {
-webkit-animation-delay: -0.16s;
-moz-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes tb-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@-moz-keyframes tb-bouncedelay {
0%, 80%, 100% { -moz-transform: scale(0) }
40% { -moz-transform: scale(1.0) }
}
@keyframes tb-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
-moz-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1.0);
-moz-transform: scale(1.0);
transform: scale(1.0);
}
}
</style>
</head> </head>
<body class="tb-default"> <body class="tb-default">
<tb-root></tb-root> <tb-root></tb-root>
<div id="tb-loading-spinner" class="tb-loading-spinner">
<div class="tb-bounce1"></div>
<div class="tb-bounce2"></div>
<div class="tb-bounce3"></div>
</div>
</body> </body>
</html> </html>

View File

@ -41,7 +41,7 @@ body, html {
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background-color: rgb(250,250,250); background-color: #eee;
overflow: hidden; overflow: hidden;
} }