Browse Source

Merge branch 'master' into release-3.5

release-3.5
Igor Kulikov 3 years ago
parent
commit
ca1cede894
  1. 9
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  2. 6
      ui-ngx/src/app/core/services/menu.service.ts
  3. 34
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  4. 17
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  5. 4
      ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html
  6. 2
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw
  7. 2
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw
  8. 4
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw
  9. 14
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  10. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  11. 88
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  12. 2
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  13. 6
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts
  14. 3
      ui-ngx/src/app/shared/models/widget.models.ts
  15. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md
  16. 10
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md
  17. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md
  18. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md
  19. 10
      ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md
  20. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md
  21. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

9
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -86,6 +86,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -294,7 +295,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return; return;
} }
if (inactivityTimeout <= 0L) { if (inactivityTimeout <= 0L) {
inactivityTimeout = defaultInactivityTimeoutInSec; inactivityTimeout = defaultInactivityTimeoutMs;
} }
log.trace("on Device Activity Timeout Update device id {} inactivityTimeout {}", deviceId, inactivityTimeout); log.trace("on Device Activity Timeout Update device id {} inactivityTimeout {}", deviceId, inactivityTimeout);
DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
@ -442,6 +443,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
Map<TenantId, Pair<AtomicInteger, AtomicInteger>> devicesActivity = new HashMap<>(); Map<TenantId, Pair<AtomicInteger, AtomicInteger>> devicesActivity = new HashMap<>();
partitionedEntities.forEach((tpi, deviceIds) -> { partitionedEntities.forEach((tpi, deviceIds) -> {
log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size()); log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size());
Set<DeviceId> idsFromRemovedTenant = new HashSet<>();
for (DeviceId deviceId : deviceIds) { for (DeviceId deviceId : deviceIds) {
DeviceStateData stateData; DeviceStateData stateData;
try { try {
@ -453,9 +455,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
try { try {
updateInactivityStateIfExpired(ts, deviceId, stateData); updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) { if (e instanceof TenantNotFoundException) {
idsFromRemovedTenant.add(deviceId);
continue; continue;
} else {
log.warn("[{}] Failed to update inactivity state [{}]", deviceId, e.getMessage());
} }
} }
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(), Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
@ -466,6 +470,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
tenantDevicesActivity.getRight().incrementAndGet(); tenantDevicesActivity.getRight().incrementAndGet();
} }
} }
deviceIds.removeAll(idsFromRemovedTenant);
}); });
devicesActivity.forEach((tenantId, tenantDevicesActivity) -> { devicesActivity.forEach((tenantId, tenantDevicesActivity) -> {
int active = tenantDevicesActivity.getLeft().get(); int active = tenantDevicesActivity.getLeft().get();

6
ui-ngx/src/app/core/services/menu.service.ts

@ -361,7 +361,8 @@ export class MenuService {
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'mdi:alert-outline',
isMdiIcon: true
}, },
{ {
id: 'dashboards', id: 'dashboards',
@ -813,7 +814,8 @@ export class MenuService {
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'mdi:alert-outline',
isMdiIcon: true
}, },
{ {
id: 'dashboards', id: 'dashboards',

34
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -160,17 +160,41 @@
</ng-container> </ng-container>
<ng-container matColumnDef="key"> <ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> {{ 'attribute.key' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> {{ 'attribute.key' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute" class="attribute">
{{ attribute.key }} <div fxLayout="row" style="align-items: center">
<span class="ellipsis">{{ attribute.key }}</span>
<tb-copy-button
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.key"
tooltipText="{{ 'attribute.copy-key' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
</div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'attribute.value' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 60%"> {{ 'attribute.value' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute" <mat-cell *matCellDef="let attribute"
class="tb-value-cell" class="tb-value-cell attribute"
(click)="editAttribute($event, attribute)"> (click)="editAttribute($event, attribute)">
<div fxLayout="row" style="align-items: center"> <div fxFlex fxLayout="row" style="align-items: center">
<span style="overflow: hidden;text-overflow: ellipsis;" fxFlex>{{attribute.value | tbJson}}</span> <div fxFlex fxLayout="row" style="overflow: hidden; align-items: center;">
<span class="ellipsis">{{attribute.value | tbJson}}</span>
<tb-copy-button
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.value | tbJson"
tooltipText="{{ 'attribute.copy-value' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
</div>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"> <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</span> </span>

17
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -51,6 +51,7 @@
overflow: auto; overflow: auto;
.mat-mdc-table { .mat-mdc-table {
table-layout: fixed; table-layout: fixed;
min-width: 450px;
} }
} }
@ -65,6 +66,22 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
.attribute-copy {
visibility: hidden;
transition: visibility 0.1s;
}
.attribute:hover {
.attribute-copy {
visibility: visible;
}
}
} }
} }

4
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html

@ -27,6 +27,10 @@
(ngModelChange)="notifyActionUpdated()" (ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}"/> placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field> </mat-form-field>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="notifyActionUpdated()">
{{ 'widget.resource-is-module' | translate }}
</mat-checkbox>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
type="button" type="button"

2
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw

@ -9,7 +9,6 @@
} }
.edit-entity-form .boolean-value-input .checkbox-label { .edit-entity-form .boolean-value-input .checkbox-label {
margin-bottom: 8px;
color: rgba(0,0,0,0.54); color: rgba(0,0,0,0.54);
font-size: 12px; font-size: 12px;
} }
@ -61,7 +60,6 @@
} }
.add-entity-form .boolean-value-input .checkbox-label { .add-entity-form .boolean-value-input .checkbox-label {
margin-bottom: 8px;
color: rgba(0,0,0,0.54); color: rgba(0,0,0,0.54);
font-size: 12px; font-size: 12px;
} }

2
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw

@ -111,7 +111,7 @@
<!-- <button mat-icon-button color="primary"--> <!-- <button mat-icon-button color="primary"-->
<!-- aria-label="Remove"--> <!-- aria-label="Remove"-->
<!-- type="button"--> <!-- type="button"-->
<!-- (click)="removeOldRelation(i)"--> <!-- (click)="removeOldRelation(i, relation.value)"-->
<!-- matTooltip="Remove relation"--> <!-- matTooltip="Remove relation"-->
<!-- matTooltipPosition="above">--> <!-- matTooltipPosition="above">-->
<!-- <mat-icon>close</mat-icon>--> <!-- <mat-icon>close</mat-icon>-->

4
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw

@ -136,9 +136,9 @@
// vm.relations().markAsDirty(); // vm.relations().markAsDirty();
// }; // };
// //
// vm.removeOldRelation = function(index) { // vm.removeOldRelation = function(index, relation) {
// vm.oldRelations().removeAt(index); // vm.oldRelations().removeAt(index);
// vm.relationsToDelete.push(vm.oldRelationsData[index]); // vm.relationsToDelete.push(relation);
// vm.oldRelations().markAsDirty(); // vm.oldRelations().markAsDirty();
// }; // };
// //

14
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -33,6 +33,8 @@ import { HOME_COMPONENTS_MODULE_TOKEN, SHARED_HOME_COMPONENTS_MODULE_TOKEN } fro
@Injectable() @Injectable()
export class CustomDialogService { export class CustomDialogService {
private customModules: Array<Type<any>>
constructor( constructor(
private translate: TranslateService, private translate: TranslateService,
private authService: AuthService, private authService: AuthService,
@ -44,12 +46,18 @@ export class CustomDialogService {
) { ) {
} }
setAdditionalModules(modules: Array<Type<any>>) {
this.customModules = modules;
}
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any,
config?: MatDialogConfig): Observable<any> { config?: MatDialogConfig): Observable<any> {
const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule];
if (Array.isArray(this.customModules)) {
modules.push(...this.customModules);
}
return this.dynamicComponentFactoryService.createDynamicComponentFactory( return this.dynamicComponentFactoryService.createDynamicComponentFactory(
class CustomDialogComponentInstance extends CustomDialogComponent {}, class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
template,
[this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule]).pipe(
mergeMap((factory) => { mergeMap((factory) => {
const dialogData: CustomDialogContainerData = { const dialogData: CustomDialogContainerData = {
controller, controller,

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

@ -23,7 +23,6 @@ import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-t
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component'; import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { RpcWidgetsModule } from '@home/components/widget/lib/rpc/rpc-widgets.module'; import { RpcWidgetsModule } from '@home/components/widget/lib/rpc/rpc-widgets.module';
import { import {
DateRangeNavigatorPanelComponent, DateRangeNavigatorPanelComponent,
@ -33,7 +32,6 @@ import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multip
import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component'; import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component';
import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component'; import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component';
import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component'; import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
@ -93,8 +91,6 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
MarkdownWidgetComponent MarkdownWidgetComponent
], ],
providers: [ providers: [
CustomDialogService,
ImportExportService,
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }
] ]
}) })

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

@ -29,8 +29,10 @@ import {
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Optional,
Renderer2, Renderer2,
SimpleChanges, SimpleChanges,
Type,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
@ -89,7 +91,7 @@ import {
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { ResourcesService } from '@core/services/resources.service'; import { ModulesWithFactories, ResourcesService } from '@core/services/resources.service';
import { catchError, map, switchMap } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
@ -115,6 +117,8 @@ import { DialogService } from '@core/services/dialog.service';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens';
import { MODULES_MAP } from '@shared/models/constants';
import { IModulesMap } from '@modules/common/modules-map.models';
@Component({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -190,6 +194,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
@Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType<any>,
@Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType<any>,
@Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap,
private widgetService: WidgetService, private widgetService: WidgetService,
private resources: ResourcesService, private resources: ResourcesService,
private timeService: TimeService, private timeService: TimeService,
@ -1160,8 +1165,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (isDefined(customHtml) && customHtml.length > 0) { if (isDefined(customHtml) && customHtml.length > 0) {
htmlTemplate = customHtml; htmlTemplate = customHtml;
} }
this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe( this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({
() => { next: () => {
if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) {
try { try {
if (!additionalParams) { if (!additionalParams) {
@ -1169,16 +1174,17 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId',
'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction);
this.widgetContext.customDialog.setAdditionalModules(descriptor.customModules);
customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
}, },
(errorMessages: string[]) => { error: (errorMessages: string[]) => {
this.processResourcesLoadErrors(errorMessages); this.processResourcesLoadErrors(errorMessages);
} }
); });
break; break;
case WidgetActionType.mobileAction: case WidgetActionType.mobileAction:
const mobileAction = descriptor.mobileAction; const mobileAction = descriptor.mobileAction;
@ -1473,33 +1479,69 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
} }
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>): Observable<any> { private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>, actionDescriptor: WidgetActionDescriptor): Observable<any> {
const resourceTasks: Observable<string>[] = [];
const modulesTasks: Observable<ModulesWithFactories | string>[] = [];
if (isDefined(customCss) && customCss.length > 0) { if (isDefined(customCss) && customCss.length > 0) {
this.cssParser.cssPreviewNamespace = actionNamespace; this.cssParser.cssPreviewNamespace = actionNamespace;
this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace');
} }
const resourceTasks: Observable<string>[] = [];
if (isDefined(customResources) && customResources.length > 0) { if (isDefined(customResources) && customResources.length > 0) {
customResources.forEach((resource) => { customResources.forEach(resource => {
resourceTasks.push( if (resource.isModule) {
this.resources.loadResource(resource.url).pipe( modulesTasks.push(
catchError(e => of(`Failed to load custom action resource: '${resource.url}'`)) this.resources.loadFactories(resource.url, this.modulesMap).pipe(
) catchError((e: Error) => of(e?.message ? e.message : `Failed to load custom action resource module: '${resource.url}'`))
); )
);
} else {
resourceTasks.push(
this.resources.loadResource(resource.url).pipe(
catchError(() => of(`Failed to load custom action resource: '${resource.url}'`))
)
);
}
}); });
return forkJoin(resourceTasks).pipe(
switchMap(msgs => { if (modulesTasks.length) {
let errors: string[]; const modulesObservable: Observable<string | Type<any>[]> = forkJoin(modulesTasks).pipe(
if (msgs && msgs.length) { map(res => {
errors = msgs.filter(msg => msg && msg.length > 0); const msg = res.find(r => typeof r === 'string');
if (msg) {
return msg as string;
} else {
const modulesWithFactoriesList = res as ModulesWithFactories[];
const resModulesWithFactories: ModulesWithFactories = {
modules: modulesWithFactoriesList.map(mf => mf.modules).flat(),
factories: modulesWithFactoriesList.map(mf => mf.factories).flat()
};
return resModulesWithFactories.modules;
} }
if (errors && errors.length) { })
return throwError(errors); );
resourceTasks.push(modulesObservable.pipe(
map((resolvedModules) => {
if (typeof resolvedModules === 'string') {
return resolvedModules;
} else { } else {
return of(null); actionDescriptor.customModules = resolvedModules;
return null;
} }
} })));
)); }
return forkJoin(resourceTasks).pipe(
switchMap(msgs => {
const errors = msgs.filter(msg => msg && msg.length > 0);
if (errors.length > 0) {
return throwError(() => errors);
} else {
return of(null);
}}
));
} else { } else {
return of(null); return of(null);
} }

2
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -42,7 +42,7 @@ const routes: Routes = [
title: 'alarm.alarms', title: 'alarm.alarms',
breadcrumb: { breadcrumb: {
label: 'alarm.alarms', label: 'alarm.alarms',
icon: 'notifications' icon: 'mdi:alert-outline'
}, },
isPage: true, isPage: true,
alarmsMode: AlarmsMode.ALL alarmsMode: AlarmsMode.ALL

6
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts

@ -26,7 +26,7 @@ import { mergeMap, Observable, of } from 'rxjs';
import { Dashboard } from '@app/shared/models/dashboard.models'; import { Dashboard } from '@app/shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { UserSettingsService } from '@core/http/user-settings.service'; import { UserSettingsService } from '@core/http/user-settings.service';
import { UserDashboardAction } from '@shared/models/user-settings.models'; import { UserDashboardAction } from '@shared/models/user-settings.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -47,7 +47,9 @@ export class DashboardResolver implements Resolve<Dashboard> {
return this.dashboardService.getDashboard(dashboardId).pipe( return this.dashboardService.getDashboard(dashboardId).pipe(
mergeMap((dashboard) => mergeMap((dashboard) =>
(getCurrentAuthUser(this.store).isPublic ? of(null) : (getCurrentAuthUser(this.store).isPublic ? of(null) :
this.userSettingService.reportUserDashboardAction(dashboardId, UserDashboardAction.VISIT, {ignoreLoading: true})).pipe( this.userSettingService.reportUserDashboardAction(dashboardId, UserDashboardAction.VISIT,
{ignoreLoading: true, ignoreErrors: true})).pipe(
catchError(() => of(dashboard)),
map(() => dashboard) map(() => dashboard)
)), )),
map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard)) map((dashboard) => this.dashboardUtils.validateAndUpdateDashboard(dashboard))

3
ui-ngx/src/app/shared/models/widget.models.ts

@ -32,7 +32,7 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; import { AfterViewInit, Directive, EventEmitter, Inject, OnInit, Type } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
@ -560,6 +560,7 @@ export interface CustomActionDescriptor {
customResources?: Array<WidgetResource>; customResources?: Array<WidgetResource>;
customHtml?: string; customHtml?: string;
customCss?: string; customCss?: string;
customModules?: Type<any>[];
} }
export interface WidgetActionDescriptor extends CustomActionDescriptor { export interface WidgetActionDescriptor extends CustomActionDescriptor {

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md

@ -82,7 +82,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

10
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md

@ -5,7 +5,7 @@
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" <form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
(ngSubmit)="save()" class="edit-entity-form"> (ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar fxLayout="row" color="primary">
<h2>Edit </h2> <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button (click)="cancel()" type="button"> <button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -67,7 +67,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -84,7 +84,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">
@ -109,7 +109,7 @@
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
aria-label="Remove" aria-label="Remove"
type="button" type="button"
(click)="removeOldRelation(i)" (click)="removeOldRelation(i, relation.value)"
matTooltip="Remove relation" matTooltip="Remove relation"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -130,7 +130,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md

@ -82,9 +82,9 @@ function EditEntityDialogController(instance) {
vm.relations().markAsDirty(); vm.relations().markAsDirty();
}; };
vm.removeOldRelation = function(index) { vm.removeOldRelation = function(index, relation) {
vm.oldRelations().removeAt(index); vm.oldRelations().removeAt(index);
vm.relationsToDelete.push(vm.oldRelationsData[index]); vm.relationsToDelete.push(relation);
vm.oldRelations().markAsDirty(); vm.oldRelations().markAsDirty();
}; };

4
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md

@ -82,7 +82,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

10
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md

@ -5,7 +5,7 @@
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" <form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
(ngSubmit)="save()" class="edit-entity-form"> (ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary"> <mat-toolbar fxLayout="row" color="primary">
<h2>Edit </h2> <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button (click)="cancel()" type="button"> <button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -67,7 +67,7 @@
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>
<label class="checkbox-label">Boolean Value</label> <label class="checkbox-label">Boolean Value</label>
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">
{{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
@ -84,7 +84,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">
@ -109,7 +109,7 @@
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
aria-label="Remove" aria-label="Remove"
type="button" type="button"
(click)="removeOldRelation(i)" (click)="removeOldRelation(i, relation.value)"
matTooltip="Remove relation" matTooltip="Remove relation"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
@ -130,7 +130,7 @@
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select formControlName="direction" name="direction"> <mat-select formControlName="direction" name="direction">
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">
{{ ("relation.search-direction." + direction.value) | translate}}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="relation.get('direction').hasError('required')"> <mat-error *ngIf="relation.get('direction').hasError('required')">

4
ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md

@ -82,9 +82,9 @@ function EditEntityDialogController(instance) {
vm.relations().markAsDirty(); vm.relations().markAsDirty();
}; };
vm.removeOldRelation = function(index) { vm.removeOldRelation = function(index, relation) {
vm.oldRelations().removeAt(index); vm.oldRelations().removeAt(index);
vm.relationsToDelete.push(vm.oldRelationsData[index]); vm.relationsToDelete.push(relation);
vm.oldRelations().markAsDirty(); vm.oldRelations().markAsDirty();
}; };

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

@ -691,7 +691,9 @@
"selected-attributes": "{ count, plural, =1 {1 attribute} other {# attributes} } selected", "selected-attributes": "{ count, plural, =1 {1 attribute} other {# attributes} } selected",
"selected-telemetry": "{ count, plural, =1 {1 telemetry unit} other {# telemetry units} } selected", "selected-telemetry": "{ count, plural, =1 {1 telemetry unit} other {# telemetry units} } selected",
"no-attributes-text": "No attributes found", "no-attributes-text": "No attributes found",
"no-telemetry-text": "No telemetry found" "no-telemetry-text": "No telemetry found",
"copy-key": "Copy key",
"copy-value": "Copy value"
}, },
"api-usage": { "api-usage": {
"api-features": "API features", "api-features": "API features",

Loading…
Cancel
Save