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 { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models'; 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 { UtilsService } from '@core/services/utils.service';
import { import {
AliasFilterType, AliasFilterType,
@ -65,7 +65,9 @@ import { Device, DeviceCredentialsType } from '@shared/models/device.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { import {
AlarmData, AlarmData,
AlarmDataQuery, AlarmFilter, AlarmFilterConfig, AlarmDataQuery,
AlarmFilter,
AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
EntityData, EntityData,
EntityDataQuery, EntityDataQuery,
@ -418,7 +420,11 @@ export class EntityService {
break; break;
case EntityType.WIDGETS_BUNDLE: case EntityType.WIDGETS_BUNDLE:
pageLink.sortOrder.property = 'title'; 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; break;
case EntityType.NOTIFICATION_TARGET: case EntityType.NOTIFICATION_TARGET:
pageLink.sortOrder.property = 'name'; 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>> { public getWidgetBundles(pageLink: PageLink, fullSearch = false,
return this.http.get<PageData<WidgetsBundle>>(`/api/widgetsBundles${pageLink.toQuery()}&fullSearch=${fullSearch}`, tenantOnly = false, config?: RequestConfig): Observable<PageData<WidgetsBundle>> {
defaultHttpOptionsFromConfig(config)); return this.http.get<PageData<WidgetsBundle>>(
`/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`,
defaultHttpOptionsFromConfig(config)
);
} }
public getWidgetsBundle(widgetsBundleId: string, 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 { tb-entity-subtype-list {
flex: 1; flex: 1;
width: 180px; width: 180px;
.mdc-evolution-chip-set__chips {
width: 100%;
}
} }
.mat-mdc-chip { .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) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const unacknowledgedAlarms = alarms.filter(alarm => { const unacknowledgedAlarms = alarms.filter(alarm => !alarm.acknowledged);
return alarm.status === AlarmStatus.CLEARED_UNACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
let title = ''; let title = '';
let content = ''; let content = '';
if (!unacknowledgedAlarms.length) { if (!unacknowledgedAlarms.length) {
@ -356,9 +354,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const activeAlarms = alarms.filter(alarm => { const activeAlarms = alarms.filter(alarm => !alarm.cleared);
return alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
let title = ''; let title = '';
let content = ''; let content = '';
if (!activeAlarms.length) { 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. 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"> fxFlex tb-fullscreen [fullscreenElement]="elRef.nativeElement" [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-dashboard-toolbar" <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 { mat-drawer-container.tb-dashboard-layouts {
width: 100%; width: 100%;
&.tb-shrinked { &.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} { @media #{$mat-gt-md} {
width: 75% !important; width: min(85%, 956px) !important;
} }
@media #{$mat-gt-xl} { @media screen and (min-width: 2000px) {
width: 60% !important; 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, columns: 2,
breakpoints: { breakpoints: {
'screen and (min-width: 2000px)': 5, 'screen and (min-width: 2000px)': 5,
'gt-md': 4, 'screen and (min-width: 1097px)': 4,
'gt-xs': 3 '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. limitations under the License.
--> -->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()"> <form fxLayout="column" class="tb-filter" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()">
<ng-container *ngFor="let column of showColumns"> <div fxLayout="column" class="tb-filter-container mat-padding">
<ng-container [ngSwitch]="column.key"> <ng-container *ngFor="let column of showColumns">
<ng-template [ngSwitchCase]="isSelector(column.key)"> <ng-container [ngSwitch]="column.key">
<mat-form-field> <ng-template [ngSwitchCase]="isSelector(column.key)">
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field>
<mat-select [formControlName]="column.key"> <mat-label>{{ column.title | translate}}</mat-label>
<mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option> <mat-select [formControlName]="column.key">
<mat-option *ngFor="let value of selectorValues(column.key)" [value]="value"> <mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option>
{{ value }} <mat-option *ngFor="let value of selectorValues(column.key)" [value]="value">
</mat-option> {{ value }}
</mat-select> </mat-option>
</mat-form-field> </mat-select>
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="isNumberFields(column.key)"> </ng-template>
<mat-form-field> <ng-template [ngSwitchCase]="isNumberFields(column.key)">
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field>
<input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key"> <mat-label>{{ column.title | translate}}</mat-label>
<mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')"> <input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key">
{{ 'event.min-value' | translate }} <mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')">
</mat-error> {{ 'event.min-value' | translate }}
</mat-form-field> </mat-error>
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="'isError'"> </ng-template>
<tb-checkbox formControlName="isError" [falseValue]="''" <ng-template [ngSwitchCase]="'isError'">
(ngModelChange)="changeIsError($event)"> <tb-checkbox formControlName="isError" [falseValue]="''"
{{ 'event.has-error' | translate }} (ngModelChange)="changeIsError($event)">
</tb-checkbox> {{ 'event.has-error' | translate }}
</ng-template> </tb-checkbox>
<ng-template [ngSwitchCase]="'errorStr'"> </ng-template>
<mat-form-field fxHide [fxShow]="showErrorMsgFields()"> <ng-template [ngSwitchCase]="'errorStr'">
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field fxHide [fxShow]="showErrorMsgFields()">
<input matInput type="text" name="errorSearchText" formControlName="errorStr"> <mat-label>{{ column.title | translate}}</mat-label>
</mat-form-field> <input matInput type="text" name="errorSearchText" formControlName="errorStr">
</ng-template> </mat-form-field>
<ng-container *ngSwitchDefault> </ng-template>
<mat-form-field> <ng-container *ngSwitchDefault>
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field>
<input matInput type="text" [name]="column.key" [formControlName]="column.key"> <mat-label>{{ column.title | translate}}</mat-label>
</mat-form-field> <input matInput type="text" [name]="column.key" [formControlName]="column.key">
</mat-form-field>
</ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </div>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> <div fxLayout="row" class="tb-panel-actions mat-padding" fxLayoutAlign="end center">
<button type="button" <button type="button"
mat-button mat-button
(click)="cancel()"> (click)="cancel()">

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

@ -16,20 +16,11 @@
:host { :host {
width: 100%; width: 100%;
min-width: 300px; 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 { .tb-filter {
overflow: hidden; height: 100%;
background-color: #fff; &-container {
} overflow: auto;
}
.mat-padding {
padding: 16px;
} }
} }

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 { EventService } from '@app/core/http/event.service';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EntityTypeResource } from '@shared/models/entity-type.models'; 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 { PageData } from '@shared/models/page/page-data';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
@ -50,6 +50,7 @@ import {
EventFilterPanelData, EventFilterPanelData,
FilterEntityColumn FilterEntityColumn
} from '@home/components/event/event-filter-panel.component'; } from '@home/components/event/event-filter-panel.component';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> { export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
@ -457,19 +458,16 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig({
config.backdropClass = 'cdk-overlay-transparent-backdrop'; panelClass: 'tb-panel-container',
config.hasBackdrop = true; backdropClass: 'cdk-overlay-transparent-backdrop',
const connectedPosition: ConnectedPosition = { hasBackdrop: true,
originX: 'end', height: 'fit-content',
originY: 'bottom', maxHeight: '65vh'
overlayX: 'end', });
overlayY: 'top' config.positionStrategy = this.overlay.position()
}; .flexibleConnectedTo(target as HTMLElement)
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions(DEFAULT_OVERLAY_POSITIONS);
.withPositions([connectedPosition]);
config.maxHeight = '70vh';
config.height = 'min-content';
const overlayRef = this.overlay.create(config); const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => { overlayRef.backdropClick().subscribe(() => {
@ -491,7 +489,11 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent, const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent,
this.viewContainerRef, injector)); this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => { componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) { if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) {
this.filterParams = componentRef.instance.result.filterParams; this.filterParams = componentRef.instance.result.filterParams;
this.getTable().paginator.pageIndex = 0; 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; padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex;
height: 100%;
overflow: hidden; overflow: hidden;
.mat-horizontal-stepper-wrapper { .mat-horizontal-stepper-wrapper {
flex: 1 1 100%; max-height: 100%;
} }
.mat-horizontal-content-container { .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 }}" keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}" removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}" noKeysText="{{ 'widgets.table.no-columns' | translate }}"
requiredKeysText="{{ 'widgets.table.alarm-column-error' | translate }}"
hideDataKeyColor hideDataKeyColor
hideUnits hideUnits
hideDecimals 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.defaultColor = config.defaultColor;
this.widgetConfig.config.settings.colorPlate = config.colorPlate; 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; 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; let valid = datasources.filter(datasource => datasource?.dataKeys?.length).length > 0;
if (!valid) { if (!valid) {
this.timeseriesKeyError = true; 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(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig({
config.backdropClass = 'cdk-overlay-transparent-backdrop'; panelClass: 'tb-panel-container',
config.hasBackdrop = true; backdropClass: 'cdk-overlay-transparent-backdrop',
const connectedPosition: ConnectedPosition = { hasBackdrop: true,
originX: 'end', height: 'fit-content',
originY: 'bottom', maxHeight: '75vh'
overlayX: 'end', });
overlayY: 'top' config.positionStrategy = this.overlay.position()
}; .flexibleConnectedTo(target as HTMLElement)
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions(DEFAULT_OVERLAY_POSITIONS);
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config); const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => { overlayRef.backdropClick().subscribe(() => {
@ -549,9 +548,18 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
useValue: overlayRef useValue: overlayRef
} }
]; ];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, const componentRef = overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector)); this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges(); this.ctx.detectChanges();
} }
@ -888,12 +896,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.alarmsDatasource.selection.hasValue()) { if (this.alarmsDatasource.selection.hasValue()) {
const alarmIds = this.alarmsDatasource.selection.selected.filter( const unacknowledgedAlarms = this.alarmsDatasource.selection.selected.filter(
(alarmId) => alarmId !== NULL_UUID alarm => alarm.id.id !== NULL_UUID && !alarm.acknowledged
); );
if (alarmIds.length) { let title = '';
const title = this.translate.instant('alarm.aknowledge-alarms-title', {count: alarmIds.length}); let content = '';
const content = this.translate.instant('alarm.aknowledge-alarms-text', {count: alarmIds.length}); 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( this.dialogService.confirm(
title, title,
content, content,
@ -901,16 +918,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.translate.instant('action.yes') this.translate.instant('action.yes')
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (res) { const tasks: Observable<AlarmInfo>[] = [];
const tasks: Observable<AlarmInfo>[] = []; for (const alarm of unacknowledgedAlarms) {
for (const alarmId of alarmIds) { tasks.push(this.alarmService.ackAlarm(alarm.id.id));
tasks.push(this.alarmService.ackAlarm(alarmId));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
} }
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
} }
}); });
} }
@ -944,12 +959,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.alarmsDatasource.selection.hasValue()) { if (this.alarmsDatasource.selection.hasValue()) {
const alarmIds = this.alarmsDatasource.selection.selected.filter( const activeAlarms = this.alarmsDatasource.selection.selected.filter(
(alarmId) => alarmId !== NULL_UUID alarm => alarm.id.id !== NULL_UUID && !alarm.cleared
); );
if (alarmIds.length) { let title = '';
const title = this.translate.instant('alarm.clear-alarms-title', {count: alarmIds.length}); let content = '';
const content = this.translate.instant('alarm.clear-alarms-text', {count: alarmIds.length}); 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( this.dialogService.confirm(
title, title,
content, content,
@ -957,16 +981,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.translate.instant('action.yes') this.translate.instant('action.yes')
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (res) { const tasks: Observable<AlarmInfo>[] = [];
const tasks: Observable<AlarmInfo>[] = []; for (const alarm of activeAlarms) {
for (const alarmId of alarmIds) { tasks.push(this.alarmService.clearAlarm(alarm.id.id));
tasks.push(this.alarmService.clearAlarm(alarmId));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
} }
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
} }
}); });
} }
@ -1124,7 +1146,8 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private alarmsSubject = new BehaviorSubject<AlarmDataInfo[]>([]); private alarmsSubject = new BehaviorSubject<AlarmDataInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<AlarmDataInfo>>(emptyPageData<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>(); private selectionModeChanged = new EventEmitter<boolean>();
@ -1202,7 +1225,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
} }
if (this.selection.hasValue()) { if (this.selection.hasValue()) {
const alarmIds = alarms.map((alarm) => alarm.id.id); 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); this.selection.deselect(...toRemove);
if (this.selection.isEmpty()) { if (this.selection.isEmpty()) {
isEmptySelection = true; isEmptySelection = true;
@ -1276,14 +1299,14 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
toggleSelection(alarm: AlarmDataInfo) { toggleSelection(alarm: AlarmDataInfo) {
const hasValue = this.selection.hasValue(); const hasValue = this.selection.hasValue();
this.selection.toggle(alarm.id.id); this.selection.toggle(alarm);
if (hasValue !== this.selection.hasValue()) { if (hasValue !== this.selection.hasValue()) {
this.onSelectionModeChanged(this.selection.hasValue()); this.onSelectionModeChanged(this.selection.hasValue());
} }
} }
isSelected(alarm: AlarmDataInfo): boolean { isSelected(alarm: AlarmDataInfo): boolean {
return this.selection.isSelected(alarm.id.id); return this.selection.isSelected(alarm);
} }
clearSelection() { clearSelection() {
@ -1304,7 +1327,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
} }
} else { } else {
alarms.forEach(row => { alarms.forEach(row => {
this.selection.select(row.id.id); this.selection.select(row);
}); });
if (numSelected === 0) { if (numSelected === 0) {
this.onSelectionModeChanged(true); this.onSelectionModeChanged(true);

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

@ -17,7 +17,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
min-width: 300px; min-width: 300px;
overflow: hidden; overflow: auto;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
box-shadow: 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 cssjs from '@core/css/css';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 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 { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { entityTypeTranslations } from '@shared/models/entity-type.models';
@ -83,7 +83,7 @@ import {
TableWidgetSettings, TableWidgetSettings,
widthStyle widthStyle
} from '@home/components/widget/lib/table-widget.models'; } 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 { ComponentPortal } from '@angular/cdk/portal';
import { import {
DISPLAY_COLUMNS_PANEL_DATA, DISPLAY_COLUMNS_PANEL_DATA,
@ -106,6 +106,7 @@ import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { AggregationType } from '@shared/models/time/time.models'; import { AggregationType } from '@shared/models/time/time.models';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
interface EntitiesTableWidgetSettings extends TableWidgetSettings { interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string; entitiesTitle: string;
@ -462,18 +463,17 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.currentTarget; const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig({
config.backdropClass = 'cdk-overlay-transparent-backdrop'; panelClass: 'tb-panel-container',
config.hasBackdrop = true; backdropClass: 'cdk-overlay-transparent-backdrop',
const connectedPosition: ConnectedPosition = { hasBackdrop: true,
originX: 'end', height: 'fit-content',
originY: 'bottom', maxHeight: '75vh'
overlayX: 'end', });
overlayY: 'top' config.positionStrategy = this.overlay.position()
}; .flexibleConnectedTo(target as HTMLElement)
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions(DEFAULT_OVERLAY_POSITIONS);
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config); const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => { overlayRef.backdropClick().subscribe(() => {
@ -481,11 +481,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}); });
const columns: DisplayColumn[] = this.columns.map(column => ({ const columns: DisplayColumn[] = this.columns.map(column => ({
title: column.title, title: column.title,
def: column.def, def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1, display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def] selectable: this.columnSelectionAvailability[column.def]
})); }));
const providers: StaticProvider[] = [ const providers: StaticProvider[] = [
{ {
@ -506,9 +506,18 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
useValue: overlayRef useValue: overlayRef
} }
]; ];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, const componentRef = overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector)); this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges(); 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> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic"> <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> </mat-form-field>
<tb-color-input asBoxInput <tb-color-input asBoxInput
colorClearButton colorClearButton
@ -102,7 +102,7 @@
<div translate>widgets.gauge.needle-circle</div> <div translate>widgets.gauge.needle-circle</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic"> <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> </mat-form-field>
<tb-color-input asBoxInput <tb-color-input asBoxInput
colorClearButton 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 { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; 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 { emptyPageData, PageData } from '@shared/models/page/page-data';
import { catchError, debounceTime, distinctUntilChanged, map, skip, startWith, takeUntil } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, map, skip, startWith, takeUntil } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
@ -81,7 +81,7 @@ import {
TableWidgetDataKeySettings, TableWidgetDataKeySettings,
TableWidgetSettings TableWidgetSettings
} from '@home/components/widget/lib/table-widget.models'; } 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 { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -93,6 +93,7 @@ import {
} from '@home/components/widget/lib/display-columns-panel.component'; } from '@home/components/widget/lib/display-columns-panel.component';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean; showTimestamp: boolean;
@ -411,23 +412,23 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.sources.length) { if (this.sources.length) {
const target = $event.target || $event.currentTarget; const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig({
config.backdropClass = 'cdk-overlay-transparent-backdrop'; panelClass: 'tb-panel-container',
config.hasBackdrop = true; backdropClass: 'cdk-overlay-transparent-backdrop',
const connectedPosition: ConnectedPosition = { hasBackdrop: true,
originX: 'end', height: 'fit-content',
originY: 'bottom', maxHeight: '75vh'
overlayX: 'end', });
overlayY: 'top' config.positionStrategy = this.overlay.position()
}; .flexibleConnectedTo(target as HTMLElement)
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions(DEFAULT_OVERLAY_POSITIONS);
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config); const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => { overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose(); overlayRef.dispose();
}); });
const source = this.sources[this.sourceIndex]; const source = this.sources[this.sourceIndex];
this.prepareDisplayedColumn(); this.prepareDisplayedColumn();
@ -450,8 +451,16 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
]; ];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, const componentRef = overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector)); this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges(); 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" *ngFor="let entitySubtype of entitySubtypeList"
[removable]="!disabled" [removable]="!disabled"
(removed)="remove(entitySubtype)"> (removed)="remove(entitySubtype)">
{{entitySubtype}} {{customTranslate(entitySubtype)}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row> </mat-chip-row>
<input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}" <input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}"
@ -33,6 +33,7 @@
(focusin)="onFocus()" (focusin)="onFocus()"
formControlName="entitySubtype" formControlName="entitySubtype"
matAutocompleteOrigin matAutocompleteOrigin
matChipInputAddOnBlur
#origin="matAutocompleteOrigin" #origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin" [matAutocompleteConnectedTo]="origin"
[matAutocomplete]="entitySubtypeAutocomplete" [matAutocomplete]="entitySubtypeAutocomplete"
@ -47,11 +48,6 @@
<mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype"> <mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype">
<span [innerHTML]="entitySubtype | highlight:searchText"></span> <span [innerHTML]="entitySubtype | highlight:searchText"></span>
</mat-option> </mat-option>
<mat-option *ngIf="!(filteredEntitySubtypeList | async)?.length" [value]="null">
<span>
{{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete> </mat-autocomplete>
<div matSuffix> <div matSuffix>
<ng-content select="[matSuffix]"></ng-content> <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 { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { UtilsService } from '@core/services/utils.service';
@Component({ @Component({
selector: 'tb-entity-subtype-list', selector: 'tb-entity-subtype-list',
@ -129,6 +130,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
private edgeService: EdgeService, private edgeService: EdgeService,
private entityViewService: EntityViewService, private entityViewService: EntityViewService,
private alarmService: AlarmService, private alarmService: AlarmService,
private utils: UtilsService,
private fb: FormBuilder) { private fb: FormBuilder) {
this.entitySubtypeListFormGroup = this.fb.group({ this.entitySubtypeListFormGroup = this.fb.group({
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []],
@ -298,7 +300,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
} else { } else {
result = subTypes.filter(subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true); result = subTypes.filter(subType => searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true);
} }
if (!result.length) { if (!result.length && searchText.length) {
result = [searchText]; result = [searchText];
} }
return result; return result;
@ -372,4 +374,8 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
}, 0); }, 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. /// 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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { getAce } from '@shared/models/ace/ace.models'; 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({ @Component({
selector: 'tb-markdown-editor', selector: 'tb-markdown-editor',
@ -30,7 +42,8 @@ import { getAce } from '@shared/models/ace/ace.models';
useExisting: forwardRef(() => MarkdownEditorComponent), useExisting: forwardRef(() => MarkdownEditorComponent),
multi: true multi: true
} }
] ],
encapsulation: ViewEncapsulation.None
}) })
export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, OnDestroy { export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, OnDestroy {
@ -42,11 +55,13 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
@Input() helpId: string; @Input() helpId: string;
@Input()
@coerceBoolean()
required: boolean;
@ViewChild('markdownEditor', {static: true}) @ViewChild('markdownEditor', {static: true})
markdownEditorElmRef: ElementRef; markdownEditorElmRef: ElementRef;
private markdownEditor: Ace.Editor;
editorMode = true; editorMode = true;
fullscreen = false; fullscreen = false;
@ -54,22 +69,15 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
markdownValue: string; markdownValue: string;
renderValue: string; renderValue: string;
ignoreChange = false; private markdownEditor: Ace.Editor;
private ignoreChange = false;
private propagateChange = null;
private requiredValue: boolean;
get required(): boolean { private editorResize$: ResizeObserver;
return this.requiredValue; private editorsResizeCaf: CancelAnimationFrame;
} private propagateChange: (value: any) => void = () => {};
@Input() constructor(private cd: ChangeDetectorRef,
set required(value: boolean) { private raf: RafService) {
this.requiredValue = coerceBooleanProperty(value);
}
constructor() {
} }
ngOnInit(): void { ngOnInit(): void {
@ -100,6 +108,10 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
this.updateView(); 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 { ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
}
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
if (this.markdownEditor) { if (this.markdownEditor) {
this.markdownEditor.destroy(); 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() { onFullscreen() {
if (this.markdownEditor) { if (this.markdownEditor) {
setTimeout(() => { setTimeout(() => {
@ -159,4 +169,25 @@ export class MarkdownEditorComponent implements OnInit, ControlValueAccessor, On
}, 0); }, 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. /// 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 { animate, state, style, transition, trigger } from '@angular/animations';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-error', selector: 'tb-error',
template: ` template: `
<div [@animation]="state" [ngStyle]="{marginTop: noMargin ? '0' : '0.5rem', fontSize: '.75rem'}"> <div [@animation]="state" [ngStyle]="{marginTop: noMargin ? '0' : '0.5rem', fontSize: '.75rem'}">
<mat-error > <mat-error>
{{message}} {{message}}
</mat-error> </mat-error>
</div> </div>
`, `,
styles: [` styles: [`
@ -36,21 +36,23 @@ import { coerceBoolean } from '@shared/decorators/coercion';
trigger('animation', [ trigger('animation', [
state('show', style({ state('show', style({
opacity: 1, opacity: 1,
transform: 'translateY(0)'
})), })),
state('hide', style({ state('hide', style({
opacity: 0, opacity: 0,
transform: 'translateY(-1rem)' transform: 'translateY(-1rem)'
})), })),
transition('show => hide', animate('200ms ease-out')), transition('* <=> *', animate('200ms ease-out'))
transition('* => show', animate('200ms ease-in'))
]), ]),
] ]
}) })
export class TbErrorComponent { export class TbErrorComponent {
errorValue: any; errorValue: string;
state: any; state = 'hide';
message; message: string;
constructor(private cd: ChangeDetectorRef) {
}
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -58,15 +60,13 @@ export class TbErrorComponent {
@Input() @Input()
set error(value) { set error(value) {
if (value && !this.message) { if (this.errorValue !== value) {
this.message = value;
this.state = 'hide';
setTimeout(() => {
this.state = 'show';
});
} else {
this.errorValue = value; this.errorValue = value;
if (value) {
this.message = value;
}
this.state = value ? 'show' : 'hide'; 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; originator: EntityId;
severity: AlarmSeverity; severity: AlarmSeverity;
status: AlarmStatus; status: AlarmStatus;
acknowledged: boolean;
cleared: boolean;
startTs: number; startTs: number;
endTs: number; endTs: number;
ackTs: number; ackTs: number;
@ -181,6 +183,8 @@ export const simulatedAlarm: AlarmInfo = {
type: 'TEMPERATURE', type: 'TEMPERATURE',
severity: AlarmSeverity.MAJOR, severity: AlarmSeverity.MAJOR,
status: AlarmStatus.ACTIVE_UNACK, status: AlarmStatus.ACTIVE_UNACK,
acknowledged: false,
cleared: false,
details: { details: {
message: 'Temperature is high!' message: 'Temperature is high!'
}, },

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

@ -6314,6 +6314,7 @@
"pagination": "Pagination", "pagination": "Pagination",
"rows": "Rows", "rows": "Rows",
"timeseries-column-error": "At least one timeseries column should be specified", "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", "table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" "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; 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; overflow: hidden;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;

Loading…
Cancel
Save