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>