This is an automated email from the ASF dual-hosted git repository.

pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 6d691f465a NIFI-15253 - Add additionalDetails with Setup instructions 
for Azure Git DevOps Flow Registry Client
6d691f465a is described below

commit 6d691f465ab1859b0f51c439cc484d18480b59fe
Author: Pierre Villard <[email protected]>
AuthorDate: Tue Nov 25 13:49:28 2025 +0100

    NIFI-15253 - Add additionalDetails with Setup instructions for Azure Git 
DevOps Flow Registry Client
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #10562.
---
 .../additionalDetails.md                           | 50 ++++++++++++++++++++++
 .../registry-client-table.component.html           |  8 +++-
 .../registry-client-table.component.spec.ts        | 10 +++++
 .../registry-client-table.component.ts             | 11 +++--
 .../registry-clients.component.html                |  1 +
 .../registry-clients.component.spec.ts             | 28 ++++++++++++
 .../registry-clients/registry-clients.component.ts | 22 ++++++++++
 .../component-state/component-state.component.html |  7 ++-
 8 files changed, 129 insertions(+), 8 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-azure-bundle/nifi-azure-registry-clients/src/main/resources/docs/org.apache.nifi.azure.devops.AzureDevOpsFlowRegistryClient/additionalDetails.md
 
b/nifi-extension-bundles/nifi-azure-bundle/nifi-azure-registry-clients/src/main/resources/docs/org.apache.nifi.azure.devops.AzureDevOpsFlowRegistryClient/additionalDetails.md
new file mode 100644
index 0000000000..a93750a18d
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-azure-bundle/nifi-azure-registry-clients/src/main/resources/docs/org.apache.nifi.azure.devops.AzureDevOpsFlowRegistryClient/additionalDetails.md
@@ -0,0 +1,50 @@
+<!--
+  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.
+-->
+
+# Azure DevOps Flow Registry Client – Service Principal Setup
+
+This component stores NiFi versioned flows in Azure DevOps Git using the Azure 
DevOps Git REST API. It authenticates with Microsoft Entra service principals 
through OAuth 2.0 client credentials.
+
+## Entra ID (Microsoft Entra) – Service Principal
+1. App registrations → New registration → name (e.g., `NiFi Git DevOps 
Registry Client`); single-tenant is fine; no redirect URI.
+2. Record `Directory (tenant) ID` and `Application (client) ID`.
+3. Certificates & secrets → New client secret. Note the provided **Value**.
+
+## Azure DevOps
+1. Organization must use the same tenant.
+2. Organization Settings → Users → Add users → paste the **Application 
(client) ID**; Access Level = **Basic**; add to the right projects/groups 
(e.g., Contributors). Do not send email.
+
+## NiFi Management Controller Services
+- **StandardWebClientServiceProvider**: default timeouts are usually fine; set 
proxy/SSL if needed. Enable.
+- **StandardOauth2AccessTokenProvider**:
+  - Token Endpoint (replace with value "Directory (tenant) ID"): 
`https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token`
+  - Grant Type: `Client Credentials`
+  - Client ID: Application (client) ID
+  - Client Secret: value from the created client secret
+  - Scope: `https://app.vssps.visualstudio.com/.default`
+  - Configure proxy/SSL if required.
+  - Optional: Verify to confirm a token can be obtained
+  - Enable
+
+## NiFi Flow Registry Client
+- Type: `AzureDevOpsFlowRegistryClient`
+- Azure DevOps API URL: `https://dev.azure.com`
+- Organization, Project, Repository Name
+- Default Branch: must already exist (e.g., `main`).
+- Repository Path: optional subfolder; **no leading or trailing `/`**.
+- Authentication Strategy: `Service Principal`.
+- OAuth2 Access Token Provider: choose the configured 
`StandardOauth2AccessTokenProvider`.
+- Web Client Service: choose the `StandardWebClientServiceProvider`.
+- Optionally Verify to confirm read/write.
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.html
index 3bd6600d04..e7dd1c3bc3 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.html
@@ -101,7 +101,7 @@
                 <th mat-header-cell *matHeaderCellDef></th>
                 <td mat-cell *matCellDef="let item">
                     <div class="flex items-center justify-end gap-x-2">
-                        @if (canConfigure(item) || canDelete(item) || 
canClearBulletins(item)) {
+                        @if (canConfigure(item) || canDelete(item) || 
canClearBulletins(item) || canRead(item)) {
                             <button
                                 mat-icon-button
                                 type="button"
@@ -129,6 +129,12 @@
                                     Clear Bulletins
                                 </button>
                             }
+                            @if (canRead(item)) {
+                                <button mat-menu-item 
(click)="viewDocumentationClicked(item)">
+                                    <i class="fa fa-book primary-color 
mr-2"></i>
+                                    View Documentation
+                                </button>
+                            }
                         </mat-menu>
                     </div>
                 </td>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.spec.ts
index 147973219c..485cdbd224 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.spec.ts
@@ -440,5 +440,15 @@ describe('RegistryClientTable', () => {
 
             
expect(component.clearBulletinsRegistryClient.next).toHaveBeenCalledWith(mockEntity);
         });
+
+        it('should emit viewRegistryClientDocumentation when 
viewDocumentationClicked is called', async () => {
+            const { component } = await setup();
+            const mockEntity = createMockRegistryClientEntity();
+            jest.spyOn(component.viewRegistryClientDocumentation, 'next');
+
+            component.viewDocumentationClicked(mockEntity);
+
+            
expect(component.viewRegistryClientDocumentation.next).toHaveBeenCalledWith(mockEntity);
+        });
     });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.ts
index c57f56f0c6..1976ef2fbb 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-client-table/registry-client-table.component.ts
@@ -19,7 +19,6 @@ import { Component, EventEmitter, Input, Output, inject } 
from '@angular/core';
 import { MatTableDataSource, MatTableModule } from '@angular/material/table';
 import { MatSortModule, Sort } from '@angular/material/sort';
 import { NgClass } from '@angular/common';
-import { ReportingTaskEntity } from '../../../state/reporting-tasks';
 import { TextTip, NiFiCommon, NifiTooltipDirective } from '@nifi/shared';
 import { BulletinsTip } from 
'../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
 import { ValidationErrorsTip } from 
'../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
@@ -55,6 +54,8 @@ export class RegistryClientTable {
 
     @Output() selectRegistryClient: EventEmitter<RegistryClientEntity> = new 
EventEmitter<RegistryClientEntity>();
     @Output() configureRegistryClient: EventEmitter<RegistryClientEntity> = 
new EventEmitter<RegistryClientEntity>();
+    @Output() viewRegistryClientDocumentation: 
EventEmitter<RegistryClientEntity> =
+        new EventEmitter<RegistryClientEntity>();
     @Output() deleteRegistryClient: EventEmitter<RegistryClientEntity> = new 
EventEmitter<RegistryClientEntity>();
     @Output() clearBulletinsRegistryClient: EventEmitter<RegistryClientEntity> 
=
         new EventEmitter<RegistryClientEntity>();
@@ -167,11 +168,15 @@ export class RegistryClientTable {
         this.clearBulletinsRegistryClient.next(entity);
     }
 
-    select(entity: ReportingTaskEntity): void {
+    viewDocumentationClicked(entity: RegistryClientEntity): void {
+        this.viewRegistryClientDocumentation.next(entity);
+    }
+
+    select(entity: RegistryClientEntity): void {
         this.selectRegistryClient.next(entity);
     }
 
-    isSelected(entity: ReportingTaskEntity): boolean {
+    isSelected(entity: RegistryClientEntity): boolean {
         if (this.selectedRegistryClientId) {
             return entity.id == this.selectedRegistryClientId;
         }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.html
index be456a5b50..f036fa91a8 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.html
@@ -36,6 +36,7 @@
                         [registryClients]="registryClientState.registryClients"
                         (selectRegistryClient)="selectRegistryClient($event)"
                         
(configureRegistryClient)="configureRegistryClient($event)"
+                        
(viewRegistryClientDocumentation)="viewRegistryClientDocumentation($event)"
                         (deleteRegistryClient)="deleteRegistryClient($event)"
                         
(clearBulletinsRegistryClient)="clearBulletinsRegistryClient($event)"></registry-client-table>
                 </div>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.spec.ts
index 670df3b268..62a9d80afd 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.spec.ts
@@ -26,6 +26,7 @@ import { ComponentType } from '@nifi/shared';
 import * as RegistryClientsActions from 
'../../state/registry-clients/registry-clients.actions';
 import { currentUserFeatureKey } from '../../../../state/current-user';
 import * as fromCurrentUser from 
'../../../../state/current-user/current-user.reducer';
+import { navigateToComponentDocumentation } from 
'../../../../state/documentation/documentation.actions';
 
 describe('RegistryClients', () => {
     // Mock data factories
@@ -233,5 +234,32 @@ describe('RegistryClients', () => {
                 })
             );
         });
+
+        it('should dispatch navigateToComponentDocumentation action when 
viewRegistryClientDocumentation is called', async () => {
+            const { component, store } = await setup();
+            const mockRegistryClientEntity = createMockRegistryClientEntity();
+            jest.spyOn(store, 'dispatch');
+
+            
component.viewRegistryClientDocumentation(mockRegistryClientEntity);
+
+            expect(store.dispatch).toHaveBeenCalledWith(
+                navigateToComponentDocumentation({
+                    request: {
+                        backNavigation: {
+                            route: ['/settings', 'registry-clients', 
mockRegistryClientEntity.id],
+                            routeBoundary: ['/documentation'],
+                            context: 'Registry Client'
+                        },
+                        parameters: {
+                            componentType: ComponentType.FlowRegistryClient,
+                            type: mockRegistryClientEntity.component.type,
+                            group: 
mockRegistryClientEntity.component.bundle.group,
+                            artifact: 
mockRegistryClientEntity.component.bundle.artifact,
+                            version: 
mockRegistryClientEntity.component.bundle.version
+                        }
+                    }
+                })
+            );
+        });
     });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.ts
index 32bec3cf60..7591f73918 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/settings/ui/registry-clients/registry-clients.component.ts
@@ -45,6 +45,7 @@ import { NgxSkeletonLoaderComponent } from 
'ngx-skeleton-loader';
 import { MatIconButton } from '@angular/material/button';
 import { RegistryClientTable } from 
'./registry-client-table/registry-client-table.component';
 import { ComponentType, NiFiCommon } from '@nifi/shared';
+import { navigateToComponentDocumentation } from 
'../../../../state/documentation/documentation.actions';
 
 @Component({
     selector: 'registry-clients',
@@ -131,6 +132,27 @@ export class RegistryClients implements OnInit, OnDestroy {
         );
     }
 
+    viewRegistryClientDocumentation(entity: RegistryClientEntity): void {
+        this.store.dispatch(
+            navigateToComponentDocumentation({
+                request: {
+                    backNavigation: {
+                        route: ['/settings', 'registry-clients', entity.id],
+                        routeBoundary: ['/documentation'],
+                        context: 'Registry Client'
+                    },
+                    parameters: {
+                        componentType: ComponentType.FlowRegistryClient,
+                        type: entity.component.type,
+                        group: entity.component.bundle.group,
+                        artifact: entity.component.bundle.artifact,
+                        version: entity.component.bundle.version
+                    }
+                }
+            })
+        );
+    }
+
     clearBulletinsRegistryClient(entity: RegistryClientEntity): void {
         // Get the most recent bulletin timestamp from the entity's bulletins
         // This will be reconstructed from the time-only string to a full 
timestamp
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-state/component-state.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-state/component-state.component.html
index 29a379d79f..dc9c8fa8b3 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-state/component-state.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/ui/common/component-state/component-state.component.html
@@ -80,9 +80,7 @@
                     -->
                     <table mat-table class="hidden">
                         <tr mat-header-row *matHeaderRowDef="[]; sticky: 
true"></tr>
-                        <tr
-                            mat-row
-                            *matRowDef="let row; columns: []"></tr>
+                        <tr mat-row *matRowDef="let row; columns: []"></tr>
                     </table>
                 </div>
                 <div class="listing-table flex-1 relative">
@@ -115,7 +113,8 @@
                                             </th>
                                         }
                                         @if 
(displayedColumns.includes('actions')) {
-                                            <th class="mat-mdc-header-cell 
mdc-data-table__header-cell action-column"></th>
+                                            <th
+                                                class="mat-mdc-header-cell 
mdc-data-table__header-cell action-column"></th>
                                         }
                                     </tr>
                                 </thead>

Reply via email to