mcgilman commented on code in PR #9548:
URL: https://github.com/apache/nifi/pull/9548#discussion_r1873884611


##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html:
##########
@@ -15,19 +15,89 @@
   ~ limitations under the License.
   -->
 
-<h2 mat-dialog-title>
-    <div class="flex justify-between items-baseline">
-        <div>
-            {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+<h2 id="edit-processor-header" mat-dialog-title>
+    <div class="flex justify-between items-center">
+        <div class="flex items-baseline">
+            <div class="mr-2">
+                {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+            </div>
+            |
+            <div class="ml-2 text-base">
+                {{ formatType() }}
+            </div>
         </div>
-        <div class="text-base">
-            {{ formatType(request.entity) }}
+        <div class="flex">
+            <div>
+                @if (isStoppable()) {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            <i class="mr-2 success-color-default fa 
fa-play"></i>Running<i
+                                class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                        </div>
+                    </button>
+                } @else if (isRunnable()) {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            <i class="mr-2 error-color-variant fa 
fa-stop"></i>Stopped<i
+                                class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                        </div>
+                    </button>
+                } @else {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            @if (isInvalid()) {
+                                <i class="mr-2 fa fa-warning 
caution-color"></i>
+                            } @else if (isDisabled()) {
+                                <i class="mr-2 icon icon-enable-false"></i>
+                            } @else {
+                                <i class="mr-2 fa fa-circle-o-notch fa-spin 
primary-color"></i>
+                            }
+                            {{ formatRunStatus() }}

Review Comment:
   Was it intended to leave off `<i class="ml-2 -mt-1 fa fa-sort-desc"></i>` 
for these run statuses?



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts:
##########
@@ -1556,6 +1569,113 @@ export class FlowEffects {
                             );
                         });
 
+                    this.store
+                        .select(selectProcessor(processorId))
+                        .pipe(
+                            takeUntil(editDialogReference.afterClosed()),
+                            isDefinedAndNotNull(),
+                            filter((processorEntity) => {
+                                return (
+                                    processorEntity.revision.clientId === 
this.client.getClientId() ||
+                                    (runStatusChanged
+                                        ? false
+                                        : processorEntity.revision.clientId 
=== request.entity.revision.clientId)
+                                );

Review Comment:
   This condition can be simplified to
   
   ```
   return (
       processorEntity.revision.clientId === this.client.getClientId() ||
       (!runStatusChanged &&
           processorEntity.revision.clientId === 
request.entity.revision.clientId)
   );
   ```
   
   However, this logic isn't quite right. The first condition is intended to 
allow through updates issued but the current client. Once the dialog is opened, 
that would only include changes to the scheduled state which impacts the run 
status.
   
   The second condition is designed to allow any changes issued by the client 
who made the most recent change as long as the current client hasn't changed 
the schedule state. The issue I see here is that the other client could have 
made any modification. For instance, changing a property. The 
`processorUpdates` in the dialog does not account this. It only considers the 
status, revision, bulletins, and permissions.
   
   Since the system is currently based on optimistic locking, I think we should 
ignore changes from other clients like we would for other cases (like regular 
Processor updates). If another client makes a change, the current client will 
need to get the most up to date changes by refreshing the canvas. I think we 
could simple this filter to simply limit to changes made by the current client 
once they have modified the run status.
   
   ```
   return (
       runStatusChanged && processorEntity.revision.clientId === 
this.client.getClientId()
   );
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts:
##########
@@ -401,8 +460,144 @@ export class EditProcessor extends TabbedDialog {
         });
     }
 
+    hasBulletins(): boolean {
+        return this.request.entity.permissions.canRead && 
!this.nifiCommon.isEmpty(this.bulletins);
+    }
+
+    getBulletinsTipData(): BulletinsTipInput {
+        return {
+            bulletins: this.bulletins
+        };
+    }
+
+    getBulletinTooltipPosition(): ConnectedPosition {
+        return {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetX: -8,
+            offsetY: 8
+        };
+    }
+
+    getMostSevereBulletinLevel(): string | null {
+        // determine the most severe of the bulletins
+        const mostSevere = 
this.canvasUtils.getMostSevereBulletin(this.bulletins);
+        return mostSevere ? mostSevere.bulletin.level.toLowerCase() : null;
+    }
+
+    isStoppable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Running';
+    }
+
+    isInvalid(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Invalid';
+    }
+
+    isDisabled(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Disabled';
+    }
+
+    isRunnable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Stopped'
+        );
+    }
+
+    isDisableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) &&
+            (this.status.aggregateSnapshot.runStatus === 'Stopped' ||
+                this.status.aggregateSnapshot.runStatus === 'Invalid')
+        );
+    }
+
+    isEnableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Disabled'
+        );
+    }
+
+    private canOperate(): boolean {
+        return this.request.entity.permissions.canWrite || 
this.request.entity.operatePermissions?.canWrite;
+    }
+
+    stop(entity: any) {
+        this.stopComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    start(entity: any) {
+        this.startComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    disable(entity: any) {
+        this.disableComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    enable(entity: any) {
+        this.enableComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,

Review Comment:
   ```suggestion
               revision: this.client.getRevision({
                   ...this.request.entity,
                   revision: this.revision
               }),
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts:
##########
@@ -401,8 +460,144 @@ export class EditProcessor extends TabbedDialog {
         });
     }
 
+    hasBulletins(): boolean {
+        return this.request.entity.permissions.canRead && 
!this.nifiCommon.isEmpty(this.bulletins);
+    }
+
+    getBulletinsTipData(): BulletinsTipInput {
+        return {
+            bulletins: this.bulletins
+        };
+    }
+
+    getBulletinTooltipPosition(): ConnectedPosition {
+        return {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetX: -8,
+            offsetY: 8
+        };
+    }
+
+    getMostSevereBulletinLevel(): string | null {
+        // determine the most severe of the bulletins
+        const mostSevere = 
this.canvasUtils.getMostSevereBulletin(this.bulletins);
+        return mostSevere ? mostSevere.bulletin.level.toLowerCase() : null;
+    }
+
+    isStoppable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Running';
+    }
+
+    isInvalid(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Invalid';
+    }
+
+    isDisabled(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Disabled';
+    }
+
+    isRunnable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Stopped'
+        );
+    }
+
+    isDisableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) &&
+            (this.status.aggregateSnapshot.runStatus === 'Stopped' ||
+                this.status.aggregateSnapshot.runStatus === 'Invalid')
+        );
+    }
+
+    isEnableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Disabled'
+        );
+    }
+
+    private canOperate(): boolean {
+        return this.request.entity.permissions.canWrite || 
this.request.entity.operatePermissions?.canWrite;
+    }
+
+    stop(entity: any) {
+        this.stopComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    start(entity: any) {
+        this.startComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,

Review Comment:
   ```suggestion
               revision: this.client.getRevision({
                   ...this.request.entity,
                   revision: this.revision
               }),
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html:
##########
@@ -15,19 +15,89 @@
   ~ limitations under the License.
   -->
 
-<h2 mat-dialog-title>
-    <div class="flex justify-between items-baseline">
-        <div>
-            {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+<h2 id="edit-processor-header" mat-dialog-title>
+    <div class="flex justify-between items-center">
+        <div class="flex items-baseline">
+            <div class="mr-2">
+                {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+            </div>
+            |
+            <div class="ml-2 text-base">
+                {{ formatType() }}
+            </div>
         </div>
-        <div class="text-base">
-            {{ formatType(request.entity) }}
+        <div class="flex">
+            <div>
+                @if (isStoppable()) {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            <i class="mr-2 success-color-default fa 
fa-play"></i>Running<i
+                                class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                        </div>
+                    </button>
+                } @else if (isRunnable()) {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">

Review Comment:
   This button has focus when this dialog opens. I'm not sure that's 
problematic but I wanted to raise it here to ensure that is in fact what we 
want to do.
   
   ![Screenshot 2024-12-06 at 2 47 44 
PM](https://github.com/user-attachments/assets/0ad394b0-9e64-4de7-a4be-da0d83c2d4c7)
   



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts:
##########
@@ -253,6 +275,32 @@ export class EditProcessor extends TabbedDialog {
                 new FormControl({ value: this.runDurationMillis, disabled: 
this.readonly }, Validators.required)
             );
         }
+
+        this.initialize(request.entity);
+    }
+
+    initialize(entity: any) {

Review Comment:
   ```suggestion
       processRunStateUpdates(entity: any) {
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html:
##########
@@ -15,19 +15,89 @@
   ~ limitations under the License.
   -->
 
-<h2 mat-dialog-title>
-    <div class="flex justify-between items-baseline">
-        <div>
-            {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+<h2 id="edit-processor-header" mat-dialog-title>
+    <div class="flex justify-between items-center">
+        <div class="flex items-baseline">
+            <div class="mr-2">
+                {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+            </div>
+            |
+            <div class="ml-2 text-base">
+                {{ formatType() }}
+            </div>
         </div>
-        <div class="text-base">
-            {{ formatType(request.entity) }}
+        <div class="flex">
+            <div>
+                @if (isStoppable()) {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            <i class="mr-2 success-color-default fa 
fa-play"></i>Running<i
+                                class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                        </div>
+                    </button>
+                } @else if (isRunnable()) {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            <i class="mr-2 error-color-variant fa 
fa-stop"></i>Stopped<i
+                                class="ml-2 -mt-1 fa fa-sort-desc"></i>
+                        </div>
+                    </button>
+                } @else {
+                    <button type="button" mat-stroked-button 
[matMenuTriggerFor]="operateMenu">
+                        <div class="flex items-center">
+                            @if (isInvalid()) {
+                                <i class="mr-2 fa fa-warning 
caution-color"></i>
+                            } @else if (isDisabled()) {
+                                <i class="mr-2 icon icon-enable-false"></i>
+                            } @else {
+                                <i class="mr-2 fa fa-circle-o-notch fa-spin 
primary-color"></i>
+                            }
+                            {{ formatRunStatus() }}
+                        </div>
+                    </button>
+                }
+                <mat-menu #operateMenu="matMenu" xPosition="before">

Review Comment:
   We should do something when this menu is empty. For instance when the 
component is `Stopping`
   
   ![Screenshot 2024-12-06 at 2 48 13 
PM](https://github.com/user-attachments/assets/54bc8451-a590-4ec2-b4d8-3810d3303248)
   



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts:
##########
@@ -401,8 +460,144 @@ export class EditProcessor extends TabbedDialog {
         });
     }
 
+    hasBulletins(): boolean {
+        return this.request.entity.permissions.canRead && 
!this.nifiCommon.isEmpty(this.bulletins);
+    }
+
+    getBulletinsTipData(): BulletinsTipInput {
+        return {
+            bulletins: this.bulletins
+        };
+    }
+
+    getBulletinTooltipPosition(): ConnectedPosition {
+        return {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetX: -8,
+            offsetY: 8
+        };
+    }
+
+    getMostSevereBulletinLevel(): string | null {
+        // determine the most severe of the bulletins
+        const mostSevere = 
this.canvasUtils.getMostSevereBulletin(this.bulletins);
+        return mostSevere ? mostSevere.bulletin.level.toLowerCase() : null;
+    }
+
+    isStoppable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Running';
+    }
+
+    isInvalid(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Invalid';
+    }
+
+    isDisabled(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Disabled';
+    }
+
+    isRunnable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Stopped'
+        );
+    }
+
+    isDisableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) &&
+            (this.status.aggregateSnapshot.runStatus === 'Stopped' ||
+                this.status.aggregateSnapshot.runStatus === 'Invalid')
+        );
+    }
+
+    isEnableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Disabled'
+        );
+    }
+
+    private canOperate(): boolean {
+        return this.request.entity.permissions.canWrite || 
this.request.entity.operatePermissions?.canWrite;
+    }
+
+    stop(entity: any) {
+        this.stopComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    start(entity: any) {
+        this.startComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,
+            errorStrategy: 'snackbar'
+        });
+    }
+
+    disable(entity: any) {
+        this.disableComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,

Review Comment:
   ```suggestion
               revision: this.client.getRevision({
                   ...this.request.entity,
                   revision: this.revision
               }),
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts:
##########
@@ -401,8 +460,144 @@ export class EditProcessor extends TabbedDialog {
         });
     }
 
+    hasBulletins(): boolean {
+        return this.request.entity.permissions.canRead && 
!this.nifiCommon.isEmpty(this.bulletins);
+    }
+
+    getBulletinsTipData(): BulletinsTipInput {
+        return {
+            bulletins: this.bulletins
+        };
+    }
+
+    getBulletinTooltipPosition(): ConnectedPosition {
+        return {
+            originX: 'end',
+            originY: 'bottom',
+            overlayX: 'end',
+            overlayY: 'top',
+            offsetX: -8,
+            offsetY: 8
+        };
+    }
+
+    getMostSevereBulletinLevel(): string | null {
+        // determine the most severe of the bulletins
+        const mostSevere = 
this.canvasUtils.getMostSevereBulletin(this.bulletins);
+        return mostSevere ? mostSevere.bulletin.level.toLowerCase() : null;
+    }
+
+    isStoppable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Running';
+    }
+
+    isInvalid(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Invalid';
+    }
+
+    isDisabled(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return this.status.aggregateSnapshot.runStatus === 'Disabled';
+    }
+
+    isRunnable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Stopped'
+        );
+    }
+
+    isDisableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) &&
+            (this.status.aggregateSnapshot.runStatus === 'Stopped' ||
+                this.status.aggregateSnapshot.runStatus === 'Invalid')
+        );
+    }
+
+    isEnableable(): boolean {
+        if (!this.canOperate()) {
+            return false;
+        }
+
+        return (
+            !(
+                this.status.aggregateSnapshot.runStatus === 'Running' ||
+                this.status.aggregateSnapshot.activeThreadCount > 0
+            ) && this.status.aggregateSnapshot.runStatus === 'Disabled'
+        );
+    }
+
+    private canOperate(): boolean {
+        return this.request.entity.permissions.canWrite || 
this.request.entity.operatePermissions?.canWrite;
+    }
+
+    stop(entity: any) {
+        this.stopComponentRequest.next({
+            id: entity.id,
+            uri: entity.uri,
+            type: ComponentType.Processor,
+            revision: this.revision,

Review Comment:
   ```suggestion
               revision: this.client.getRevision({
                   ...this.request.entity,
                   revision: this.revision
               }),
   ```



-- 
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]

Reply via email to