Calculated Fields Debug Events dialog basic implementation

This commit is contained in:
mpetrov 2025-02-06 15:47:52 +02:00
parent 58864b140e
commit 2214bf8ef2
16 changed files with 310 additions and 24 deletions

View File

@ -29,7 +29,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
@JsonSubTypes.Type(value = RuleChainDebugEventFilter.class, name = "DEBUG_RULE_CHAIN"),
@JsonSubTypes.Type(value = ErrorEventFilter.class, name = "ERROR"),
@JsonSubTypes.Type(value = LifeCycleEventFilter.class, name = "LC_EVENT"),
@JsonSubTypes.Type(value = StatisticsEventFilter.class, name = "STATS")
@JsonSubTypes.Type(value = StatisticsEventFilter.class, name = "STATS"),
@JsonSubTypes.Type(value = CalculatedFieldDebugEventFilter.class, name = "DEBUG_CALCULATED_FIELD")
})
public interface EventFilter {

View File

@ -35,8 +35,12 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models';
import { CalculatedFieldDialogComponent } from './components/public-api';
import {
CalculatedField,
CalculatedFieldDebugDialogData,
CalculatedFieldDialogData
} from '@shared/models/calculated-field.models';
import { CalculatedFieldDebugDialogComponent, CalculatedFieldDialogComponent } from './components/public-api';
import { ImportExportService } from '@shared/import-export/import-export.service';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
@ -46,6 +50,14 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1';
readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'),
action: this.openDebugDialog.bind(this),
data: {
tenantId: this.tenantId,
entityId: this.entityId,
},
};
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
@ -126,6 +138,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void {
const additionalActionConfig = {
...this.additionalDebugActionConfig,
action: () => this.openDebugDialog({...this.additionalDebugActionConfig.data, id }),
};
const { viewContainerRef } = this.getTable();
if ($event) {
$event.stopPropagation();
@ -140,6 +156,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
maxDebugModeDuration: this.maxDebugModeDuration,
entityLabel: this.translate.instant('debug-settings.calculated-field'),
additionalActionConfig,
...debugSettings
},
{},
@ -183,11 +200,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
tenantId: this.tenantId,
entityName: this.entityName,
additionalDebugActionConfig: this.additionalDebugActionConfig,
}
})
.afterClosed();
}
private openDebugDialog(data: CalculatedFieldDebugDialogData): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data
})
.afterClosed()
.subscribe();
}
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
if ($event) {
$event.stopPropagation();

View File

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2024 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 class="h-[77vh] w-screen min-w-80 max-w-4xl">
<mat-toolbar color="primary">
<h2>{{ 'calculated-fields.debugging' | translate}}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content class="tb-form-panel h-full">
<tb-event-table
[tenantId]="data.tenantId"
[debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[active]="true"
[entityId]="data?.id"
[functionTestButtonLabel]="'common.test-function' | translate"
/>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</div>

View File

@ -0,0 +1,53 @@
///
/// Copyright © 2016-2024 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 { AfterViewInit, Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { EventTableComponent } from '@home/components/event/event-table.component';
import { CalculatedFieldDebugDialogData } from '@shared/models/calculated-field.models';
@Component({
selector: 'tb-calculated-field-debug-dialog',
templateUrl: './calculated-field-debug-dialog.component.html',
})
export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, null> implements AfterViewInit {
@ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent;
readonly DebugEventType = DebugEventType;
readonly debugEventTypes = DebugEventType;
readonly EventType = EventType;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, null>) {
super(store, router, dialogRef);
}
ngAfterViewInit(): void {
this.eventsTable.entitiesTable.updateData();
}
cancel(): void {
this.dialogRef.close(null);
}
}

View File

@ -51,6 +51,7 @@
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityLabel]="'debug-settings.calculated-field' | translate"
[debugLimitsConfiguration]="data.debugLimitsConfiguration"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">

View File

@ -65,6 +65,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
map(argumentsObj => Object.keys(argumentsObj))
);
additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig,
action: () => this.data.additionalDebugActionConfig.action({
...this.data.additionalDebugActionConfig.data,
id: this.data.value.id,
}),
} : null;
readonly OutputTypeTranslations = OutputTypeTranslations;
readonly OutputType = OutputType;
readonly AttributeScope = AttributeScope;

View File

@ -17,3 +17,4 @@
export * from './dialog/calculated-field-dialog.component';
export * from './arguments-table/calculated-field-arguments-table.component';
export * from './panel/calculated-field-argument-panel.component';
export * from './debug-dialog/calculated-field-debug-dialog.component';

View File

@ -32,7 +32,7 @@ import { EntityDebugSettingsPanelComponent } from './entity-debug-settings-panel
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { BehaviorSubject, of, shareReplay, timer } from 'rxjs';
import { SECOND, MINUTE } from '@shared/models/time/time.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { AdditionalDebugActionConfig, EntityDebugSettings } from '@shared/models/entity.models';
import { map, switchMap, takeWhile } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
@ -61,6 +61,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
@Input() debugLimitsConfiguration: string;
@Input() entityLabel: string;
@Input() additionalActionConfig: AdditionalDebugActionConfig;
debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false],
@ -133,7 +134,8 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
...debugSettings,
maxDebugModeDuration: this.maxDebugModeDuration,
debugLimitsConfiguration: this.debugLimitsConfiguration,
entityLabel: this.entityLabel
entityLabel: this.entityLabel,
additionalActionConfig: this.additionalActionConfig,
},
{},
{}, {}, true);

View File

@ -48,20 +48,32 @@
</button>
</div>
</div>
<div class="flex justify-end">
<button mat-button
color="primary"
type="button"
(click)="onCancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
[disabled]="(isLoading$ | async) || !onFailuresControl.dirty && !debugAllControl.dirty"
(click)="onApply()">
{{ 'action.apply' | translate }}
</button>
<div class="flex justify-between">
<div>
@if (additionalActionConfig) {
<button mat-button
color="primary"
type="button"
(click)="additionalActionConfig.action()">
{{ additionalActionConfig.title | translate }}
</button>
}
</div>
<div class="flex gap-2">
<button mat-button
color="primary"
type="button"
(click)="onCancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
[disabled]="(isLoading$ | async) || !onFailuresControl.dirty && !debugAllControl.dirty"
(click)="onApply()">
{{ 'action.apply' | translate }}
</button>
</div>
</div>
</div>

View File

@ -21,7 +21,7 @@ import {
Component,
EventEmitter,
Input,
OnInit
OnInit,
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
@ -32,7 +32,7 @@ import { SECOND } from '@shared/models/time/time.models';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { of, shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { AdditionalDebugActionConfig, EntityDebugSettings } from '@shared/models/entity.models';
import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators';
@Component({
@ -54,6 +54,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
@Input() allEnabledUntil = 0;
@Input() maxDebugModeDuration: number;
@Input() debugLimitsConfiguration: string;
@Input() additionalActionConfig: AdditionalDebugActionConfig;
onFailuresControl = this.fb.control(false);
debugAllControl = this.fb.control(false);

View File

@ -355,6 +355,86 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
'48px')
);
break;
case DebugEventType.DEBUG_CALCULATED_FIELD:
this.columns[0].width = '160px';
this.columns.push(
new EntityTableColumn<Event>('entityId', 'event.entity-id', '150px',
(entity) => entity.body.entityId,
() => ({padding: '0 12px 0 0'}),
false,
() => ({padding: '0 12px 0 0'}),
() => undefined,
false,
{
name: this.translate.instant('event.copy-entity-id'),
icon: 'content_paste',
style: {
padding: '4px',
'font-size': '16px',
color: 'rgba(0,0,0,.87)'
},
isEnabled: () => true,
onAction: ($event, entity) => entity.body.entityId,
type: CellActionDescriptorType.COPY_BUTTON
}
),
new EntityTableColumn<Event>('messageId', 'event.message-id', '150px',
(entity) => entity.body.msgId ?? '',
() => ({padding: '0 12px 0 0'}),
false,
() => ({padding: '0 12px 0 0'}),
() => undefined,
false,
{
name: this.translate.instant('event.copy-message-id'),
icon: 'content_paste',
style: {
padding: '4px',
'font-size': '16px',
color: 'rgba(0,0,0,.87)'
},
isEnabled: () => true,
onAction: ($event, entity) => entity.body.msgId ?? '',
type: CellActionDescriptorType.COPY_BUTTON
}
),
new EntityTableColumn<Event>('messageType', 'event.message-type', '150px',
(entity) => entity.body.msgType ?? '',
() => ({padding: '0 12px 0 0'}),
false
),
new EntityActionTableColumn<Event>('arguments', 'event.arguments',
{
name: this.translate.instant('action.view'),
icon: 'more_horiz',
isEnabled: (entity) => entity.body.arguments !== undefined,
onAction: ($event, entity) => this.showContent($event, entity.body.arguments,
'event.arguments', ContentType.JSON, true)
},
'100px'
),
new EntityActionTableColumn<Event>('result', 'event.result',
{
name: this.translate.instant('action.view'),
icon: 'more_horiz',
isEnabled: (entity) => entity.body.result !== undefined,
onAction: ($event, entity) => this.showContent($event, entity.body.result,
'event.result', ContentType.JSON, true)
},
'100px'
),
new EntityActionTableColumn<Event>('error', 'event.error',
{
name: this.translate.instant('action.view'),
icon: 'more_horiz',
isEnabled: (entity) => entity.body.error && entity.body.error.length > 0,
onAction: ($event, entity) => this.showContent($event, entity.body.error,
'event.error')
},
'100px'
)
);
break;
}
if (updateTableColumns) {
this.getTable().columnsUpdated(true);
@ -446,6 +526,15 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
{key: 'errorStr', title: 'event.error'}
);
break;
case DebugEventType.DEBUG_CALCULATED_FIELD:
this.filterColumns.push(
{key: 'entityId', title: 'event.entity-id'},
{key: 'messageId', title: 'event.message-id'},
{key: 'messageType', title: 'event.message-type'},
{key: 'isError', title: 'event.error'},
{key: 'errorStr', title: 'event.error'}
);
break;
}
}

View File

@ -195,6 +195,9 @@ import {
import {
CalculatedFieldArgumentPanelComponent
} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component';
import {
CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component';
@NgModule({
declarations:
@ -343,6 +346,7 @@ import {
CalculatedFieldDialogComponent,
CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent,
],
imports: [
CommonModule,
@ -485,6 +489,7 @@ import {
CalculatedFieldDialogComponent,
CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent,
],
providers: [
WidgetComponentService,

View File

@ -14,7 +14,12 @@
/// limitations under the License.
///
import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models';
import {
AdditionalDebugActionConfig,
EntityDebugSettings,
HasTenantId,
HasVersion
} from '@shared/models/entity.models';
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
import { EntityId } from '@shared/models/id/entity-id';
@ -128,6 +133,13 @@ export interface CalculatedFieldDialogData {
debugLimitsConfiguration: string;
tenantId: string;
entityName?: string;
additionalDebugActionConfig: AdditionalDebugActionConfig;
}
export interface CalculatedFieldDebugDialogData {
id?: CalculatedFieldId;
entityId: EntityId;
tenantId: string;
}
export interface ArgumentEntityTypeParams {

View File

@ -21,6 +21,7 @@ import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models';
import { TenantId } from '@shared/models/id/tenant-id';
import { RuleChainMetaData } from '@shared/models/rule-chain.models';
import { CalculatedFieldDebugDialogData } from '@shared/models/calculated-field.models';
export interface EntityInfo {
name?: string;
@ -203,4 +204,12 @@ export interface EntityDebugSettings {
allEnabledUntil?: number;
}
export type AdditionalDebugActionConfigData = CalculatedFieldDebugDialogData;
export interface AdditionalDebugActionConfig {
action?: (data?: AdditionalDebugActionConfigData) => void;
title: string;
data: AdditionalDebugActionConfigData;
}
export type VersionedEntity = EntityInfoData & HasVersion | RuleChainMetaData;

View File

@ -29,7 +29,8 @@ export enum EventType {
export enum DebugEventType {
DEBUG_RULE_NODE = 'DEBUG_RULE_NODE',
DEBUG_RULE_CHAIN = 'DEBUG_RULE_CHAIN'
DEBUG_RULE_CHAIN = 'DEBUG_RULE_CHAIN',
DEBUG_CALCULATED_FIELD = 'DEBUG_CALCULATED_FIELD'
}
export const eventTypeTranslations = new Map<EventType | DebugEventType, string>(
@ -39,6 +40,7 @@ export const eventTypeTranslations = new Map<EventType | DebugEventType, string>
[EventType.STATS, 'event.type-stats'],
[DebugEventType.DEBUG_RULE_NODE, 'event.type-debug-rule-node'],
[DebugEventType.DEBUG_RULE_CHAIN, 'event.type-debug-rule-chain'],
[DebugEventType.DEBUG_CALCULATED_FIELD, 'event.type-debug-calculated-field'],
]
);
@ -80,7 +82,7 @@ export interface DebugRuleChainEventBody extends BaseEventBody {
error?: string;
}
export type EventBody = ErrorEventBody & LcEventEventBody & StatsEventBody & DebugRuleNodeEventBody & DebugRuleChainEventBody;
export type EventBody = ErrorEventBody & LcEventEventBody & StatsEventBody & DebugRuleNodeEventBody & DebugRuleChainEventBody & CalculatedFieldEventBody;
export interface Event extends BaseData<EventId> {
tenantId: TenantId;
@ -90,6 +92,16 @@ export interface Event extends BaseData<EventId> {
body: EventBody;
}
export interface CalculatedFieldEventBody extends BaseFilterEventBody {
calculatedFieldId: string;
entityId: string;
entityType: EntityType;
arguments: string,
result: string,
msgId: string;
msgType: string;
}
export interface BaseFilterEventBody {
server?: string;
}

View File

@ -1014,6 +1014,7 @@
"script": "Script"
},
"arguments": "Arguments",
"debugging": "Calculated field debugging",
"argument-name": "Argument name",
"datasource": "Datasource",
"add-argument": "Add argument",
@ -1026,6 +1027,7 @@
"argument-customer": "Customer",
"argument-tenant": "Current tenant",
"argument-type": "Argument type",
"see-debug-events": "See debug events",
"attribute": "Attribute",
"timeseries-key": "Time series key",
"device-name": "Device name",
@ -2710,6 +2712,9 @@
"type-stats": "Statistics",
"type-debug-rule-node": "Debug",
"type-debug-rule-chain": "Debug",
"type-debug-calculated-field": "Debug",
"arguments": "Arguments",
"result": "Result",
"no-events-prompt": "No events found",
"error": "Error",
"alarm": "Alarm",