mcgilman commented on code in PR #8241: URL: https://github.com/apache/nifi/pull/8241#discussion_r1450786595
########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts: ########## @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { RouterLink } from '@angular/router'; +import { NgClass, NgIf } from '@angular/common'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatSortModule, Sort } from '@angular/material/sort'; +import { FlowAnalysisRuleEntity } from '../../../state/flow-analysis-rules'; +import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component'; +import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component'; +import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component'; +import { NiFiCommon } from '../../../../../service/nifi-common.service'; +import { BulletinsTipInput, TextTipInput, ValidationErrorsTipInput } from '../../../../../state/shared'; +import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive'; +import { ReportingTaskEntity } from '../../../state/reporting-tasks'; + +@Component({ + selector: 'flow-analysis-rule-table', + standalone: true, + templateUrl: './flow-analysis-rule-table.component.html', + imports: [ + MatButtonModule, + MatDialogModule, + MatTableModule, + MatSortModule, + NgIf, + NgClass, + NifiTooltipDirective, + RouterLink + ], + styleUrls: ['./flow-analysis-rule-table.component.scss', '../../../../../../assets/styles/listing-table.scss'] +}) +export class FlowAnalysisRuleTable { + @Input() set flowAnalysisRules(flowAnalysisRuleEntities: FlowAnalysisRuleEntity[]) { + this.dataSource = new MatTableDataSource<FlowAnalysisRuleEntity>(flowAnalysisRuleEntities); + this.dataSource.data = this.sortFlowAnalysisRules(flowAnalysisRuleEntities, this.sort); + } + @Input() selectedFlowAnalysisRuleId!: string; + @Input() definedByCurrentGroup!: (entity: FlowAnalysisRuleEntity) => boolean; + @Input() canModifyParent!: (entity: FlowAnalysisRuleEntity) => boolean; + + @Output() selectFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() deleteFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = + new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() enableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() disableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = + new EventEmitter<FlowAnalysisRuleEntity>(); + + sort: Sort = { + active: 'name', + direction: 'asc' + }; + + protected readonly TextTip = TextTip; + protected readonly BulletinsTip = BulletinsTip; + protected readonly ValidationErrorsTip = ValidationErrorsTip; + + displayedColumns: string[] = ['moreDetails', 'name', 'type', 'bundle', 'state', 'actions']; + dataSource: MatTableDataSource<FlowAnalysisRuleEntity> = new MatTableDataSource<FlowAnalysisRuleEntity>(); + + constructor(private nifiCommon: NiFiCommon) {} + + updateSort(sort: Sort): void { + this.sort = sort; + this.dataSource.data = this.sortFlowAnalysisRules(this.dataSource.data, sort); + } + + sortFlowAnalysisRules(items: FlowAnalysisRuleEntity[], sort: Sort): FlowAnalysisRuleEntity[] { + const data: FlowAnalysisRuleEntity[] = items.slice(); + return data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + + let retVal: number = 0; + switch (sort.active) { + case 'name': + retVal = this.nifiCommon.compareString(this.formatType(a), this.formatType(b)); + break; + case 'type': + retVal = this.nifiCommon.compareString(this.formatType(a), this.formatType(b)); + break; + case 'bundle': + retVal = this.nifiCommon.compareString(this.formatBundle(a), this.formatBundle(b)); + break; + case 'state': + retVal = this.nifiCommon.compareString(this.formatState(a), this.formatState(b)); + break; + } + + return retVal * (isAsc ? 1 : -1); + }); + } + + canRead(entity: FlowAnalysisRuleEntity): boolean { + return entity.permissions.canRead; + } + + canWrite(entity: FlowAnalysisRuleEntity): boolean { + return entity.permissions.canWrite; + } + + canOperate(entity: FlowAnalysisRuleEntity): boolean { + if (this.canWrite(entity)) { + return true; + } + return !!entity.operatePermissions?.canWrite; + } + + hasComments(entity: FlowAnalysisRuleEntity): boolean { + return !this.nifiCommon.isBlank(entity.component.comments); + } + + getCommentsTipData(entity: FlowAnalysisRuleEntity): TextTipInput { + return { + text: entity.component.comments + }; + } + + hasErrors(entity: FlowAnalysisRuleEntity): boolean { + return !this.nifiCommon.isEmpty(entity.component.validationErrors); + } + + getValidationErrorsTipData(entity: FlowAnalysisRuleEntity): ValidationErrorsTipInput { + return { + isValidating: entity.status.validationStatus === 'VALIDATING', + validationErrors: entity.component.validationErrors + }; + } + + hasBulletins(entity: FlowAnalysisRuleEntity): boolean { + return !this.nifiCommon.isEmpty(entity.bulletins); + } + + getBulletinsTipData(entity: FlowAnalysisRuleEntity): BulletinsTipInput { + return { + bulletins: entity.bulletins + }; + } + + getStateIcon(entity: FlowAnalysisRuleEntity): string { + if (entity.status.validationStatus === 'VALIDATING') { + return 'validating fa fa-spin fa-circle-o-notch'; + } else if (entity.status.validationStatus === 'INVALID') { + return 'invalid fa fa-warning'; + } else { + if (entity.status.runStatus === 'DISABLED') { + return 'disabled icon icon-enable-false'; + } else if (entity.status.runStatus === 'ENABLED') { + return 'enabled fa fa-flash'; + } + } + return ''; + } + + formatState(entity: FlowAnalysisRuleEntity): string { + if (entity.status.validationStatus === 'VALIDATING') { + return 'Validating'; + } else if (entity.status.validationStatus === 'INVALID') { + return 'Invalid'; + } else { + if (entity.status.runStatus === 'DISABLED') { + return 'Disabled'; + } else if (entity.status.runStatus === 'ENABLED') { + return 'Enabled'; + } + } + return ''; + } + + formatType(entity: FlowAnalysisRuleEntity): string { + return this.nifiCommon.formatType(entity.component); + } + + formatBundle(entity: FlowAnalysisRuleEntity): string { + return this.nifiCommon.formatBundle(entity.component.bundle); + } + + isDisabled(entity: FlowAnalysisRuleEntity): boolean { + return entity.status.runStatus === 'DISABLED'; + } + + isEnabledOrEnabling(entity: FlowAnalysisRuleEntity): boolean { + return entity.status.runStatus === 'ENABLED' || entity.status.runStatus === 'ENABLING'; + } + + hasActiveThreads(entity: ReportingTaskEntity): boolean { + return entity.status?.activeThreadCount > 0; + } + + canConfigure(entity: FlowAnalysisRuleEntity): boolean { + return this.canRead(entity) && this.canWrite(entity) && this.isDisabled(entity); + } + + configureClicked(entity: FlowAnalysisRuleEntity, event: MouseEvent): void { + event.stopPropagation(); + this.configureFlowAnalysisRule.next(entity); + } + + canEnable(entity: FlowAnalysisRuleEntity): boolean { + const userAuthorized: boolean = this.canRead(entity) && this.canOperate(entity); + return userAuthorized && this.isDisabled(entity) && entity.status.validationStatus === 'VALID'; + } + + enabledClicked(entity: FlowAnalysisRuleEntity, event: MouseEvent): void { + this.enableFlowAnalysisRule.next(entity); + } + + canDisable(entity: FlowAnalysisRuleEntity): boolean { + const userAuthorized: boolean = this.canRead(entity) && this.canOperate(entity); + return userAuthorized && this.isEnabledOrEnabling(entity); + } + + disableClicked(entity: FlowAnalysisRuleEntity, event: MouseEvent): void { + this.disableFlowAnalysisRule.next(entity); + } + + canChangeVersion(entity: FlowAnalysisRuleEntity): boolean { + return ( + this.isDisabled(entity) && + this.canRead(entity) && + this.canWrite(entity) && + entity.component.multipleVersionsAvailable === true + ); + } + + canDelete(entity: FlowAnalysisRuleEntity): boolean { + return this.isDisabled(entity) && this.canRead(entity) && this.canWrite(entity) && this.canModifyParent(entity); Review Comment: We can check directly check the users `controller` permissions. We don't need to use a callback approach here like we needed with Controller Services since their parents depend on its scope. ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts: ########## @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { RouterLink } from '@angular/router'; +import { NgClass, NgIf } from '@angular/common'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatSortModule, Sort } from '@angular/material/sort'; +import { FlowAnalysisRuleEntity } from '../../../state/flow-analysis-rules'; +import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component'; +import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component'; +import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component'; +import { NiFiCommon } from '../../../../../service/nifi-common.service'; +import { BulletinsTipInput, TextTipInput, ValidationErrorsTipInput } from '../../../../../state/shared'; +import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive'; +import { ReportingTaskEntity } from '../../../state/reporting-tasks'; + +@Component({ + selector: 'flow-analysis-rule-table', + standalone: true, + templateUrl: './flow-analysis-rule-table.component.html', + imports: [ + MatButtonModule, + MatDialogModule, + MatTableModule, + MatSortModule, + NgIf, + NgClass, + NifiTooltipDirective, + RouterLink + ], + styleUrls: ['./flow-analysis-rule-table.component.scss', '../../../../../../assets/styles/listing-table.scss'] +}) +export class FlowAnalysisRuleTable { + @Input() set flowAnalysisRules(flowAnalysisRuleEntities: FlowAnalysisRuleEntity[]) { + this.dataSource = new MatTableDataSource<FlowAnalysisRuleEntity>(flowAnalysisRuleEntities); Review Comment: Re-creating the `dataSource` is not necessary with this sorting strategy. ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts: ########## @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { RouterLink } from '@angular/router'; +import { NgClass, NgIf } from '@angular/common'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatSortModule, Sort } from '@angular/material/sort'; +import { FlowAnalysisRuleEntity } from '../../../state/flow-analysis-rules'; +import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component'; +import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component'; +import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component'; +import { NiFiCommon } from '../../../../../service/nifi-common.service'; +import { BulletinsTipInput, TextTipInput, ValidationErrorsTipInput } from '../../../../../state/shared'; +import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive'; +import { ReportingTaskEntity } from '../../../state/reporting-tasks'; + +@Component({ + selector: 'flow-analysis-rule-table', + standalone: true, + templateUrl: './flow-analysis-rule-table.component.html', + imports: [ + MatButtonModule, + MatDialogModule, + MatTableModule, + MatSortModule, + NgIf, + NgClass, + NifiTooltipDirective, + RouterLink + ], + styleUrls: ['./flow-analysis-rule-table.component.scss', '../../../../../../assets/styles/listing-table.scss'] +}) +export class FlowAnalysisRuleTable { + @Input() set flowAnalysisRules(flowAnalysisRuleEntities: FlowAnalysisRuleEntity[]) { + this.dataSource = new MatTableDataSource<FlowAnalysisRuleEntity>(flowAnalysisRuleEntities); + this.dataSource.data = this.sortFlowAnalysisRules(flowAnalysisRuleEntities, this.sort); + } + @Input() selectedFlowAnalysisRuleId!: string; + @Input() definedByCurrentGroup!: (entity: FlowAnalysisRuleEntity) => boolean; + @Input() canModifyParent!: (entity: FlowAnalysisRuleEntity) => boolean; + + @Output() selectFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() deleteFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = + new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() enableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() disableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = + new EventEmitter<FlowAnalysisRuleEntity>(); + + sort: Sort = { + active: 'name', + direction: 'asc' + }; + + protected readonly TextTip = TextTip; + protected readonly BulletinsTip = BulletinsTip; + protected readonly ValidationErrorsTip = ValidationErrorsTip; + + displayedColumns: string[] = ['moreDetails', 'name', 'type', 'bundle', 'state', 'actions']; + dataSource: MatTableDataSource<FlowAnalysisRuleEntity> = new MatTableDataSource<FlowAnalysisRuleEntity>(); + + constructor(private nifiCommon: NiFiCommon) {} + + updateSort(sort: Sort): void { + this.sort = sort; + this.dataSource.data = this.sortFlowAnalysisRules(this.dataSource.data, sort); + } + + sortFlowAnalysisRules(items: FlowAnalysisRuleEntity[], sort: Sort): FlowAnalysisRuleEntity[] { + const data: FlowAnalysisRuleEntity[] = items.slice(); + return data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + + let retVal: number = 0; + switch (sort.active) { + case 'name': + retVal = this.nifiCommon.compareString(this.formatType(a), this.formatType(b)); Review Comment: This should be comparing `a.component.name` and `b.component.name`. ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/state/flow-analysis-rules/flow-analysis-rules.effects.ts: ########## @@ -0,0 +1,536 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import * as FlowAnalysisRuleActions from './flow-analysis-rules.actions'; +import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { NiFiState } from '../../../../state'; +import { selectFlowAnalysisRuleTypes } from '../../../../state/extension-types/extension-types.selectors'; +import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component'; +import { FlowAnalysisRuleService } from '../../service/flow-analysis-rule.service'; +import { Client } from '../../../../service/client.service'; +import { ManagementControllerServiceService } from '../../service/management-controller-service.service'; +import { CreateFlowAnalysisRule } from '../../ui/flow-analysis-rules/create-flow-analysis-rule/create-flow-analysis-rule.component'; +import { Router } from '@angular/router'; +import { selectSaving } from '../management-controller-services/management-controller-services.selectors'; +import { + InlineServiceCreationRequest, + InlineServiceCreationResponse, + NewPropertyDialogRequest, + NewPropertyDialogResponse, + Property, + PropertyDescriptor, + UpdateControllerServiceRequest +} from '../../../../state/shared'; +import { EditFlowAnalysisRule } from '../../ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component'; +import { CreateFlowAnalysisRuleSuccess } from './index'; +import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component'; +import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component'; +import * as ManagementControllerServicesActions from '../management-controller-services/management-controller-services.actions'; +import { ExtensionTypesService } from '../../../../service/extension-types.service'; + +@Injectable() +export class FlowAnalysisRulesEffects { + constructor( + private actions$: Actions, + private store: Store<NiFiState>, + private client: Client, + private managementControllerServiceService: ManagementControllerServiceService, + private extensionTypesService: ExtensionTypesService, + private flowAnalysisRuleService: FlowAnalysisRuleService, + private dialog: MatDialog, + private router: Router + ) {} + + loadFlowAnalysisRule$ = createEffect(() => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.loadFlowAnalysisRules), + switchMap(() => + from(this.flowAnalysisRuleService.getFlowAnalysisRule()).pipe( + map((response) => + FlowAnalysisRuleActions.loadFlowAnalysisRulesSuccess({ + response: { + flowAnalysisRules: response.flowAnalysisRules, + loadedTimestamp: response.currentTime + } + }) + ), + catchError((error) => + of( + FlowAnalysisRuleActions.flowAnalysisRuleApiError({ + error: error.error + }) + ) + ) + ) + ) + ) + ); + + openNewFlowAnalysisRuleDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.openNewFlowAnalysisRuleDialog), + withLatestFrom(this.store.select(selectFlowAnalysisRuleTypes)), + tap(([action, flowAnalysisRuleTypes]) => { + this.dialog.open(CreateFlowAnalysisRule, { + data: { + flowAnalysisRuleTypes + }, + panelClass: 'medium-dialog' + }); + }) + ), + { dispatch: false } + ); + + createFlowAnalysisRule$ = createEffect(() => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.createFlowAnalysisRule), + map((action) => action.request), + switchMap((request) => + from(this.flowAnalysisRuleService.createFlowAnalysisRule(request)).pipe( + map((response) => + FlowAnalysisRuleActions.createFlowAnalysisRuleSuccess({ + response: { + flowAnalysisRule: response + } + }) + ), + catchError((error) => + of( + FlowAnalysisRuleActions.flowAnalysisRuleApiError({ + error: error.error + }) + ) + ) + ) + ) + ) + ); + + createFlowAnalysisRuleSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.createFlowAnalysisRuleSuccess), + map((action) => action.response), + tap(() => { + this.dialog.closeAll(); + }), + switchMap((response: CreateFlowAnalysisRuleSuccess) => + of( + FlowAnalysisRuleActions.selectFlowAnalysisRule({ + request: { + id: response.flowAnalysisRule.id + } + }) + ) + ) + ) + ); + + promptFlowAnalysisRuleDeletion$ = createEffect( + () => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.promptFlowAnalysisRuleDeletion), + map((action) => action.request), + tap((request) => { + const dialogReference = this.dialog.open(YesNoDialog, { + data: { + title: 'Delete Flow Analysis Rule', + message: `Delete reporting task ${request.flowAnalysisRule.component.name}?` + }, + panelClass: 'small-dialog' + }); + + dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => { + this.store.dispatch( + FlowAnalysisRuleActions.deleteFlowAnalysisRule({ + request + }) + ); + }); + }) + ), + { dispatch: false } + ); + + deleteFlowAnalysisRule$ = createEffect(() => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.deleteFlowAnalysisRule), + map((action) => action.request), + switchMap((request) => + from(this.flowAnalysisRuleService.deleteFlowAnalysisRule(request)).pipe( + map((response) => + FlowAnalysisRuleActions.deleteFlowAnalysisRuleSuccess({ + response: { + flowAnalysisRule: response + } + }) + ), + catchError((error) => + of( + FlowAnalysisRuleActions.flowAnalysisRuleApiError({ + error: error.error + }) + ) + ) + ) + ) + ) + ); + + navigateToEditFlowAnalysisRule$ = createEffect( + () => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.navigateToEditFlowAnalysisRule), + map((action) => action.id), + tap((id) => { + this.router.navigate(['/settings', 'flow-analysis-rules', id, 'edit']); + }) + ), + { dispatch: false } + ); + + openConfigureFlowAnalysisRuleDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(FlowAnalysisRuleActions.openConfigureFlowAnalysisRuleDialog), + map((action) => action.request), + tap((request) => { + const ruleId: string = request.id; + + const editDialogReference = this.dialog.open(EditFlowAnalysisRule, { + data: { + flowAnalysisRule: request.flowAnalysisRule + }, + id: ruleId, + panelClass: 'large-dialog' + }); + + editDialogReference.componentInstance.saving$ = this.store.select(selectSaving); + + editDialogReference.componentInstance.createNewProperty = ( + existingProperties: string[], + allowsSensitive: boolean + ): Observable<Property> => { + const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive }; + const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, { + data: dialogRequest, + panelClass: 'small-dialog' + }); + + return newPropertyDialogReference.componentInstance.newProperty.pipe( + take(1), + switchMap((dialogResponse: NewPropertyDialogResponse) => { + return this.flowAnalysisRuleService + .getPropertyDescriptor(request.id, dialogResponse.name, dialogResponse.sensitive) + .pipe( + take(1), + map((response) => { + newPropertyDialogReference.close(); + + return { + property: dialogResponse.name, + value: null, + descriptor: response.propertyDescriptor + }; + }) + ); + }) + ); + }; + + const goTo = (commands: string[], destination: string): void => { + if (editDialogReference.componentInstance.editFlowAnalysisRuleForm.dirty) { + const saveChangesDialogReference = this.dialog.open(YesNoDialog, { + data: { + title: 'Flow Analysis Rule Configuration', + message: `Save changes before going to this ${destination}?` + }, + panelClass: 'small-dialog' + }); + + saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => { + editDialogReference.componentInstance.submitForm(commands); + }); + + saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => { + editDialogReference.close('ROUTED'); + this.router.navigate(commands); + }); + } else { + editDialogReference.close('ROUTED'); + this.router.navigate(commands); + } + }; + + editDialogReference.componentInstance.goToService = (serviceId: string) => { + const commands: string[] = ['/settings', 'management-controller-services', serviceId]; + goTo(commands, 'Controller Service'); + }; + + editDialogReference.componentInstance.createNewService = ( + request: InlineServiceCreationRequest + ): Observable<InlineServiceCreationResponse> => { + const descriptor: PropertyDescriptor = request.descriptor; + + // fetch all services that implement the requested service api + return this.extensionTypesService + .getImplementingControllerServiceTypes( + // @ts-ignore + descriptor.identifiesControllerService, + descriptor.identifiesControllerServiceBundle + ) + .pipe( + take(1), + switchMap((implementingTypesResponse) => { + // show the create controller service dialog with the types that implemented the interface + const createServiceDialogReference = this.dialog.open(CreateControllerService, { + data: { + controllerServiceTypes: implementingTypesResponse.controllerServiceTypes + }, + panelClass: 'medium-dialog' + }); + + return createServiceDialogReference.componentInstance.createControllerService.pipe( + take(1), + switchMap((controllerServiceType) => { + // typically this sequence would be implemented with ngrx actions, however we are + // currently in an edit session and we need to return both the value (new service id) + // and updated property descriptor so the table renders correctly + return this.managementControllerServiceService + .createControllerService({ + revision: { + clientId: this.client.getClientId(), + version: 0 + }, + controllerServiceType: controllerServiceType.type, + controllerServiceBundle: controllerServiceType.bundle + }) + .pipe( + take(1), + switchMap((createResponse) => { + // dispatch an inline create service success action so the new service is in the state + this.store.dispatch( + ManagementControllerServicesActions.inlineCreateControllerServiceSuccess( Review Comment: The controller service state is not loaded when this tab is active. We don't need to dispatch this action here as the services will be loaded when that tab is selected. I took a quick look and it appears I made this mistake earlier in `FlowEffects`. Dispatching `ControllerServicesActions.inlineCreateControllerServiceSuccess` is unnecessary there too. Same for `ReportingTaskEffects`. This is required in the `ManagementControllerServiceEffects` and `ControllerServiceEffects` because the Edit Controller Service dialog is open over the Controller Service listing. Adding a new service will result in the new service being added to the listing. ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/settings/ui/flow-analysis-rules/flow-analysis-rule-table/flow-analysis-rule-table.component.ts: ########## @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { RouterLink } from '@angular/router'; +import { NgClass, NgIf } from '@angular/common'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatSortModule, Sort } from '@angular/material/sort'; +import { FlowAnalysisRuleEntity } from '../../../state/flow-analysis-rules'; +import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component'; +import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component'; +import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component'; +import { NiFiCommon } from '../../../../../service/nifi-common.service'; +import { BulletinsTipInput, TextTipInput, ValidationErrorsTipInput } from '../../../../../state/shared'; +import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive'; +import { ReportingTaskEntity } from '../../../state/reporting-tasks'; + +@Component({ + selector: 'flow-analysis-rule-table', + standalone: true, + templateUrl: './flow-analysis-rule-table.component.html', + imports: [ + MatButtonModule, + MatDialogModule, + MatTableModule, + MatSortModule, + NgIf, + NgClass, + NifiTooltipDirective, + RouterLink + ], + styleUrls: ['./flow-analysis-rule-table.component.scss', '../../../../../../assets/styles/listing-table.scss'] +}) +export class FlowAnalysisRuleTable { + @Input() set flowAnalysisRules(flowAnalysisRuleEntities: FlowAnalysisRuleEntity[]) { + this.dataSource = new MatTableDataSource<FlowAnalysisRuleEntity>(flowAnalysisRuleEntities); + this.dataSource.data = this.sortFlowAnalysisRules(flowAnalysisRuleEntities, this.sort); + } + @Input() selectedFlowAnalysisRuleId!: string; + @Input() definedByCurrentGroup!: (entity: FlowAnalysisRuleEntity) => boolean; + @Input() canModifyParent!: (entity: FlowAnalysisRuleEntity) => boolean; + + @Output() selectFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() deleteFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = + new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() enableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); + @Output() disableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = + new EventEmitter<FlowAnalysisRuleEntity>(); + + sort: Sort = { + active: 'name', + direction: 'asc' + }; + + protected readonly TextTip = TextTip; + protected readonly BulletinsTip = BulletinsTip; + protected readonly ValidationErrorsTip = ValidationErrorsTip; + + displayedColumns: string[] = ['moreDetails', 'name', 'type', 'bundle', 'state', 'actions']; + dataSource: MatTableDataSource<FlowAnalysisRuleEntity> = new MatTableDataSource<FlowAnalysisRuleEntity>(); + + constructor(private nifiCommon: NiFiCommon) {} + + updateSort(sort: Sort): void { + this.sort = sort; + this.dataSource.data = this.sortFlowAnalysisRules(this.dataSource.data, sort); + } + + sortFlowAnalysisRules(items: FlowAnalysisRuleEntity[], sort: Sort): FlowAnalysisRuleEntity[] { + const data: FlowAnalysisRuleEntity[] = items.slice(); + return data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + + let retVal: number = 0; + switch (sort.active) { + case 'name': + retVal = this.nifiCommon.compareString(this.formatType(a), this.formatType(b)); + break; + case 'type': + retVal = this.nifiCommon.compareString(this.formatType(a), this.formatType(b)); + break; + case 'bundle': + retVal = this.nifiCommon.compareString(this.formatBundle(a), this.formatBundle(b)); + break; + case 'state': + retVal = this.nifiCommon.compareString(this.formatState(a), this.formatState(b)); + break; + } + + return retVal * (isAsc ? 1 : -1); + }); + } + + canRead(entity: FlowAnalysisRuleEntity): boolean { + return entity.permissions.canRead; + } + + canWrite(entity: FlowAnalysisRuleEntity): boolean { + return entity.permissions.canWrite; + } + + canOperate(entity: FlowAnalysisRuleEntity): boolean { + if (this.canWrite(entity)) { + return true; + } + return !!entity.operatePermissions?.canWrite; + } + + hasComments(entity: FlowAnalysisRuleEntity): boolean { + return !this.nifiCommon.isBlank(entity.component.comments); + } + + getCommentsTipData(entity: FlowAnalysisRuleEntity): TextTipInput { + return { + text: entity.component.comments + }; + } + + hasErrors(entity: FlowAnalysisRuleEntity): boolean { + return !this.nifiCommon.isEmpty(entity.component.validationErrors); + } + + getValidationErrorsTipData(entity: FlowAnalysisRuleEntity): ValidationErrorsTipInput { + return { + isValidating: entity.status.validationStatus === 'VALIDATING', + validationErrors: entity.component.validationErrors + }; + } + + hasBulletins(entity: FlowAnalysisRuleEntity): boolean { + return !this.nifiCommon.isEmpty(entity.bulletins); + } + + getBulletinsTipData(entity: FlowAnalysisRuleEntity): BulletinsTipInput { + return { + bulletins: entity.bulletins + }; + } + + getStateIcon(entity: FlowAnalysisRuleEntity): string { + if (entity.status.validationStatus === 'VALIDATING') { + return 'validating fa fa-spin fa-circle-o-notch'; + } else if (entity.status.validationStatus === 'INVALID') { + return 'invalid fa fa-warning'; + } else { + if (entity.status.runStatus === 'DISABLED') { + return 'disabled icon icon-enable-false'; + } else if (entity.status.runStatus === 'ENABLED') { + return 'enabled fa fa-flash'; + } + } + return ''; + } + + formatState(entity: FlowAnalysisRuleEntity): string { + if (entity.status.validationStatus === 'VALIDATING') { + return 'Validating'; + } else if (entity.status.validationStatus === 'INVALID') { + return 'Invalid'; + } else { + if (entity.status.runStatus === 'DISABLED') { + return 'Disabled'; + } else if (entity.status.runStatus === 'ENABLED') { + return 'Enabled'; + } + } + return ''; + } + + formatType(entity: FlowAnalysisRuleEntity): string { + return this.nifiCommon.formatType(entity.component); + } + + formatBundle(entity: FlowAnalysisRuleEntity): string { + return this.nifiCommon.formatBundle(entity.component.bundle); + } + + isDisabled(entity: FlowAnalysisRuleEntity): boolean { + return entity.status.runStatus === 'DISABLED'; + } + + isEnabledOrEnabling(entity: FlowAnalysisRuleEntity): boolean { + return entity.status.runStatus === 'ENABLED' || entity.status.runStatus === 'ENABLING'; Review Comment: I don't think we need to handle the transitional state. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
