NIFI-13047: Adding property history to the property tooltip (#8652)

* NIFI-13047:
- Adding property history to the property tooltip in the Edit dialogs for Processors, Controller Services, Reporting Tasks, Parameter Providers, and Flow Analysis Rules.

* NIFI-13054:
- Addressing review feedback.

This closes #8652
This commit is contained in:
Matt Gilman 2024-04-16 13:47:23 -04:00 committed by GitHub
parent 44b1353440
commit 05558ca2de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 222 additions and 46 deletions

View File

@ -30,6 +30,7 @@ import { EditControllerService } from '../../../../ui/common/controller-service/
import { import {
ComponentType, ComponentType,
ControllerServiceReferencingComponent, ControllerServiceReferencingComponent,
EditControllerServiceDialogRequest,
UpdateControllerServiceRequest UpdateControllerServiceRequest
} from '../../../../state/shared'; } from '../../../../state/shared';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -196,6 +197,30 @@ export class ControllerServicesEffects {
this.actions$.pipe( this.actions$.pipe(
ofType(ControllerServicesActions.openConfigureControllerServiceDialog), ofType(ControllerServicesActions.openConfigureControllerServiceDialog),
map((action) => action.request), map((action) => action.request),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
switchMap(([request, processGroupId]) =>
from(this.propertyTableHelperService.getComponentHistory(request.id)).pipe(
map((history) => {
return {
...request,
history: history.componentHistory
} as EditControllerServiceDialogRequest;
}),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
ControllerServicesActions.selectControllerService({
request: {
processGroupId,
id: request.id
}
})
);
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
})
)
),
concatLatestFrom(() => [ concatLatestFrom(() => [
this.store.select(selectParameterContext), this.store.select(selectParameterContext),
this.store.select(selectCurrentProcessGroupId) this.store.select(selectCurrentProcessGroupId)
@ -205,9 +230,7 @@ export class ControllerServicesEffects {
const editDialogReference = this.dialog.open(EditControllerService, { const editDialogReference = this.dialog.open(EditControllerService, {
...LARGE_DIALOG, ...LARGE_DIALOG,
data: { data: request,
controllerService: request.controllerService
},
id: serviceId id: serviceId
}); });

View File

@ -1102,11 +1102,15 @@ export class FlowEffects {
ofType(FlowActions.openEditProcessorDialog), ofType(FlowActions.openEditProcessorDialog),
map((action) => action.request), map((action) => action.request),
switchMap((request) => switchMap((request) =>
from(this.flowService.getProcessor(request.entity.id)).pipe( combineLatest([
map((entity) => { this.flowService.getProcessor(request.entity.id),
this.propertyTableHelperService.getComponentHistory(request.entity.id)
]).pipe(
map(([entity, history]) => {
return { return {
...request, ...request,
entity entity,
history: history.componentHistory
}; };
}), }),
tap({ tap({

View File

@ -19,6 +19,7 @@ import { BreadcrumbEntity, Position } from '../shared';
import { import {
BulletinEntity, BulletinEntity,
Bundle, Bundle,
ComponentHistory,
ComponentType, ComponentType,
DocumentedType, DocumentedType,
ParameterContextReferenceEntity, ParameterContextReferenceEntity,
@ -346,6 +347,7 @@ export interface EditComponentDialogRequest {
type: ComponentType; type: ComponentType;
uri: string; uri: string;
entity: any; entity: any;
history?: ComponentHistory;
} }
export interface EditRemotePortDialogRequest extends EditComponentDialogRequest { export interface EditRemotePortDialogRequest extends EditComponentDialogRequest {

View File

@ -170,6 +170,7 @@
[parameterContext]="parameterContext" [parameterContext]="parameterContext"
[convertToParameter]="convertToParameter" [convertToParameter]="convertToParameter"
[goToService]="goToService" [goToService]="goToService"
[propertyHistory]="request.history"
[supportsSensitiveDynamicProperties]=" [supportsSensitiveDynamicProperties]="
request.entity.component.supportsSensitiveDynamicProperties request.entity.component.supportsSensitiveDynamicProperties
"></property-table> "></property-table>

View File

@ -31,7 +31,7 @@ import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors'; import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import { UpdateControllerServiceRequest } from '../../../../state/shared'; import { UpdateControllerServiceRequest } from '../../../../state/shared';
import { EditFlowAnalysisRule } from '../../ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component'; import { EditFlowAnalysisRule } from '../../ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component';
import { CreateFlowAnalysisRuleSuccess } from './index'; import { CreateFlowAnalysisRuleSuccess, EditFlowAnalysisRuleDialogRequest } from './index';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service'; import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import * as ErrorActions from '../../../../state/error/error.actions'; import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service'; import { ErrorHelper } from '../../../../service/error-helper.service';
@ -218,14 +218,38 @@ export class FlowAnalysisRulesEffects {
this.actions$.pipe( this.actions$.pipe(
ofType(FlowAnalysisRuleActions.openConfigureFlowAnalysisRuleDialog), ofType(FlowAnalysisRuleActions.openConfigureFlowAnalysisRuleDialog),
map((action) => action.request), map((action) => action.request),
switchMap((request) =>
from(this.propertyTableHelperService.getComponentHistory(request.id)).pipe(
map((history) => {
return {
...request,
history: history.componentHistory
} as EditFlowAnalysisRuleDialogRequest;
}),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowAnalysisRuleActions.selectFlowAnalysisRule({
request: {
id: request.id
}
})
);
this.store.dispatch(
FlowAnalysisRuleActions.flowAnalysisRuleSnackbarApiError({
error: errorResponse.error
})
);
}
})
)
),
tap((request) => { tap((request) => {
const ruleId: string = request.id; const ruleId: string = request.id;
const editDialogReference = this.dialog.open(EditFlowAnalysisRule, { const editDialogReference = this.dialog.open(EditFlowAnalysisRule, {
...LARGE_DIALOG, ...LARGE_DIALOG,
data: { data: request,
flowAnalysisRule: request.flowAnalysisRule
},
id: ruleId id: ruleId
}); });

View File

@ -15,7 +15,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { BulletinEntity, Bundle, DocumentedType, Permissions, Revision } from '../../../../state/shared'; import {
BulletinEntity,
Bundle,
ComponentHistory,
DocumentedType,
Permissions,
Revision
} from '../../../../state/shared';
export const flowAnalysisRulesFeatureKey = 'flowAnalysisRules'; export const flowAnalysisRulesFeatureKey = 'flowAnalysisRules';
@ -88,6 +95,7 @@ export interface ConfigureFlowAnalysisRuleRequest {
export interface EditFlowAnalysisRuleDialogRequest { export interface EditFlowAnalysisRuleDialogRequest {
id: string; id: string;
flowAnalysisRule: FlowAnalysisRuleEntity; flowAnalysisRule: FlowAnalysisRuleEntity;
history?: ComponentHistory;
} }
export interface DeleteFlowAnalysisRuleRequest { export interface DeleteFlowAnalysisRuleRequest {

View File

@ -32,6 +32,7 @@ import { EditControllerService } from '../../../../ui/common/controller-service/
import { import {
ComponentType, ComponentType,
ControllerServiceReferencingComponent, ControllerServiceReferencingComponent,
EditControllerServiceDialogRequest,
UpdateControllerServiceRequest UpdateControllerServiceRequest
} from '../../../../state/shared'; } from '../../../../state/shared';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -188,14 +189,38 @@ export class ManagementControllerServicesEffects {
this.actions$.pipe( this.actions$.pipe(
ofType(ManagementControllerServicesActions.openConfigureControllerServiceDialog), ofType(ManagementControllerServicesActions.openConfigureControllerServiceDialog),
map((action) => action.request), map((action) => action.request),
switchMap((request) =>
from(this.propertyTableHelperService.getComponentHistory(request.id)).pipe(
map((history) => {
return {
...request,
history: history.componentHistory
} as EditControllerServiceDialogRequest;
}),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
ManagementControllerServicesActions.selectControllerService({
request: {
id: request.id
}
})
);
this.store.dispatch(
ManagementControllerServicesActions.managementControllerServicesSnackbarApiError({
error: errorResponse.error
})
);
}
})
)
),
tap((request) => { tap((request) => {
const serviceId: string = request.id; const serviceId: string = request.id;
const editDialogReference = this.dialog.open(EditControllerService, { const editDialogReference = this.dialog.open(EditControllerService, {
...LARGE_DIALOG, ...LARGE_DIALOG,
data: { data: request,
controllerService: request.controllerService
},
id: serviceId id: serviceId
}); });

View File

@ -18,6 +18,7 @@
import { import {
AffectedComponentEntity, AffectedComponentEntity,
Bundle, Bundle,
ComponentHistory,
DocumentedType, DocumentedType,
ParameterContextReferenceEntity, ParameterContextReferenceEntity,
ParameterEntity, ParameterEntity,
@ -158,6 +159,7 @@ export interface DeleteParameterProviderSuccess {
export interface EditParameterProviderRequest { export interface EditParameterProviderRequest {
id: string; id: string;
parameterProvider: ParameterProviderEntity; parameterProvider: ParameterProviderEntity;
history?: ComponentHistory;
} }
export interface ConfigureParameterProviderRequest { export interface ConfigureParameterProviderRequest {

View File

@ -49,7 +49,7 @@ import { CreateParameterProvider } from '../../ui/parameter-providers/create-par
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component'; import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditParameterProvider } from '../../ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component'; import { EditParameterProvider } from '../../ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service'; import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import { ParameterProviderEntity, UpdateParameterProviderRequest } from './index'; import { EditParameterProviderRequest, ParameterProviderEntity, UpdateParameterProviderRequest } from './index';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service'; import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import { FetchParameterProviderParameters } from '../../ui/parameter-providers/fetch-parameter-provider-parameters/fetch-parameter-provider-parameters.component'; import { FetchParameterProviderParameters } from '../../ui/parameter-providers/fetch-parameter-provider-parameters/fetch-parameter-provider-parameters.component';
import * as ErrorActions from '../../../../state/error/error.actions'; import * as ErrorActions from '../../../../state/error/error.actions';
@ -266,13 +266,33 @@ export class ParameterProvidersEffects {
this.actions$.pipe( this.actions$.pipe(
ofType(ParameterProviderActions.openConfigureParameterProviderDialog), ofType(ParameterProviderActions.openConfigureParameterProviderDialog),
map((action) => action.request), map((action) => action.request),
switchMap((request) =>
from(this.propertyTableHelperService.getComponentHistory(request.id)).pipe(
map((history) => {
return {
...request,
history: history.componentHistory
} as EditParameterProviderRequest;
}),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
ParameterProviderActions.selectParameterProvider({
request: {
id: request.id
}
})
);
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
})
)
),
tap((request) => { tap((request) => {
const id = request.id; const id = request.id;
const editDialogReference = this.dialog.open(EditParameterProvider, { const editDialogReference = this.dialog.open(EditParameterProvider, {
...LARGE_DIALOG, ...LARGE_DIALOG,
data: { data: request,
parameterProvider: request.parameterProvider
},
id id
}); });

View File

@ -15,7 +15,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { BulletinEntity, Bundle, DocumentedType, Permissions, Revision } from '../../../../state/shared'; import {
BulletinEntity,
Bundle,
ComponentHistory,
DocumentedType,
Permissions,
Revision
} from '../../../../state/shared';
export const reportingTasksFeatureKey = 'reportingTasks'; export const reportingTasksFeatureKey = 'reportingTasks';
@ -66,6 +73,7 @@ export interface UpdateReportingTaskRequest {
export interface EditReportingTaskDialogRequest { export interface EditReportingTaskDialogRequest {
id: string; id: string;
reportingTask: ReportingTaskEntity; reportingTask: ReportingTaskEntity;
history?: ComponentHistory;
} }
export interface StartReportingTaskRequest { export interface StartReportingTaskRequest {

View File

@ -30,7 +30,7 @@ import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors'; import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import { UpdateControllerServiceRequest } from '../../../../state/shared'; import { UpdateControllerServiceRequest } from '../../../../state/shared';
import { EditReportingTask } from '../../ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component'; import { EditReportingTask } from '../../ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component';
import { CreateReportingTaskSuccess } from './index'; import { CreateReportingTaskSuccess, EditReportingTaskDialogRequest } from './index';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service'; import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service'; import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import * as ErrorActions from '../../../../state/error/error.actions'; import * as ErrorActions from '../../../../state/error/error.actions';
@ -232,14 +232,36 @@ export class ReportingTasksEffects {
this.actions$.pipe( this.actions$.pipe(
ofType(ReportingTaskActions.openConfigureReportingTaskDialog), ofType(ReportingTaskActions.openConfigureReportingTaskDialog),
map((action) => action.request), map((action) => action.request),
switchMap((request) =>
from(this.propertyTableHelperService.getComponentHistory(request.id)).pipe(
map((history) => {
return {
...request,
history: history.componentHistory
} as EditReportingTaskDialogRequest;
}),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
ReportingTaskActions.selectReportingTask({
request: {
id: request.id
}
})
);
this.store.dispatch(
ReportingTaskActions.reportingTasksSnackbarApiError({ error: errorResponse.error })
);
}
})
)
),
tap((request) => { tap((request) => {
const taskId: string = request.id; const taskId: string = request.id;
const editDialogReference = this.dialog.open(EditReportingTask, { const editDialogReference = this.dialog.open(EditReportingTask, {
...LARGE_DIALOG, ...LARGE_DIALOG,
data: { data: request,
reportingTask: request.reportingTask
},
id: taskId id: taskId
}); });

View File

@ -73,6 +73,7 @@
formControlName="properties" formControlName="properties"
[createNewProperty]="createNewProperty" [createNewProperty]="createNewProperty"
[createNewService]="createNewService" [createNewService]="createNewService"
[propertyHistory]="request.history"
[goToService]="goToService"> [goToService]="goToService">
</property-table> </property-table>
</div> </div>

View File

@ -71,6 +71,7 @@
formControlName="properties" formControlName="properties"
[createNewProperty]="createNewProperty" [createNewProperty]="createNewProperty"
[createNewService]="createNewService" [createNewService]="createNewService"
[propertyHistory]="request.history"
[goToService]="goToService"></property-table> [goToService]="goToService"></property-table>
</div> </div>
</mat-tab> </mat-tab>

View File

@ -89,6 +89,7 @@
[createNewProperty]="createNewProperty" [createNewProperty]="createNewProperty"
[createNewService]="createNewService" [createNewService]="createNewService"
[goToService]="goToService" [goToService]="goToService"
[propertyHistory]="request.history"
[supportsSensitiveDynamicProperties]=" [supportsSensitiveDynamicProperties]="
request.reportingTask.component.supportsSensitiveDynamicProperties request.reportingTask.component.supportsSensitiveDynamicProperties
"> ">

View File

@ -19,6 +19,7 @@ import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { catchError, EMPTY, map, Observable, switchMap, take, takeUntil, tap } from 'rxjs'; import { catchError, EMPTY, map, Observable, switchMap, take, takeUntil, tap } from 'rxjs';
import { import {
ComponentHistoryEntity,
ControllerServiceCreator, ControllerServiceCreator,
ControllerServiceEntity, ControllerServiceEntity,
CreateControllerServiceRequest, CreateControllerServiceRequest,
@ -37,20 +38,29 @@ import { Client } from './client.service';
import { NiFiState } from '../state'; import { NiFiState } from '../state';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { snackBarError } from '../state/error/error.actions'; import { snackBarError } from '../state/error/error.actions';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { LARGE_DIALOG, SMALL_DIALOG } from '../index'; import { LARGE_DIALOG, SMALL_DIALOG } from '../index';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class PropertyTableHelperService { export class PropertyTableHelperService {
private static readonly API: string = '../nifi-api';
constructor( constructor(
private httpClient: HttpClient,
private dialog: MatDialog, private dialog: MatDialog,
private store: Store<NiFiState>, private store: Store<NiFiState>,
private extensionTypesService: ExtensionTypesService, private extensionTypesService: ExtensionTypesService,
private client: Client private client: Client
) {} ) {}
getComponentHistory(componentId: string): Observable<ComponentHistoryEntity> {
return this.httpClient.get<ComponentHistoryEntity>(
`${PropertyTableHelperService.API}/flow/history/components/${componentId}`
);
}
/** /**
* Returns a function that can be used to pass into a PropertyTable to support creating a new property * Returns a function that can be used to pass into a PropertyTable to support creating a new property
* @param id id of the component to create the property for * @param id id of the component to create the property for

View File

@ -128,6 +128,7 @@ export interface CreateControllerServiceDialogRequest {
export interface EditControllerServiceDialogRequest { export interface EditControllerServiceDialogRequest {
id: string; id: string;
controllerService: ControllerServiceEntity; controllerService: ControllerServiceEntity;
history?: ComponentHistory;
} }
export interface UpdateControllerServiceRequest { export interface UpdateControllerServiceRequest {
@ -204,6 +205,25 @@ export interface ProvenanceEventDialogRequest {
event: ProvenanceEvent; event: ProvenanceEvent;
} }
export interface PreviousValue {
previousValue: string;
timestamp: string;
userIdentity: string;
}
export interface PropertyHistory {
previousValues: PreviousValue[];
}
export interface ComponentHistory {
componentId: string;
propertyHistory: { [key: string]: PropertyHistory };
}
export interface ComponentHistoryEntity {
componentHistory: ComponentHistory;
}
export interface TextTipInput { export interface TextTipInput {
text: string; text: string;
} }
@ -232,6 +252,7 @@ export interface BulletinsTipInput {
export interface PropertyTipInput { export interface PropertyTipInput {
descriptor: PropertyDescriptor; descriptor: PropertyDescriptor;
propertyHistory?: PropertyHistory;
} }
export interface ParameterTipInput { export interface ParameterTipInput {

View File

@ -102,6 +102,7 @@
[goToParameter]="goToParameter" [goToParameter]="goToParameter"
[convertToParameter]="convertToParameter" [convertToParameter]="convertToParameter"
[goToService]="goToService" [goToService]="goToService"
[propertyHistory]="request.history"
[supportsSensitiveDynamicProperties]=" [supportsSensitiveDynamicProperties]="
request.controllerService.component.supportsSensitiveDynamicProperties request.controllerService.component.supportsSensitiveDynamicProperties
"></property-table> "></property-table>

View File

@ -37,14 +37,12 @@
{{ item.descriptor.displayName }} {{ item.descriptor.displayName }}
</div> </div>
<div> <div>
@if (hasInfo(item.descriptor)) {
<div <div
class="fa fa-question-circle primary-color" class="fa fa-question-circle primary-color"
nifiTooltip nifiTooltip
[tooltipComponentType]="PropertyTip" [tooltipComponentType]="PropertyTip"
[tooltipInputData]="getPropertyTipData(item)" [tooltipInputData]="getPropertyTipData(item)"
[delayClose]="false"></div> [delayClose]="false"></div>
}
</div> </div>
</div> </div>
</td> </td>

View File

@ -34,6 +34,7 @@ import { NiFiCommon } from '../../../service/nifi-common.service';
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { import {
AllowableValueEntity, AllowableValueEntity,
ComponentHistory,
InlineServiceCreationRequest, InlineServiceCreationRequest,
InlineServiceCreationResponse, InlineServiceCreationResponse,
Parameter, Parameter,
@ -105,6 +106,7 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
@Input() convertToParameter!: (name: string, sensitive: boolean, value: string | null) => Observable<string>; @Input() convertToParameter!: (name: string, sensitive: boolean, value: string | null) => Observable<string>;
@Input() goToService!: (serviceId: string) => void; @Input() goToService!: (serviceId: string) => void;
@Input() supportsSensitiveDynamicProperties = false; @Input() supportsSensitiveDynamicProperties = false;
@Input() propertyHistory: ComponentHistory | undefined;
private static readonly PARAM_REF_REGEX: RegExp = /#{[a-zA-Z0-9-_. ]+}/; private static readonly PARAM_REF_REGEX: RegExp = /#{[a-zA-Z0-9-_. ]+}/;
@ -348,14 +350,6 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
return 'property-' + item.id; return 'property-' + item.id;
} }
hasInfo(descriptor: PropertyDescriptor): boolean {
return (
!this.nifiCommon.isBlank(descriptor.description) ||
!this.nifiCommon.isBlank(descriptor.defaultValue) ||
descriptor.supportsEl
);
}
isSensitiveProperty(descriptor: PropertyDescriptor): boolean { isSensitiveProperty(descriptor: PropertyDescriptor): boolean {
return descriptor.sensitive; return descriptor.sensitive;
} }
@ -396,7 +390,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
getPropertyTipData(item: PropertyItem): PropertyTipInput { getPropertyTipData(item: PropertyItem): PropertyTipInput {
return { return {
descriptor: item.descriptor descriptor: item.descriptor,
propertyHistory: this.propertyHistory?.propertyHistory[item.property]
}; };
} }

View File

@ -16,8 +16,8 @@
--> -->
<div class="tooltip" [style.left.px]="left" [style.top.px]="top"> <div class="tooltip" [style.left.px]="left" [style.top.px]="top">
@if (data?.descriptor; as descriptor) {
<div class="flex flex-col gap-y-3"> <div class="flex flex-col gap-y-3">
@if (data?.descriptor; as descriptor) {
@if (hasDescription(descriptor)) { @if (hasDescription(descriptor)) {
<div>{{ descriptor.description }}</div> <div>{{ descriptor.description }}</div>
} }
@ -38,7 +38,16 @@
[bundle]="descriptor.identifiesControllerServiceBundle"></controller-service-api> [bundle]="descriptor.identifiesControllerServiceBundle"></controller-service-api>
</div> </div>
} }
<!-- TODO - Property History --> }
@if (data?.propertyHistory; as propertyHistory) {
<div>
<b>History</b>
<ul class="px-2">
@for (previousValue of propertyHistory.previousValues; track previousValue) {
<li>{{ previousValue.previousValue }} - {{ previousValue.timestamp }} ({{ previousValue.userIdentity }})</li>
}
</ul>
</div> </div>
} }
</div> </div>
</div>