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.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -73,6 +74,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@Slf4j
@Service
@ -347,16 +349,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
try {
long now = System.currentTimeMillis();
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();
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
ToUsageStatsServiceMsg.Builder msg = ToUsageStatsServiceMsg.newBuilder();
msg.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
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);
saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
updateTenantState(state, tenantProfileCache.get(tenantId));
}
});
} 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) {
TenantApiUsageState tenantState = myTenantStates.get(tenantId);
if (tenantState == null) {
@ -377,6 +382,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
}
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity);
List<ApiUsageRecordKey> newCounts = new ArrayList<>();
try {
List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get();
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
@ -385,7 +391,13 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
for (TsKvEntry tsKvEntry : dbValues) {
if (tsKvEntry.getKey().equals(key.getApiCountKey())) {
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)) {
hourlyEntryFound = true;
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);
myTenantStates.put(tenantId, tenantState);
saveNewCounts(tenantState, newCounts);
} catch (InterruptedException | ExecutionException 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.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalUnit;
import java.util.concurrent.ConcurrentHashMap;
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.ChronoUnit.MONTHS;
public class SchedulerUtils {
private final static ZoneId UTC = ZoneId.of("UTC");
private static final ConcurrentMap<String, ZoneId> tzMap = new ConcurrentHashMap<>();
public static ZoneId getZoneId(String tz) {
@ -44,7 +41,7 @@ public class SchedulerUtils {
}
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() {
@ -52,7 +49,7 @@ public class SchedulerUtils {
}
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() {
@ -60,7 +57,7 @@ public class SchedulerUtils {
}
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() {
@ -68,7 +65,7 @@ public class SchedulerUtils {
}
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() {

View File

@ -17,4 +17,4 @@
-->
<!--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(
'alpha-e-circle-outline',
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 ' +
'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() {
}
}
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 interface SubscriptionMessage {
severity: SubscriptionMessageSeverity,
severity: SubscriptionMessageSeverity;
message: string;
}

View File

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

View File

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

View File

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

View File

@ -27,10 +27,10 @@
</mat-toolbar>
<div mat-dialog-content>
<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 }}
</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-label translate>filter.display-label</mat-label>
<input matInput formControlName="label">
@ -39,7 +39,7 @@
{{ 'filter.autogenerated-label' | translate }}
</mat-checkbox>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-form-field class="mat-block">
<mat-label translate>filter.order-priority</mat-label>
<input matInput type="number" formControlName="order">
</mat-form-field>

View File

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

View File

@ -41,7 +41,7 @@
<span *ngIf="$index > 0" translate>filter.operation.and</span>
</div>
<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 translate>{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}</div>
<button mat-icon-button color="primary"

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<div [ngClass]="{'mat-padding': !standalone}" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit" style="min-width: 0;">
<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 { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
export interface WidgetsData {
widgets: Array<Widget>;
@ -56,6 +57,7 @@ export interface DashboardCallbacks {
}
export interface IDashboardComponent {
utils: UtilsService;
gridsterOpts: GridsterConfig;
gridster: GridsterComponent;
dashboardWidgets: DashboardWidgets;
@ -295,6 +297,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
margin: string;
title: string;
customTranslatedTitle: string;
titleTooltip: string;
showTitle: boolean;
titleStyle: {[klass: string]: any};
@ -358,8 +361,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.title = isDefined(this.widgetContext.widgetTitle)
&& 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.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.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {};

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -21,48 +21,46 @@
{{'rulechain.open-rulechain' | translate }}
</button>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="ruleNodeFormGroup">
<fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly">
<section *ngIf="ruleNode.component.type !== ruleNodeType.RULE_CHAIN">
<section fxLayout="column" fxLayout.gt-sm="row">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')
|| ruleNodeFormGroup.get('name').hasError('pattern')">
{{ 'rulenode.name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="debugMode">
{{ 'rulenode.debug-mode' | translate }}
</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>
<form [formGroup]="ruleNodeFormGroup" class="mat-padding">
<fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly">
<section *ngIf="ruleNode.component.type !== ruleNodeType.RULE_CHAIN">
<section fxLayout="column" fxLayout.gt-sm="row">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')
|| ruleNodeFormGroup.get('name').hasError('pattern')">
{{ 'rulenode.name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="debugMode">
{{ 'rulenode.debug-mode' | translate }}
</mat-checkbox>
</section>
<section *ngIf="ruleNode.component.type === ruleNodeType.RULE_CHAIN">
<tb-entity-autocomplete required
[excludeEntityIds]="[ruleChainId]"
[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>
</div>
<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 *ngIf="ruleNode.component.type === ruleNodeType.RULE_CHAIN">
<tb-entity-autocomplete required
[excludeEntityIds]="[ruleChainId]"
[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 {
margin-top: 36px;
margin-bottom: 76px;
background-color: rgb(250, 250, 250);
background-color: #eee;
.tb-login-form {
@media #{$mat-gt-xs} {
width: 550px !important;

View File

@ -433,7 +433,57 @@
"no-telemetry-text": "No telemetry found"
},
"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": "Audit",

View File

@ -16,7 +16,7 @@
-->
<!doctype html>
<html lang="en" style="width: 100%;">
<html lang="en" style="width: 100%; height: 100%;">
<head>
<meta charset="utf-8">
<title>ThingsBoard</title>
@ -24,8 +24,82 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<body class="tb-default">
<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>
</html>

View File

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