Browse Source

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

pull/9581/head
Andrii Shvaika 3 years ago
parent
commit
35914fa87f
  1. 12
      ui-ngx/src/app/core/http/entity.service.ts
  2. 9
      ui-ngx/src/app/core/http/widget.service.ts
  3. 3
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss
  4. 8
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  5. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  6. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  7. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  8. 86
      ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html
  9. 19
      ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss
  10. 30
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  11. 4
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss
  12. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html
  13. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.ts
  14. 2
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  15. 113
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  16. 2
      ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss
  17. 49
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  18. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html
  19. 39
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  20. 8
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html
  21. 8
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  22. 87
      ui-ngx/src/app/shared/components/markdown-editor.component.ts
  23. 36
      ui-ngx/src/app/shared/components/tb-error.component.ts
  24. 4
      ui-ngx/src/app/shared/models/alarm.models.ts
  25. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  26. 2
      ui-ngx/src/styles.scss

12
ui-ngx/src/app/core/http/entity.service.ts

@ -41,7 +41,7 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { RuleChainService } from '@core/http/rule-chain.service';
import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models';
import { DataKey, Datasource, DatasourceType, DeprecatedFilter, KeyInfo } from '@app/shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import {
AliasFilterType,
@ -65,7 +65,9 @@ import { Device, DeviceCredentialsType } from '@shared/models/device.models';
import { AttributeService } from '@core/http/attribute.service';
import {
AlarmData,
AlarmDataQuery, AlarmFilter, AlarmFilterConfig,
AlarmDataQuery,
AlarmFilter,
AlarmFilterConfig,
createDefaultEntityDataPageLink,
EntityData,
EntityDataQuery,
@ -418,7 +420,11 @@ export class EntityService {
break;
case EntityType.WIDGETS_BUNDLE:
pageLink.sortOrder.property = 'title';
entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, config);
entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, true, config);
break;
case EntityType.WIDGET_TYPE:
pageLink.sortOrder.property = 'name';
entitiesObservable = this.widgetService.getWidgetTypes(pageLink, true, false, DeprecatedFilter.ALL, null, config);
break;
case EntityType.NOTIFICATION_TARGET:
pageLink.sortOrder.property = 'name';

9
ui-ngx/src/app/core/http/widget.service.ts

@ -85,9 +85,12 @@ export class WidgetService {
);
}
public getWidgetBundles(pageLink: PageLink, fullSearch = false, config?: RequestConfig): Observable<PageData<WidgetsBundle>> {
return this.http.get<PageData<WidgetsBundle>>(`/api/widgetsBundles${pageLink.toQuery()}&fullSearch=${fullSearch}`,
defaultHttpOptionsFromConfig(config));
public getWidgetBundles(pageLink: PageLink, fullSearch = false,
tenantOnly = false, config?: RequestConfig): Observable<PageData<WidgetsBundle>> {
return this.http.get<PageData<WidgetsBundle>>(
`/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`,
defaultHttpOptionsFromConfig(config)
);
}
public getWidgetsBundle(widgetsBundleId: string,

3
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss

@ -40,6 +40,9 @@
tb-entity-subtype-list {
flex: 1;
width: 180px;
.mdc-evolution-chip-set__chips {
width: 100%;
}
}
.mat-mdc-chip {

8
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -319,9 +319,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
if ($event) {
$event.stopPropagation();
}
const unacknowledgedAlarms = alarms.filter(alarm => {
return alarm.status === AlarmStatus.CLEARED_UNACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
const unacknowledgedAlarms = alarms.filter(alarm => !alarm.acknowledged);
let title = '';
let content = '';
if (!unacknowledgedAlarms.length) {
@ -356,9 +354,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
if ($event) {
$event.stopPropagation();
}
const activeAlarms = alarms.filter(alarm => {
return alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
const activeAlarms = alarms.filter(alarm => !alarm.cleared);
let title = '';
let content = '';
if (!activeAlarms.length) {

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}"
<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" tb-toast
fxFlex tb-fullscreen [fullscreenElement]="elRef.nativeElement" [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-dashboard-toolbar"

12
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -125,7 +125,11 @@ div.tb-dashboard-page {
mat-drawer-container.tb-dashboard-layouts {
width: 100%;
&.tb-shrinked {
width: 40%;
width: max(40%, calc(100% - 956px));
@media screen and (min-width: 2000px) {
width: max(40%, calc(100% - 1192px));
}
}
}
@ -135,11 +139,11 @@ div.tb-dashboard-page {
}
@media #{$mat-gt-md} {
width: 75% !important;
width: min(85%, 956px) !important;
}
@media #{$mat-gt-xl} {
width: 60% !important;
@media screen and (min-width: 2000px) {
width: min(60%, 1192px) !important;
}
}
}

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -142,8 +142,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
columns: 2,
breakpoints: {
'screen and (min-width: 2000px)': 5,
'gt-md': 4,
'gt-xs': 3
'screen and (min-width: 1097px)': 4,
'gt-sm': 3,
'screen and (min-width: 721px)': 4,
'screen and (min-width: 485px)': 3
}
};

86
ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html

@ -15,50 +15,52 @@
limitations under the License.
-->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()">
<ng-container *ngFor="let column of showColumns">
<ng-container [ngSwitch]="column.key">
<ng-template [ngSwitchCase]="isSelector(column.key)">
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<mat-select [formControlName]="column.key">
<mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option>
<mat-option *ngFor="let value of selectorValues(column.key)" [value]="value">
{{ value }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="isNumberFields(column.key)">
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key">
<mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')">
{{ 'event.min-value' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="'isError'">
<tb-checkbox formControlName="isError" [falseValue]="''"
(ngModelChange)="changeIsError($event)">
{{ 'event.has-error' | translate }}
</tb-checkbox>
</ng-template>
<ng-template [ngSwitchCase]="'errorStr'">
<mat-form-field fxHide [fxShow]="showErrorMsgFields()">
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" name="errorSearchText" formControlName="errorStr">
</mat-form-field>
</ng-template>
<ng-container *ngSwitchDefault>
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" [name]="column.key" [formControlName]="column.key">
</mat-form-field>
<form fxLayout="column" class="tb-filter" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()">
<div fxLayout="column" class="tb-filter-container mat-padding">
<ng-container *ngFor="let column of showColumns">
<ng-container [ngSwitch]="column.key">
<ng-template [ngSwitchCase]="isSelector(column.key)">
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<mat-select [formControlName]="column.key">
<mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option>
<mat-option *ngFor="let value of selectorValues(column.key)" [value]="value">
{{ value }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="isNumberFields(column.key)">
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key">
<mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')">
{{ 'event.min-value' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="'isError'">
<tb-checkbox formControlName="isError" [falseValue]="''"
(ngModelChange)="changeIsError($event)">
{{ 'event.has-error' | translate }}
</tb-checkbox>
</ng-template>
<ng-template [ngSwitchCase]="'errorStr'">
<mat-form-field fxHide [fxShow]="showErrorMsgFields()">
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" name="errorSearchText" formControlName="errorStr">
</mat-form-field>
</ng-template>
<ng-container *ngSwitchDefault>
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" [name]="column.key" [formControlName]="column.key">
</mat-form-field>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
</div>
<div fxLayout="row" class="tb-panel-actions mat-padding" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">

19
ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss

@ -16,20 +16,11 @@
:host {
width: 100%;
min-width: 300px;
overflow: auto;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 16px;
.tb-filter {
height: 100%;
&-container {
overflow: auto;
}
}
}

30
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -30,7 +30,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EventService } from '@app/core/http/event.service';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EntityTypeResource } from '@shared/models/entity-type.models';
import { Observable } from 'rxjs';
import { fromEvent, Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Direction } from '@shared/models/page/sort-order';
import { DialogService } from '@core/services/dialog.service';
@ -50,6 +50,7 @@ import {
EventFilterPanelData,
FilterEntityColumn
} from '@home/components/event/event-filter-panel.component';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
@ -457,19 +458,16 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
config.maxHeight = '70vh';
config.height = 'min-content';
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
height: 'fit-content',
maxHeight: '65vh'
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions(DEFAULT_OVERLAY_POSITIONS);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
@ -491,7 +489,11 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent,
this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) {
this.filterParams = componentRef.instance.result.filterParams;
this.getTable().paginator.pageIndex = 0;

4
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss

@ -40,12 +40,10 @@
padding: 0 !important;
.mat-stepper-horizontal {
display: flex;
height: 100%;
overflow: hidden;
.mat-horizontal-stepper-wrapper {
flex: 1 1 100%;
max-height: 100%;
}
.mat-horizontal-content-container {

1
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html

@ -40,6 +40,7 @@
keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}"
requiredKeysText="{{ 'widgets.table.alarm-column-error' | translate }}"
hideDataKeyColor
hideUnits
hideDecimals

2
ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.ts

@ -131,7 +131,7 @@ export class GaugeBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.settings.defaultColor = config.defaultColor;
this.widgetConfig.config.settings.colorPlate = config.colorPlate;
this.widgetConfig.config.settings.highlights = config.highlights.rangeList;
this.widgetConfig.config.settings.highlights = config.highlights;
return this.widgetConfig;
}

2
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

@ -243,7 +243,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
}
};
}
if (this.hasAdditionalLatestDataKeys) {
if (this.hasAdditionalLatestDataKeys && !this.basicMode) {
let valid = datasources.filter(datasource => datasource?.dataKeys?.length).length > 0;
if (!valid) {
this.timeseriesKeyError = true;

113
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -503,17 +503,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
height: 'fit-content',
maxHeight: '75vh'
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions(DEFAULT_OVERLAY_POSITIONS);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
@ -549,9 +548,18 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
const componentRef = overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges();
}
@ -888,12 +896,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$event.stopPropagation();
}
if (this.alarmsDatasource.selection.hasValue()) {
const alarmIds = this.alarmsDatasource.selection.selected.filter(
(alarmId) => alarmId !== NULL_UUID
const unacknowledgedAlarms = this.alarmsDatasource.selection.selected.filter(
alarm => alarm.id.id !== NULL_UUID && !alarm.acknowledged
);
if (alarmIds.length) {
const title = this.translate.instant('alarm.aknowledge-alarms-title', {count: alarmIds.length});
const content = this.translate.instant('alarm.aknowledge-alarms-text', {count: alarmIds.length});
let title = '';
let content = '';
if (!unacknowledgedAlarms.length) {
title = this.translate.instant('alarm.selected-alarms', {count: unacknowledgedAlarms.length});
content = this.translate.instant('alarm.selected-alarms-are-acknowledged');
this.dialogService.alert(
title,
content
).subscribe();
} else {
title = this.translate.instant('alarm.aknowledge-alarms-title', {count: unacknowledgedAlarms.length});
content = this.translate.instant('alarm.aknowledge-alarms-text', {count: unacknowledgedAlarms.length});
this.dialogService.confirm(
title,
content,
@ -901,16 +918,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
if (res) {
const tasks: Observable<AlarmInfo>[] = [];
for (const alarmId of alarmIds) {
tasks.push(this.alarmService.ackAlarm(alarmId));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
const tasks: Observable<AlarmInfo>[] = [];
for (const alarm of unacknowledgedAlarms) {
tasks.push(this.alarmService.ackAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
}
});
}
@ -944,12 +959,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$event.stopPropagation();
}
if (this.alarmsDatasource.selection.hasValue()) {
const alarmIds = this.alarmsDatasource.selection.selected.filter(
(alarmId) => alarmId !== NULL_UUID
const activeAlarms = this.alarmsDatasource.selection.selected.filter(
alarm => alarm.id.id !== NULL_UUID && !alarm.cleared
);
if (alarmIds.length) {
const title = this.translate.instant('alarm.clear-alarms-title', {count: alarmIds.length});
const content = this.translate.instant('alarm.clear-alarms-text', {count: alarmIds.length});
let title = '';
let content = '';
if (!activeAlarms.length) {
title = this.translate.instant('alarm.selected-alarms', {count: activeAlarms.length});
content = this.translate.instant('alarm.selected-alarms-are-cleared');
this.dialogService.alert(
title,
content
).subscribe();
} else {
title = this.translate.instant('alarm.clear-alarms-title', {count: activeAlarms.length});
content = this.translate.instant('alarm.clear-alarms-text', {count: activeAlarms.length});
this.dialogService.confirm(
title,
content,
@ -957,16 +981,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
if (res) {
const tasks: Observable<AlarmInfo>[] = [];
for (const alarmId of alarmIds) {
tasks.push(this.alarmService.clearAlarm(alarmId));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
const tasks: Observable<AlarmInfo>[] = [];
for (const alarm of activeAlarms) {
tasks.push(this.alarmService.clearAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
}
});
}
@ -1124,7 +1146,8 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private alarmsSubject = new BehaviorSubject<AlarmDataInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<AlarmDataInfo>>(emptyPageData<AlarmDataInfo>());
public selection = new SelectionModel<string>(true, [], false);
public selection = new SelectionModel<AlarmDataInfo>(true, [], false,
(alarm1: AlarmDataInfo, alarm2: AlarmDataInfo) => alarm1.id.id === alarm2.id.id);
private selectionModeChanged = new EventEmitter<boolean>();
@ -1202,7 +1225,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
}
if (this.selection.hasValue()) {
const alarmIds = alarms.map((alarm) => alarm.id.id);
const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1);
const toRemove = this.selection.selected.filter(alarm => alarmIds.indexOf(alarm.id.id) === -1);
this.selection.deselect(...toRemove);
if (this.selection.isEmpty()) {
isEmptySelection = true;
@ -1276,14 +1299,14 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
toggleSelection(alarm: AlarmDataInfo) {
const hasValue = this.selection.hasValue();
this.selection.toggle(alarm.id.id);
this.selection.toggle(alarm);
if (hasValue !== this.selection.hasValue()) {
this.onSelectionModeChanged(this.selection.hasValue());
}
}
isSelected(alarm: AlarmDataInfo): boolean {
return this.selection.isSelected(alarm.id.id);
return this.selection.isSelected(alarm);
}
clearSelection() {
@ -1304,7 +1327,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
}
} else {
alarms.forEach(row => {
this.selection.select(row.id.id);
this.selection.select(row);
});
if (numSelected === 0) {
this.onSelectionModeChanged(true);

2
ui-ngx/src/app/modules/home/components/widget/lib/display-columns-panel.component.scss

@ -17,7 +17,7 @@
width: 100%;
height: 100%;
min-width: 300px;
overflow: hidden;
overflow: auto;
background: #fff;
border-radius: 4px;
box-shadow:

49
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -46,7 +46,7 @@ import { deepClone, hashCode, isDefined, isNumber, isObject, isUndefined } from
import cssjs from '@core/css/css';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BehaviorSubject, merge, Observable, Subject } from 'rxjs';
import { BehaviorSubject, fromEvent, merge, Observable, Subject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
@ -83,7 +83,7 @@ import {
TableWidgetSettings,
widthStyle
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DISPLAY_COLUMNS_PANEL_DATA,
@ -106,6 +106,7 @@ import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { AggregationType } from '@shared/models/time/time.models';
import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string;
@ -462,18 +463,17 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
height: 'fit-content',
maxHeight: '75vh'
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions(DEFAULT_OVERLAY_POSITIONS);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
@ -481,11 +481,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
});
const columns: DisplayColumn[] = this.columns.map(column => ({
title: column.title,
def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def]
}));
title: column.title,
def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def]
}));
const providers: StaticProvider[] = [
{
@ -506,9 +506,18 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
const componentRef = overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges();
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/analogue-compass-widget-settings.component.html

@ -86,7 +86,7 @@
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic">
<input matInput min="0" formControlName="borderOuterWidth" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput type="number" min="0" formControlName="borderOuterWidth" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
@ -102,7 +102,7 @@
<div translate>widgets.gauge.needle-circle</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic">
<input matInput min="0" formControlName="needleCircleSize" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput type="number" min="0" formControlName="needleCircleSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton

39
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -56,7 +56,7 @@ import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { catchError, debounceTime, distinctUntilChanged, map, skip, startWith, takeUntil } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator';
@ -81,7 +81,7 @@ import {
TableWidgetDataKeySettings,
TableWidgetSettings
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -93,6 +93,7 @@ import {
} from '@home/components/widget/lib/display-columns-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean;
@ -411,23 +412,23 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
$event.stopPropagation();
}
if (this.sources.length) {
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
height: 'fit-content',
maxHeight: '75vh'
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions(DEFAULT_OVERLAY_POSITIONS);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const source = this.sources[this.sourceIndex];
this.prepareDisplayedColumn();
@ -450,8 +451,16 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
const componentRef = overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges();
}
}

8
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html

@ -24,7 +24,7 @@
*ngFor="let entitySubtype of entitySubtypeList"
[removable]="!disabled"
(removed)="remove(entitySubtype)">
{{entitySubtype}}
{{customTranslate(entitySubtype)}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}"
@ -33,6 +33,7 @@
(focusin)="onFocus()"
formControlName="entitySubtype"
matAutocompleteOrigin
matChipInputAddOnBlur
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="entitySubtypeAutocomplete"
@ -47,11 +48,6 @@
<mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype">
<span [innerHTML]="entitySubtype | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntitySubtypeList | async)?.length" [value]="null">
<span>
{{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<div matSuffix>
<ng-content select="[matSuffix]"></ng-content>

8
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts

@ -33,6 +33,7 @@ import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angula
import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { UtilsService } from '@core/services/utils.service';
@Component({
selector: 'tb-entity-subtype-list',
@ -129,6 +130,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
private edgeService: EdgeService,
private entityViewService: EntityViewService,
private alarmService: AlarmService,
private utils: UtilsService,
private fb: FormBuilder) {
this.entitySubtypeListFormGroup = this.fb.group({
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
@ -298,7 +300,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
} else {
result = subTypes.filter(subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true);
}
if (!result.length) {
if (!result.length && searchText.length) {
result = [searchText];
}
return result;
@ -372,4 +374,8 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
}, 0);
}
customTranslate(entity: string) {
return this.utils.customTranslation(entity, entity);
}
}

87
ui-ngx/src/app/shared/components/markdown-editor.component.ts

@ -14,11 +14,23 @@
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getAce } from '@shared/models/ace/ace.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { coerceBoolean } from '@shared/decorators/coercion';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
@Component({
selector: 'tb-markdown-editor',
@ -30,7 +42,8 @@ import { getAce } from '@shared/models/ace/ace.models';
useExisting: forwardRef(() => MarkdownEditorComponent),
multi: true
}
]
],
encapsulation: ViewEncapsulation.None
})
export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, OnDestroy {
@ -42,11 +55,13 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
@Input() helpId: string;
@Input()
@coerceBoolean()
required: boolean;
@ViewChild('markdownEditor', {static: true})
markdownEditorElmRef: ElementRef;
private markdownEditor: Ace.Editor;
editorMode = true;
fullscreen = false;
@ -54,22 +69,15 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
markdownValue: string;
renderValue: string;
ignoreChange = false;
private propagateChange = null;
private requiredValue: boolean;
private markdownEditor: Ace.Editor;
private ignoreChange = false;
get required(): boolean {
return this.requiredValue;
}
private editorResize$: ResizeObserver;
private editorsResizeCaf: CancelAnimationFrame;
private propagateChange: (value: any) => void = () => {};
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
constructor() {
constructor(private cd: ChangeDetectorRef,
private raf: RafService) {
}
ngOnInit(): void {
@ -100,6 +108,10 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
this.updateView();
}
});
this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize();
});
this.editorResize$.observe(editorElement);
}
);
@ -107,6 +119,13 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
}
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
if (this.markdownEditor) {
this.markdownEditor.destroy();
}
@ -134,15 +153,6 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}
}
updateView() {
const editorValue = this.markdownEditor.getValue();
if (this.markdownValue !== editorValue) {
this.markdownValue = editorValue;
this.renderValue = this.markdownValue ? this.markdownValue : ' ';
this.propagateChange(this.markdownValue);
}
}
onFullscreen() {
if (this.markdownEditor) {
setTimeout(() => {
@ -159,4 +169,25 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}, 0);
}
}
private updateView() {
const editorValue = this.markdownEditor.getValue();
if (this.markdownValue !== editorValue) {
this.markdownValue = editorValue;
this.renderValue = this.markdownValue ? this.markdownValue : ' ';
this.propagateChange(this.markdownValue);
this.cd.markForCheck();
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.markdownEditor.resize();
this.markdownEditor.renderer.updateFull();
});
}
}

36
ui-ngx/src/app/shared/components/tb-error.component.ts

@ -14,17 +14,17 @@
/// limitations under the License.
///
import { Component, Input } from '@angular/core';
import { ChangeDetectorRef, Component, Input } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-error',
template: `
<div [@animation]="state" [ngStyle]="{marginTop: noMargin ? '0' : '0.5rem', fontSize: '.75rem'}">
<mat-error >
{{message}}
</mat-error>
<div [@animation]="state" [ngStyle]="{marginTop: noMargin ? '0' : '0.5rem', fontSize: '.75rem'}">
<mat-error>
{{message}}
</mat-error>
</div>
`,
styles: [`
@ -36,21 +36,23 @@ import { coerceBoolean } from '@shared/decorators/coercion';
trigger('animation', [
state('show', style({
opacity: 1,
transform: 'translateY(0)'
})),
state('hide', style({
opacity: 0,
transform: 'translateY(-1rem)'
})),
transition('show => hide', animate('200ms ease-out')),
transition('* => show', animate('200ms ease-in'))
transition('* <=> *', animate('200ms ease-out'))
]),
]
})
export class TbErrorComponent {
errorValue: any;
state: any;
message;
errorValue: string;
state = 'hide';
message: string;
constructor(private cd: ChangeDetectorRef) {
}
@Input()
@coerceBoolean()
@ -58,15 +60,13 @@ export class TbErrorComponent {
@Input()
set error(value) {
if (value && !this.message) {
this.message = value;
this.state = 'hide';
setTimeout(() => {
this.state = 'show';
});
} else {
if (this.errorValue !== value) {
this.errorValue = value;
if (value) {
this.message = value;
}
this.state = value ? 'show' : 'hide';
this.cd.markForCheck();
}
}
}

4
ui-ngx/src/app/shared/models/alarm.models.ts

@ -102,6 +102,8 @@ export interface Alarm extends BaseData<AlarmId> {
originator: EntityId;
severity: AlarmSeverity;
status: AlarmStatus;
acknowledged: boolean;
cleared: boolean;
startTs: number;
endTs: number;
ackTs: number;
@ -181,6 +183,8 @@ export const simulatedAlarm: AlarmInfo = {
type: 'TEMPERATURE',
severity: AlarmSeverity.MAJOR,
status: AlarmStatus.ACTIVE_UNACK,
acknowledged: false,
cleared: false,
details: {
message: 'Temperature is high!'
},

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -6314,6 +6314,7 @@
"pagination": "Pagination",
"rows": "Rows",
"timeseries-column-error": "At least one timeseries column should be specified",
"alarm-column-error": "At least one alarm column should be specified",
"table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode"
},

2
ui-ngx/src/styles.scss

@ -268,7 +268,7 @@ pre.tb-highlight {
letter-spacing: normal;
}
.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel {
.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel, .tb-panel-container {
overflow: hidden;
background: #fff;
border-radius: 4px;

Loading…
Cancel
Save