Browse Source

Implement Alarm widget

pull/2411/head
Igor Kulikov 6 years ago
parent
commit
bd8af1111e
  1. 2
      ui-ngx/src/app/core/api/widget-api.models.ts
  2. 8
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  3. 38
      ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
  4. 25
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.html
  5. 36
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.scss
  6. 42
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.ts
  7. 171
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  8. 3
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  9. 25
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  10. 11
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  11. 16
      ui-ngx/src/app/shared/models/page/page-link.ts
  12. 5
      ui-ngx/src/styles.scss

2
ui-ngx/src/app/core/api/widget-api.models.ts

@ -259,5 +259,7 @@ export interface IWidgetSubscription {
destroy(): void;
update(): void;
[key: string]: any;
}

8
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -30,7 +30,6 @@ import { RelationTableComponent } from '@home/components/relation/relation-table
import { RelationDialogComponent } from './relation/relation-dialog.component';
import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component';
import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
@ -64,6 +63,7 @@ import { AddWidgetToDashboardDialogComponent } from './attribute/add-widget-to-d
import { ImportDialogCsvComponent } from './import-export/import-dialog-csv.component';
import { TableColumnsAssignmentComponent } from './import-export/table-columns-assignment.component';
import { EventContentDialogComponent } from '@home/components/event/event-content-dialog.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@NgModule({
entryComponents: [
@ -73,7 +73,6 @@ import { EventContentDialogComponent } from '@home/components/event/event-conten
EventTableHeaderComponent,
RelationDialogComponent,
AlarmTableHeaderComponent,
AlarmDetailsDialogComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
AliasesEntitySelectPanelComponent,
@ -104,7 +103,6 @@ import { EventContentDialogComponent } from '@home/components/event/event-conten
RelationFiltersComponent,
AlarmTableHeaderComponent,
AlarmTableComponent,
AlarmDetailsDialogComponent,
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
@ -136,7 +134,8 @@ import { EventContentDialogComponent } from '@home/components/event/event-conten
],
imports: [
CommonModule,
SharedModule
SharedModule,
SharedHomeComponentsModule
],
exports: [
EntitiesTableComponent,
@ -149,7 +148,6 @@ import { EventContentDialogComponent } from '@home/components/event/event-conten
RelationTableComponent,
RelationFiltersComponent,
AlarmTableComponent,
AlarmDetailsDialogComponent,
AttributeTableComponent,
AliasesEntitySelectComponent,
EntityAliasesDialogComponent,

38
ui-ngx/src/app/modules/home/components/shared-home-components.module.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
@NgModule({
entryComponents: [
AlarmDetailsDialogComponent
],
declarations:
[
AlarmDetailsDialogComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
AlarmDetailsDialogComponent
]
})
export class SharedHomeComponentsModule { }

25
ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.html

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="column" class="mat-content mat-padding">
<label class="tb-title" translate>alarm.alarm-status-filter</label>
<mat-radio-group [(ngModel)]="subscription.alarmSearchStatus" fxLayout="column" fxLayoutGap="16px">
<mat-radio-button *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus" color="primary">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-radio-button>
</mat-radio-group>
</div>

36
ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
min-width: 300px;
overflow: hidden;
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;
}
}

42
ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken } from '@angular/core';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models';
export const ALARM_STATUS_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmStatusFilterPanelData');
export interface AlarmStatusFilterPanelData {
subscription: IWidgetSubscription;
}
@Component({
selector: 'tb-alarm-status-filter-panel',
templateUrl: './alarm-status-filter-panel.component.html',
styleUrls: ['./alarm-status-filter-panel.component.scss']
})
export class AlarmStatusFilterPanelComponent {
subscription: IWidgetSubscription;
alarmSearchStatuses = Object.keys(AlarmSearchStatus);
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
constructor(@Inject(ALARM_STATUS_FILTER_PANEL_DATA) public data: AlarmStatusFilterPanelData) {
this.subscription = this.data.subscription;
}
}

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

@ -39,7 +39,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { DataSource } from '@angular/cdk/typings/collections';
import { CollectionViewer, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { catchError, debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators';
@ -77,6 +77,19 @@ import {
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common';
import {
ALARM_STATUS_FILTER_PANEL_DATA,
AlarmStatusFilterPanelComponent,
AlarmStatusFilterPanelData
} from '@home/components/widget/lib/alarm-status-filter-panel.component';
import {
AlarmDetailsDialogComponent,
AlarmDetailsDialogData
} from '@home/components/alarm/alarm-details-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DialogService } from '@core/services/dialog.service';
import { AlarmService } from '@core/http/alarm.service';
interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string;
@ -172,7 +185,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private utils: UtilsService,
public translate: TranslateService,
private domSanitizer: DomSanitizer,
private datePipe: DatePipe) {
private datePipe: DatePipe,
private dialog: MatDialog,
private dialogService: DialogService,
private alarmService: AlarmService) {
super(store);
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
@ -390,7 +406,36 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
private editAlarmStatusFilter($event: Event) {
// TODO:
if ($event) {
$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 overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injectionTokens = new WeakMap<any, any>([
[ALARM_STATUS_FILTER_PANEL_DATA, {
subscription: this.subscription,
} as AlarmStatusFilterPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
overlayRef.attach(new ComponentPortal(AlarmStatusFilterPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();
}
private enterFilterMode() {
@ -538,35 +583,138 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if ($event) {
$event.stopPropagation();
}
// TODO:
if (alarm && alarm.id && alarm.id.id !== NULL_UUID) {
this.dialog.open<AlarmDetailsDialogComponent, AlarmDetailsDialogData, boolean>
(AlarmDetailsDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmId: alarm.id.id,
allowAcknowledgment: this.allowAcknowledgment,
allowClear: this.allowClear,
displayDetails: true
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.subscription.update();
}
}
);
}
}
private ackAlarm($event: Event, alarm: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
if (alarm && alarm.id && alarm.id.id !== NULL_UUID) {
this.dialogService.confirm(
this.translate.instant('alarm.aknowledge-alarm-title'),
this.translate.instant('alarm.aknowledge-alarm-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
if (res) {
this.alarmService.ackAlarm(alarm.id.id).subscribe(() => {
this.subscription.update();
});
}
}
});
}
}
public ackAlarms($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
if (this.alarmsDatasource.selection.hasValue()) {
const alarms = this.alarmsDatasource.selection.selected.filter(
(alarm) => { return alarm.id.id !== NULL_UUID }
);
if (alarms.length) {
const title = this.translate.instant('alarm.aknowledge-alarms-title', {count: alarms.length});
const content = this.translate.instant('alarm.aknowledge-alarms-text', {count: alarms.length});
this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
if (res) {
const tasks: Observable<void>[] = [];
for (const alarm of alarms) {
tasks.push(this.alarmService.ackAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
}
}
});
}
}
}
private clearAlarm($event: Event, alarm: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
if (alarm && alarm.id && alarm.id.id !== NULL_UUID) {
this.dialogService.confirm(
this.translate.instant('alarm.clear-alarm-title'),
this.translate.instant('alarm.clear-alarm-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
if (res) {
this.alarmService.clearAlarm(alarm.id.id).subscribe(() => {
this.subscription.update();
});
}
}
});
}
}
public clearAlarms($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
if (this.alarmsDatasource.selection.hasValue()) {
const alarms = this.alarmsDatasource.selection.selected.filter(
(alarm) => { return alarm.id.id !== NULL_UUID }
);
if (alarms.length) {
const title = this.translate.instant('alarm.clear-alarms-title', {count: alarms.length});
const content = this.translate.instant('alarm.clear-alarms-text', {count: alarms.length});
this.dialogService.confirm(
title,
content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')
).subscribe((res) => {
if (res) {
if (res) {
const tasks: Observable<void>[] = [];
for (const alarm of alarms) {
tasks.push(this.alarmService.clearAlarm(alarm.id.id));
}
forkJoin(tasks).subscribe(() => {
this.alarmsDatasource.clearSelection();
this.subscription.update();
});
}
}
});
}
}
}
private defaultContent(key: EntityColumn, value: any): any {
@ -722,6 +870,13 @@ class AlarmsDatasource implements DataSource<AlarmInfo> {
return this.selection.isSelected(alarm);
}
clearSelection() {
if (this.selection.hasValue()) {
this.selection.clear();
this.onSelectionModeChanged(false);
}
}
masterToggle() {
this.alarmsSubject.pipe(
tap((alarms) => {

3
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -234,6 +234,9 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string {
'.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-cell button.mat-icon-button[disabled][disabled] mat-icon {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-divider {\n'+
'border-top-color: ' + mdDarkDivider + ';\n'+
'}\n'+

25
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Inject, Injectable } from '@angular/core';
import { Inject, Injectable, Type } from '@angular/core';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { WidgetService } from '@core/http/widget.service';
import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
@ -34,7 +34,6 @@ import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
import { isFunction, isUndefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
import { SharedModule } from '@shared/shared.module';
import { WidgetComponentsModule } from '@home/components/widget/widget-components.module';
import { WINDOW } from '@core/services/window.service';
@ -43,6 +42,7 @@ import { TbFlot } from './lib/flot-widget';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetTypeId } from '@app/shared/models/id/widget-type-id';
import { TenantId } from '@app/shared/models/id/tenant-id';
import { SharedModule } from '@shared/shared.module';
const tinycolor = tinycolor_;
@ -117,16 +117,23 @@ export class WidgetComponentService {
const initSubject = new ReplaySubject();
this.init$ = initSubject.asObservable();
const loadDefaultWidgetInfoTasks = [
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type'),
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type'),
this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule]),
this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule]),
];
forkJoin(loadDefaultWidgetInfoTasks).subscribe(
() => {
initSubject.next();
},
() => {
(e) => {
let errorMessages = ['Failed to load default widget types!'];
if (e && e.length) {
errorMessages = errorMessages.concat(e);
}
console.error('Failed to load default widget types!');
initSubject.error('Failed to load default widget types!');
initSubject.error({
widgetInfo: this.errorWidgetType,
errorMessages
});
}
);
return this.init$;
@ -194,7 +201,7 @@ export class WidgetComponentService {
}
if (widgetControllerDescriptor) {
const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`;
this.loadWidgetResources(widgetInfo, widgetNamespace).subscribe(
this.loadWidgetResources(widgetInfo, widgetNamespace, [WidgetComponentsModule]).subscribe(
() => {
if (widgetControllerDescriptor.settingsSchema) {
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
@ -219,7 +226,7 @@ export class WidgetComponentService {
}
}
private loadWidgetResources(widgetInfo: WidgetInfo, widgetNamespace: string): Observable<any> {
private loadWidgetResources(widgetInfo: WidgetInfo, widgetNamespace: string, modules?: Type<any>[]): Observable<any> {
this.cssParser.cssPreviewNamespace = widgetNamespace;
this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
const resourceTasks: Observable<string>[] = [];
@ -236,7 +243,7 @@ export class WidgetComponentService {
this.dynamicComponentFactoryService.createDynamicComponentFactory(
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
widgetInfo.templateHtml,
[SharedModule, WidgetComponentsModule]
modules
).pipe(
map((factory) => {
widgetInfo.componentFactory = factory;

11
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -17,25 +17,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
import { LegendComponent } from '@home/components/widget/legend.component';
import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component';
import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@NgModule({
entryComponents: [
DisplayColumnsPanelComponent
DisplayColumnsPanelComponent,
AlarmStatusFilterPanelComponent
],
declarations:
[
DisplayColumnsPanelComponent,
AlarmStatusFilterPanelComponent,
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent
],
imports: [
CommonModule,
SharedModule
SharedModule,
SharedHomeComponentsModule
],
exports: [
EntitiesTableWidgetComponent,

16
ui-ngx/src/app/shared/models/page/page-link.ts

@ -16,7 +16,7 @@
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { getDescendantProp } from '@core/utils';
import { getDescendantProp, isObject } from '@core/utils';
export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean;
@ -28,10 +28,16 @@ const defaultPageLinkSearchFunction: PageLinkSearchFunction<any> =
const expected = ('' + textSearch).toLowerCase();
for (const key of Object.keys(entity)) {
const val = entity[key];
if (val !== null && val !== Object(val)) {
const actual = ('' + val).toLowerCase();
if (actual.indexOf(expected) !== -1) {
return true;
if (val !== null) {
if (val !== Object(val)) {
const actual = ('' + val).toLowerCase();
if (actual.indexOf(expected) !== -1) {
return true;
}
} else if (isObject(val)) {
if (defaultPageLinkSearchFunction(val, textSearch)) {
return true;
}
}
}
}

5
ui-ngx/src/styles.scss

@ -498,6 +498,11 @@ mat-label {
mat-icon {
color: rgba(0, 0, 0, .54);
}
&[disabled][disabled] {
mat-icon {
color: rgba(0, 0, 0, .26);
}
}
}
}

Loading…
Cancel
Save