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

mcgilman 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 1a4389e3c2d NIFI-15815 connector details page (#11254)
1a4389e3c2d is described below

commit 1a4389e3c2da24989de182c095ddf7b3408187e6
Author: Scott Aslan <[email protected]>
AuthorDate: Thu May 21 16:10:43 2026 -0400

    NIFI-15815 connector details page (#11254)
    
    * NIFI-15815 connector details page
    
    * address review feedback
    
    * full width details cards
    
    * address other text-base usage
---
 .../feature/connectors-routing.module.ts           |   5 +
 .../connectors-listing.effects.ts                  |   6 +-
 .../connector-configure.component.html             |   2 +-
 .../connector-configure.component.spec.ts          |  46 +-
 .../connector-configure.component.ts               |  16 +-
 .../connector-detail.component.html}               |  27 +-
 .../connector-detail.component.scss                |  16 +
 .../connector-detail.component.spec.ts             | 484 +++++++++++++++++++++
 .../connector-detail.component.ts}                 | 135 ++----
 .../connector-details-content.component.html       |  51 +++
 .../connector-details-content.component.scss       |  16 +
 .../connector-details-content.component.spec.ts    | 448 +++++++++++++++++++
 .../connector-details-content.component.ts         |  61 +++
 .../connector-configuration-step.component.html    |   4 +-
 .../connector-wizard.component.html                |   6 +-
 .../wizard-step-documentation-panel.component.html |   4 +-
 16 files changed, 1184 insertions(+), 143 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/feature/connectors-routing.module.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/feature/connectors-routing.module.ts
index 37c302814d5..e05aa5dc4df 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/feature/connectors-routing.module.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/feature/connectors-routing.module.ts
@@ -19,6 +19,7 @@ import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 import { Connectors } from './connectors.component';
 import { ConnectorConfigure } from 
'../ui/connector-configure/connector-configure.component';
+import { ConnectorDetail } from 
'../ui/connector-detail/connector-detail.component';
 
 const routes: Routes = [
     {
@@ -30,6 +31,10 @@ const routes: Routes = [
         path: ':id/configure',
         component: ConnectorConfigure
     },
+    {
+        path: ':id/detail',
+        component: ConnectorDetail
+    },
     {
         path: '',
         component: Connectors,
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connectors-listing/connectors-listing.effects.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connectors-listing/connectors-listing.effects.ts
index 2548206297f..e3c8d3b764b 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connectors-listing/connectors-listing.effects.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/state/connectors-listing/connectors-listing.effects.ts
@@ -449,10 +449,8 @@ export class ConnectorsListingEffects {
         () =>
             this.actions$.pipe(
                 ofType(navigateToViewConnectorDetails),
-                tap(() => {
-                    window.alert(
-                        'TODO: View connector details is not yet implemented. 
This feature will be implemented in a future iteration.'
-                    );
+                tap((action) => {
+                    this.router.navigate(['/connectors', action.id, 'detail']);
                 })
             ),
         { dispatch: false }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
index 20b0b3e70df..6b9f902d6fd 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
@@ -31,7 +31,7 @@
             <div class="flex-1 flex items-center justify-center" 
data-qa="error-container">
                 <div class="flex flex-col items-center gap-y-4">
                     <i class="fa fa-warning error-color text-4xl" 
data-qa="error-icon" aria-hidden="true"></i>
-                    <p class="text-base" data-qa="error-message">{{ 
errorMessage }}</p>
+                    <div data-qa="error-message">{{ errorMessage }}</div>
                     <button mat-flat-button 
(click)="returnToConnectorListing()" data-qa="return-button">
                         Return to Connectors
                     </button>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.spec.ts
index 05ac255f948..b47326d9430 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.spec.ts
@@ -154,7 +154,8 @@ describe('ConnectorConfigure', () => {
         };
 
         const mockConnectorMessageHost = {
-            startListening: vi.fn()
+            startListening: vi.fn(),
+            stopListening: vi.fn()
         };
 
         const mockClusterConnectionService = {
@@ -290,31 +291,37 @@ describe('ConnectorConfigure', () => {
             component = fixture.componentInstance;
         });
 
-        it('should sanitize and trust valid URLs', () => {
+        it('should trust valid http/https URLs', () => {
             const validUrl = 'http://localhost:4200/custom-config';
             const result = (component as unknown as { getFrameSource(url: 
string): unknown }).getFrameSource(validUrl);
 
             expect(result).toBeTruthy();
         });
 
-        it('should return null when sanitizer returns null', () => {
-            vi.spyOn(domSanitizer, 'sanitize').mockReturnValue(null);
+        it('should reject URLs with non-http/https schemes', () => {
+            const getFrameSource = (url: string) =>
+                (component as unknown as { getFrameSource(url: string): 
unknown }).getFrameSource(url);
 
-            const url = 'some-invalid-url';
-            const result = (component as unknown as { getFrameSource(url: 
string): unknown }).getFrameSource(url);
+            expect(getFrameSource('javascript:alert("xss")')).toBeNull();
+            expect(getFrameSource('data:text/html,<h1>hi</h1>')).toBeNull();
+            expect(getFrameSource('ftp://example.com/file')).toBeNull();
+        });
+
+        it('should reject malformed URLs', () => {
+            const result = (component as unknown as { getFrameSource(url: 
string): unknown }).getFrameSource(
+                'not-a-valid-url'
+            );
 
             expect(result).toBeNull();
         });
 
-        it('should use two-step sanitization process', () => {
-            const sanitizeSpy = vi.spyOn(domSanitizer, 'sanitize');
+        it('should bypass security trust for validated URLs', () => {
             const bypassSpy = vi.spyOn(domSanitizer, 
'bypassSecurityTrustResourceUrl');
 
             const url = 'http://localhost:4200/custom-config';
             (component as unknown as { getFrameSource(url: string): void 
}).getFrameSource(url);
 
-            expect(sanitizeSpy).toHaveBeenCalled();
-            expect(bypassSpy).toHaveBeenCalled();
+            
expect(bypassSpy).toHaveBeenCalledWith(expect.stringContaining(url));
         });
     });
 
@@ -519,5 +526,24 @@ describe('ConnectorConfigure', () => {
 
             expect(connectorMessageHost.startListening).not.toHaveBeenCalled();
         });
+
+        it('should stop listening before loading the next connector', () => {
+            
connectorConfigurationService.getConnector.mockReturnValue(of(mockConnectorWithCustomUrl));
+            fixture = TestBed.createComponent(ConnectorConfigure);
+            component = fixture.componentInstance;
+
+            component.ngOnInit();
+            
expect(connectorMessageHost.startListening).toHaveBeenCalledTimes(1);
+
+            const secondConnector: ConnectorEntity = {
+                ...mockConnectorWithCustomUrl,
+                id: 'test-connector-2'
+            };
+            
connectorConfigurationService.getConnector.mockReturnValue(of(secondConnector));
+            store.overrideSelector(selectConnectorIdFromRoute, 
'test-connector-2');
+            store.refreshState();
+
+            expect(connectorMessageHost.stopListening).toHaveBeenCalled();
+        });
     });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
index 9795fabc952..3e066f6e1e3 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { Component, DestroyRef, ElementRef, OnInit, SecurityContext, 
viewChild, inject } from '@angular/core';
+import { Component, DestroyRef, ElementRef, OnInit, viewChild, inject } from 
'@angular/core';
 import { HttpErrorResponse } from '@angular/common/http';
 import { Router } from '@angular/router';
 import { NiFiState } from '../../../../state';
@@ -119,6 +119,7 @@ export class ConnectorConfigure implements OnInit {
             .pipe(
                 filter((connectorId) => connectorId != null),
                 switchMap((connectorId) => {
+                    this.connectorMessageHost.stopListening();
                     this.loading = true;
                     this.frameSource = null;
                     this.connector = null;
@@ -189,13 +190,16 @@ export class ConnectorConfigure implements OnInit {
         const connectorId = this.connector?.id;
         const urlWithParams = connectorId ? 
`${configurationUrl}?connectorId=${connectorId}` : configurationUrl;
 
-        const sanitizedUrl = this.domSanitizer.sanitize(SecurityContext.URL, 
urlWithParams);
-
-        if (sanitizedUrl) {
-            return 
this.domSanitizer.bypassSecurityTrustResourceUrl(sanitizedUrl);
+        try {
+            const parsed = new URL(urlWithParams);
+            if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+                return null;
+            }
+        } catch {
+            return null;
         }
 
-        return null;
+        return this.domSanitizer.bypassSecurityTrustResourceUrl(urlWithParams);
     }
 
     returnToConnectorListing(): void {
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.html
similarity index 71%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.html
index 20b0b3e70df..dab439fb2ec 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.html
@@ -19,47 +19,48 @@
     <header class="nifi-header">
         <navigation></navigation>
     </header>
-    <context-error-banner class="wizard-context-banner" 
[context]="ErrorContextKey.CONNECTORS"></context-error-banner>
-    <div class="flex flex-1 overflow-hidden">
+    @if (!backNavigation() && !loading && !errorMessage) {
+        <div class="pl-5 pt-2">
+            <a (click)="returnToConnectorListing()" 
data-qa="fallback-back-link">
+                <i class="fa fa-arrow-left mr-2" 
aria-hidden="true"></i>Installed connectors
+            </a>
+        </div>
+    }
+    <context-error-banner 
[context]="ErrorContextKey.CONNECTORS"></context-error-banner>
+    <div class="flex flex-1 min-h-0 overflow-y-auto">
         @if (loading) {
-            <!-- Loading state -->
             <div class="flex-1 flex items-center justify-center">
                 <span *nifiSpinner="loading" data-qa="loading-spinner"></span>
             </div>
         } @else if (errorMessage) {
-            <!-- Error State (Permission Error, API Error, Not Found, etc.) -->
             <div class="flex-1 flex items-center justify-center" 
data-qa="error-container">
                 <div class="flex flex-col items-center gap-y-4">
                     <i class="fa fa-warning error-color text-4xl" 
data-qa="error-icon" aria-hidden="true"></i>
-                    <p class="text-base" data-qa="error-message">{{ 
errorMessage }}</p>
+                    <div data-qa="error-message">{{ errorMessage }}</div>
                     <button mat-flat-button 
(click)="returnToConnectorListing()" data-qa="return-button">
                         Return to Connectors
                     </button>
                 </div>
             </div>
         } @else if (frameSource) {
-            <!-- Custom configuration UI in iframe -->
             <!--
                 Sandbox flags:
                   allow-scripts        — run the embedded connector app
-                  allow-forms          — submit configuration forms
                   allow-same-origin    — postMessage, style injection, API 
calls
                   allow-popups         — open external doc links via 
target="_blank"
                   allow-popups-to-escape-sandbox — opened tabs run without 
sandbox restrictions
+                  (allow-forms omitted — read-only details view)
             -->
             <iframe
                 #iframeRef
                 class="flex-1 border-none"
                 [src]="frameSource"
-                [title]="'Custom configuration for ' + 
(connector?.component?.name || 'connector')"
-                sandbox="allow-scripts allow-forms allow-same-origin 
allow-popups allow-popups-to-escape-sandbox"
+                [title]="'Custom details for ' + (connector?.component?.name 
|| 'connector')"
+                sandbox="allow-scripts allow-same-origin allow-popups 
allow-popups-to-escape-sandbox"
                 referrerpolicy="strict-origin-when-cross-origin"
                 
(load)="systemTokensService.appendStyleSheet(iframeRef)"></iframe>
         } @else if (connector) {
-            <!-- Generic configuration wizard -->
-            <connector-wizard [connector]="connector" 
(navigateBack)="returnToConnectorListing()">
-                <div wizardHeaderContent>{{ connector.component.name }}</div>
-            </connector-wizard>
+            <connector-details-content class="w-full h-full" 
[connector]="connector"></connector-details-content>
         }
     </div>
 </div>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.scss
new file mode 100644
index 00000000000..2944f981947
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.scss
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.spec.ts
new file mode 100644
index 00000000000..9b2a7852824
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.spec.ts
@@ -0,0 +1,484 @@
+/*
+ * 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 { TestBed } from '@angular/core/testing';
+import { ConnectorDetail } from './connector-detail.component';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { SystemTokensService, ConnectorEntity, ConnectorConfigurationService } 
from '@nifi/shared';
+import { MockComponent } from 'ng-mocks';
+import { Navigation } from 
'../../../../ui/common/navigation/navigation.component';
+import { ConnectorDetailsContent } from 
'../connector-details-content/connector-details-content.component';
+import { of, throwError } from 'rxjs';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { provideRouter, Router } from '@angular/router';
+import { selectConnectorIdFromRoute } from 
'../../state/connectors-listing/connectors-listing.selectors';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ErrorHelper } from '../../../../service/error-helper.service';
+import { errorFeatureKey } from '../../../../state/error';
+import { initialState as errorInitialState } from 
'../../../../state/error/error.reducer';
+import { selectBackNavigation } from 
'../../../../state/navigation/navigation.selectors';
+import { ConnectorMessageHost } from 
'../../service/connector-message-host.service';
+import { currentUserFeatureKey } from '../../../../state/current-user';
+import * as fromCurrentUser from 
'../../../../state/current-user/current-user.reducer';
+import { navigationFeatureKey } from '../../../../state/navigation';
+import { initialState as navigationInitialState } from 
'../../../../state/navigation/navigation.reducer';
+import { flowConfigurationFeatureKey } from 
'../../../../state/flow-configuration';
+import * as fromFlowConfiguration from 
'../../../../state/flow-configuration/flow-configuration.reducer';
+import { clusterSummaryFeatureKey } from '../../../../state/cluster-summary';
+import { initialState as clusterSummaryInitialState } from 
'../../../../state/cluster-summary/cluster-summary.reducer';
+import { loginConfigurationFeatureKey } from 
'../../../../state/login-configuration';
+import { initialState as loginConfigurationInitialState } from 
'../../../../state/login-configuration/login-configuration.reducer';
+import { aboutFeatureKey } from '../../../../state/about';
+import { initialState as aboutInitialState } from 
'../../../../state/about/about.reducer';
+import { MatIconTestingModule } from '@angular/material/icon/testing';
+import type { Mocked } from 'vitest';
+
+describe('ConnectorDetail', () => {
+    interface SetupOptions {
+        connectorId?: string;
+        connectorResponse?: ConnectorEntity;
+        errorResponse?: HttpErrorResponse;
+        errorMessage?: string;
+        backNavigation?: { route: string[]; routeBoundary: string[]; context: 
string } | null;
+    }
+
+    function createMockConnector(overrides: Partial<ConnectorEntity> = {}): 
ConnectorEntity {
+        return {
+            id: 'test-connector-1',
+            uri: 'http://localhost:4200/nifi-api/connectors/test-connector-1',
+            permissions: { canRead: true, canWrite: true },
+            bulletins: [],
+            status: {
+                runStatus: 'RUNNING',
+                validationStatus: 'VALID'
+            },
+            component: {
+                id: 'test-connector-1',
+                name: 'Test Connector',
+                type: 'TestConnector',
+                state: 'RUNNING',
+                bundle: {
+                    group: 'org.apache.nifi',
+                    artifact: 'test-connector',
+                    version: '1.0.0'
+                },
+                managedProcessGroupId: 'pg-1',
+                availableActions: [
+                    { name: 'START', description: 'Start action', allowed: 
true },
+                    { name: 'STOP', description: 'Stop action', allowed: true 
},
+                    { name: 'CONFIGURE', description: 'Configure action', 
allowed: true },
+                    { name: 'DELETE', description: 'Delete action', allowed: 
true }
+                ]
+            },
+            revision: {
+                version: 1
+            },
+            ...overrides
+        };
+    }
+
+    async function setup(options: SetupOptions = {}) {
+        const {
+            connectorId = 'test-connector-1',
+            connectorResponse = createMockConnector(),
+            errorResponse,
+            errorMessage = 'Connector not found'
+        } = options;
+
+        const mockConnectorConfigurationService = {
+            getConnector: vi.fn()
+        };
+
+        const mockErrorHelper = {
+            getErrorString: vi.fn().mockReturnValue(errorMessage)
+        };
+
+        const mockConnectorMessageHost = {
+            startListening: vi.fn(),
+            stopListening: vi.fn()
+        };
+
+        await TestBed.configureTestingModule({
+            imports: [
+                ConnectorDetail,
+                MockComponent(Navigation),
+                MockComponent(ConnectorDetailsContent),
+                MatIconTestingModule
+            ],
+            providers: [
+                provideRouter([]),
+                provideMockStore({
+                    initialState: {
+                        [currentUserFeatureKey]: fromCurrentUser.initialState,
+                        [navigationFeatureKey]: navigationInitialState,
+                        [flowConfigurationFeatureKey]: 
fromFlowConfiguration.initialState,
+                        [errorFeatureKey]: errorInitialState,
+                        [clusterSummaryFeatureKey]: clusterSummaryInitialState,
+                        [loginConfigurationFeatureKey]: 
loginConfigurationInitialState,
+                        [aboutFeatureKey]: aboutInitialState
+                    },
+                    selectors: [
+                        { selector: selectConnectorIdFromRoute, value: 
connectorId },
+                        { selector: selectBackNavigation, value: 
options.backNavigation ?? null }
+                    ]
+                }),
+                {
+                    provide: SystemTokensService,
+                    useValue: {
+                        appendStyleSheet: vi.fn()
+                    }
+                },
+                {
+                    provide: ConnectorConfigurationService,
+                    useValue: mockConnectorConfigurationService
+                },
+                {
+                    provide: ErrorHelper,
+                    useValue: mockErrorHelper
+                },
+                {
+                    provide: ConnectorMessageHost,
+                    useValue: mockConnectorMessageHost
+                }
+            ],
+            schemas: [NO_ERRORS_SCHEMA]
+        }).compileComponents();
+
+        const store = TestBed.inject(MockStore);
+        const router = TestBed.inject(Router);
+        const errorHelper = TestBed.inject(ErrorHelper) as Mocked<ErrorHelper>;
+
+        if (errorResponse) {
+            
mockConnectorConfigurationService.getConnector.mockReturnValue(throwError(() => 
errorResponse));
+        } else {
+            
mockConnectorConfigurationService.getConnector.mockReturnValue(of(connectorResponse));
+        }
+
+        const fixture = TestBed.createComponent(ConnectorDetail);
+        const component = fixture.componentInstance;
+        const routerNavigateSpy = vi.spyOn(router, 'navigate');
+        const connectorConfigurationService = TestBed.inject(
+            ConnectorConfigurationService
+        ) as Mocked<ConnectorConfigurationService>;
+        const connectorMessageHost = TestBed.inject(ConnectorMessageHost) as 
Mocked<ConnectorMessageHost>;
+
+        return {
+            fixture,
+            component,
+            store,
+            router,
+            connectorConfigurationService,
+            errorHelper,
+            routerNavigateSpy,
+            connectorMessageHost
+        };
+    }
+
+    beforeEach(() => {
+        vi.clearAllMocks();
+    });
+
+    describe('Component initialization', () => {
+        it('should create', async () => {
+            const { component, fixture } = await setup();
+            fixture.detectChanges();
+
+            expect(component).toBeTruthy();
+        });
+
+        it('should set loading to true initially', async () => {
+            const { component } = await setup();
+
+            expect(component.loading).toBe(true);
+        });
+
+        it('should set loading to false after connector loads', async () => {
+            const { component } = await setup();
+            component.ngOnInit();
+
+            expect(component.loading).toBe(false);
+        });
+    });
+
+    describe('Custom details URL rendering', () => {
+        it('should set frameSource with sanitized URL and connectorId when 
detailsUrl is present', async () => {
+            const connectorWithDetailsUrl = createMockConnector({
+                component: {
+                    ...createMockConnector().component,
+                    detailsUrl: 'http://localhost:4200/custom-details'
+                }
+            });
+
+            const { component } = await setup({ connectorResponse: 
connectorWithDetailsUrl });
+            component.ngOnInit();
+
+            expect(component.frameSource).toBeTruthy();
+            expect((component.frameSource as 
any).changingThisBreaksApplicationSecurity).toContain(
+                
'http://localhost:4200/custom-details?connectorId=test-connector-1'
+            );
+            expect(component.connector).toEqual(connectorWithDetailsUrl);
+            expect(component.loading).toBe(false);
+        });
+
+        it('should set frameSource to null when detailsUrl is absent', async 
() => {
+            const { component } = await setup();
+            component.ngOnInit();
+
+            expect(component.frameSource).toBeNull();
+            expect(component.loading).toBe(false);
+        });
+    });
+
+    describe('URL sanitization', () => {
+        it('should sanitize and trust valid URLs with connectorId appended', 
async () => {
+            const connectorWithDetailsUrl = createMockConnector({
+                component: {
+                    ...createMockConnector().component,
+                    detailsUrl: 'http://localhost:4200/custom-details'
+                }
+            });
+
+            const { component } = await setup({ connectorResponse: 
connectorWithDetailsUrl });
+            component.ngOnInit();
+
+            expect(component.frameSource).toBeTruthy();
+            expect((component.frameSource as 
any).changingThisBreaksApplicationSecurity).toContain(
+                
'http://localhost:4200/custom-details?connectorId=test-connector-1'
+            );
+        });
+
+        it('should reject URLs with non-http/https schemes', async () => {
+            const { component } = await setup();
+            component.connector = createMockConnector();
+
+            expect((component as 
any).getFrameSource('javascript:alert("xss")')).toBeNull();
+            expect((component as 
any).getFrameSource('data:text/html,<h1>hi</h1>')).toBeNull();
+            expect((component as 
any).getFrameSource('ftp://example.com/file')).toBeNull();
+        });
+
+        it('should reject malformed URLs', async () => {
+            const { component } = await setup();
+            component.connector = createMockConnector();
+
+            expect((component as 
any).getFrameSource('not-a-valid-url')).toBeNull();
+        });
+    });
+
+    describe('Permission handling', () => {
+        it('should set errorMessage when canRead is false', async () => {
+            const connectorWithoutReadPermission = createMockConnector({
+                permissions: { canRead: false, canWrite: false }
+            });
+
+            const { component } = await setup({ connectorResponse: 
connectorWithoutReadPermission });
+            component.ngOnInit();
+
+            expect(component.errorMessage).toBe('Insufficient permissions to 
view this connector.');
+            expect(component.frameSource).toBeNull();
+        });
+
+        it('should not set errorMessage when canRead is true', async () => {
+            const { component } = await setup();
+            component.ngOnInit();
+
+            expect(component.errorMessage).toBeNull();
+        });
+    });
+
+    describe('Error handling', () => {
+        it('should set errorMessage on fetch error', async () => {
+            const errorResponse = new HttpErrorResponse({
+                error: 'Connector not found',
+                status: 404,
+                statusText: 'Not Found'
+            });
+            const errorMessage = 'Connector not found';
+
+            const { component, errorHelper } = await setup({
+                errorResponse,
+                errorMessage
+            });
+
+            component.ngOnInit();
+
+            
expect(errorHelper.getErrorString).toHaveBeenCalledWith(errorResponse);
+            expect(component.errorMessage).toBe(errorMessage);
+            expect(component.loading).toBe(false);
+        });
+    });
+
+    describe('Route parameter changes', () => {
+        it('should reload connector when route parameter changes', async () => 
{
+            const firstConnector = createMockConnector();
+            const secondConnector = createMockConnector({
+                id: 'test-connector-2',
+                component: {
+                    ...createMockConnector().component,
+                    detailsUrl: 'http://localhost:4200/custom-details'
+                }
+            });
+
+            const { component, connectorConfigurationService, store } = await 
setup({
+                connectorResponse: firstConnector
+            });
+
+            component.ngOnInit();
+            expect(component.connector).toEqual(firstConnector);
+
+            
connectorConfigurationService.getConnector.mockReturnValue(of(secondConnector));
+            store.overrideSelector(selectConnectorIdFromRoute, 
'test-connector-2');
+            store.refreshState();
+
+            
expect(connectorConfigurationService.getConnector).toHaveBeenCalledTimes(2);
+        });
+
+        it('should stop listening before loading the next connector', async () 
=> {
+            const firstConnector = createMockConnector({
+                component: {
+                    ...createMockConnector().component,
+                    detailsUrl: 'http://localhost:4200/custom-details'
+                }
+            });
+            const secondConnector = createMockConnector({ id: 
'test-connector-2' });
+
+            const { component, connectorConfigurationService, 
connectorMessageHost, store } = await setup({
+                connectorResponse: firstConnector
+            });
+
+            component.ngOnInit();
+            
expect(connectorMessageHost.startListening).toHaveBeenCalledTimes(1);
+
+            
connectorConfigurationService.getConnector.mockReturnValue(of(secondConnector));
+            store.overrideSelector(selectConnectorIdFromRoute, 
'test-connector-2');
+            store.refreshState();
+
+            expect(connectorMessageHost.stopListening).toHaveBeenCalled();
+        });
+    });
+
+    describe('navigation', () => {
+        it('should navigate to connector listing when returnToConnectorListing 
is called', async () => {
+            const { component, routerNavigateSpy } = await setup();
+            component.ngOnInit();
+
+            component.returnToConnectorListing();
+
+            expect(routerNavigateSpy).toHaveBeenCalledWith(['/connectors']);
+        });
+    });
+
+    describe('fallback back link', () => {
+        it('should show fallback back link when no backNavigation, not 
loading, and no error', async () => {
+            const { fixture } = await setup({ backNavigation: null });
+            fixture.detectChanges();
+
+            const link = 
fixture.nativeElement.querySelector('[data-qa="fallback-back-link"]');
+            expect(link).toBeTruthy();
+            expect(link.textContent).toContain('Installed connectors');
+        });
+
+        it('should navigate to connector listing when fallback back link is 
clicked', async () => {
+            const { fixture, routerNavigateSpy } = await setup({ 
backNavigation: null });
+            fixture.detectChanges();
+
+            const link = 
fixture.nativeElement.querySelector('[data-qa="fallback-back-link"]');
+            link.click();
+
+            expect(routerNavigateSpy).toHaveBeenCalledWith(['/connectors']);
+        });
+
+        it('should hide fallback back link when backNavigation exists', async 
() => {
+            const { fixture } = await setup({
+                backNavigation: {
+                    route: ['/connectors', 'test-connector-1'],
+                    routeBoundary: ['/connectors', 'test-connector-1', 
'detail'],
+                    context: 'connectors'
+                }
+            });
+            fixture.detectChanges();
+
+            const link = 
fixture.nativeElement.querySelector('[data-qa="fallback-back-link"]');
+            expect(link).toBeNull();
+        });
+
+        it('should hide fallback back link while loading', async () => {
+            const { component, fixture } = await setup({ backNavigation: null, 
connectorId: null as any });
+            fixture.detectChanges();
+
+            expect(component.loading).toBe(true);
+            const link = 
fixture.nativeElement.querySelector('[data-qa="fallback-back-link"]');
+            expect(link).toBeNull();
+        });
+
+        it('should hide fallback back link when there is an error', async () 
=> {
+            const errorResponse = new HttpErrorResponse({
+                error: 'Not found',
+                status: 404,
+                statusText: 'Not Found'
+            });
+            const { fixture } = await setup({ backNavigation: null, 
errorResponse });
+            fixture.detectChanges();
+
+            const link = 
fixture.nativeElement.querySelector('[data-qa="fallback-back-link"]');
+            expect(link).toBeNull();
+        });
+    });
+
+    describe('postMessage host', () => {
+        it('should start listening when connector has custom details URL', 
async () => {
+            const connectorWithDetailsUrl = createMockConnector({
+                component: {
+                    ...createMockConnector().component,
+                    detailsUrl: 'http://localhost:4200/custom-details'
+                }
+            });
+
+            const { component, connectorMessageHost } = await setup({
+                connectorResponse: connectorWithDetailsUrl
+            });
+            component.ngOnInit();
+
+            
expect(connectorMessageHost.startListening).toHaveBeenCalledTimes(1);
+            expect(connectorMessageHost.startListening).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    expectedOrigin: 'http://localhost:4200',
+                    iframeElement: expect.any(Function)
+                })
+            );
+        });
+
+        it('should not start listening when no custom details URL', async () 
=> {
+            const { component, connectorMessageHost } = await setup();
+            component.ngOnInit();
+
+            expect(connectorMessageHost.startListening).not.toHaveBeenCalled();
+        });
+
+        it('should not start listening when connector lacks permissions', 
async () => {
+            const noPermissionsConnector = createMockConnector({
+                permissions: { canRead: false, canWrite: false }
+            });
+
+            const { component, connectorMessageHost } = await setup({
+                connectorResponse: noPermissionsConnector
+            });
+            component.ngOnInit();
+
+            expect(connectorMessageHost.startListening).not.toHaveBeenCalled();
+        });
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.ts
similarity index 51%
copy from 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
copy to 
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.ts
index 9795fabc952..89ac1e1de69 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-configure/connector-configure.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-detail/connector-detail.component.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { Component, DestroyRef, ElementRef, OnInit, SecurityContext, 
viewChild, inject } from '@angular/core';
+import { Component, DestroyRef, ElementRef, OnInit, viewChild, inject } from 
'@angular/core';
 import { HttpErrorResponse } from '@angular/common/http';
 import { Router } from '@angular/router';
 import { NiFiState } from '../../../../state';
@@ -24,106 +24,60 @@ import { DomSanitizer, SafeResourceUrl } from 
'@angular/platform-browser';
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { Navigation } from 
'../../../../ui/common/navigation/navigation.component';
 import {
-    CONNECTOR_MESSAGE_NAMESPACE,
-    ConnectorConfigurationService,
-    ConnectorEntity,
-    ConnectorWizardConfig,
-    ConnectorWizard,
-    StandardConnectorWizardStore,
-    ConnectorWizardStore,
-    CONNECTOR_WIZARD_CONFIG,
-    ParentToConnectorMessage,
     SystemTokensService,
+    ConnectorEntity,
+    ConnectorConfigurationService,
     NifiSpinnerDirective
 } from '@nifi/shared';
-import { catchError, distinctUntilChanged, filter, switchMap } from 
'rxjs/operators';
-import { of } from 'rxjs';
+import { ConnectorDetailsContent } from 
'../connector-details-content/connector-details-content.component';
+import { catchError, filter, switchMap } from 'rxjs/operators';
 import { selectConnectorIdFromRoute } from 
'../../state/connectors-listing/connectors-listing.selectors';
-import { selectDisconnectionAcknowledged } from 
'../../../../state/cluster-summary/cluster-summary.selectors';
-import { ClusterConnectionService } from 
'../../../../service/cluster-connection.service';
 import { MatButton } from '@angular/material/button';
 import { ErrorHelper } from '../../../../service/error-helper.service';
 import { ContextErrorBanner } from 
'../../../../ui/common/context-error-banner/context-error-banner.component';
 import { ErrorContextKey } from '../../../../state/error';
+import { selectBackNavigation } from 
'../../../../state/navigation/navigation.selectors';
+import { of } from 'rxjs';
 import { ConnectorMessageHost } from 
'../../service/connector-message-host.service';
 
-function connectorWizardConfigFactory(): ConnectorWizardConfig {
-    const router = inject(Router);
-    const clusterConnectionService = inject(ClusterConnectionService);
-    return {
-        getDisconnectedNodeAcknowledged: () => 
clusterConnectionService.isDisconnectionAcknowledged(),
-        onApplySuccess: (connectorId) => {
-            if (connectorId) {
-                router.navigate(['/connectors', connectorId]);
-            } else {
-                router.navigate(['/connectors']);
-            }
-        },
-        onNavigateBack: (connectorId: string | null) => {
-            if (connectorId) {
-                router.navigate(['/connectors', connectorId]);
-            } else {
-                router.navigate(['/connectors']);
-            }
-        }
-    };
-}
-
 @Component({
-    selector: 'connector-configure',
-    standalone: true,
-    imports: [Navigation, ConnectorWizard, MatButton, ContextErrorBanner, 
NifiSpinnerDirective],
-    templateUrl: './connector-configure.component.html',
-    styleUrls: ['./connector-configure.component.scss'],
-    providers: [
-        StandardConnectorWizardStore,
-        { provide: ConnectorWizardStore, useExisting: 
StandardConnectorWizardStore },
-        { provide: CONNECTOR_WIZARD_CONFIG, useFactory: 
connectorWizardConfigFactory }
-    ],
-    host: {
-        class: 'block h-full'
-    }
+    selector: 'connector-detail',
+    imports: [Navigation, ConnectorDetailsContent, MatButton, 
NifiSpinnerDirective, ContextErrorBanner],
+    templateUrl: './connector-detail.component.html',
+    styleUrls: ['./connector-detail.component.scss']
 })
-export class ConnectorConfigure implements OnInit {
+export class ConnectorDetail implements OnInit {
     private store = inject<Store<NiFiState>>(Store);
     private router = inject(Router);
     private domSanitizer = inject(DomSanitizer);
     private connectorConfigurationService = 
inject(ConnectorConfigurationService);
-    private clusterConnectionService = inject(ClusterConnectionService);
-    private errorHelper = inject(ErrorHelper);
     private destroyRef = inject(DestroyRef);
+    private errorHelper = inject(ErrorHelper);
     protected systemTokensService = inject(SystemTokensService);
     private connectorMessageHost = inject(ConnectorMessageHost);
 
     readonly iframeRef = viewChild<ElementRef<HTMLIFrameElement>>('iframeRef');
 
+    backNavigation = this.store.selectSignal(selectBackNavigation);
+    connectorIdFromRoute = this.store.selectSignal(selectConnectorIdFromRoute);
     frameSource: SafeResourceUrl | null = null;
     connector: ConnectorEntity | null = null;
     loading = true;
     errorMessage: string | null = null;
 
-    private childConnectorUiReady = false;
+    protected readonly ErrorContextKey = ErrorContextKey;
 
     ngOnInit(): void {
-        this.store
-            .select(selectDisconnectionAcknowledged)
-            .pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
-            .subscribe(() => {
-                if (this.childConnectorUiReady && 
this.connector?.component?.configurationUrl) {
-                    this.postDisconnectedNodeAcknowledgmentToChild();
-                }
-            });
-
         this.store
             .select(selectConnectorIdFromRoute)
             .pipe(
                 filter((connectorId) => connectorId != null),
                 switchMap((connectorId) => {
+                    this.connectorMessageHost.stopListening();
                     this.loading = true;
                     this.frameSource = null;
                     this.connector = null;
                     this.errorMessage = null;
-                    this.childConnectorUiReady = false;
 
                     return 
this.connectorConfigurationService.getConnector(connectorId!).pipe(
                         catchError((errorResponse: HttpErrorResponse) => {
@@ -144,63 +98,40 @@ export class ConnectorConfigure implements OnInit {
                 this.loading = false;
                 this.errorMessage = null;
 
-                if (!connector.permissions.canRead || 
!connector.permissions.canWrite) {
-                    this.errorMessage = 'Insufficient permissions to configure 
this connector.';
+                if (!connector.permissions.canRead) {
+                    this.errorMessage = 'Insufficient permissions to view this 
connector.';
                     return;
                 }
 
-                if (connector.component?.configurationUrl) {
-                    this.frameSource = 
this.getFrameSource(connector.component.configurationUrl);
+                if (connector.component?.detailsUrl) {
+                    this.frameSource = 
this.getFrameSource(connector.component.detailsUrl);
 
                     this.connectorMessageHost.startListening({
                         destroyRef: this.destroyRef,
-                        expectedOrigin: 
ConnectorMessageHost.extractOrigin(connector.component.configurationUrl),
-                        iframeElement: () => this.iframeRef()?.nativeElement,
-                        onConnectorUiReady: () => {
-                            this.childConnectorUiReady = true;
-                            this.postDisconnectedNodeAcknowledgmentToChild();
-                        }
+                        expectedOrigin: 
ConnectorMessageHost.extractOrigin(connector.component.detailsUrl),
+                        iframeElement: () => this.iframeRef()?.nativeElement
                     });
                 }
             });
     }
 
-    private postDisconnectedNodeAcknowledgmentToChild(): void {
-        const iframe = this.iframeRef()?.nativeElement;
-        const configurationUrl = this.connector?.component?.configurationUrl;
-        if (!iframe?.contentWindow || !configurationUrl) {
-            return;
-        }
-        const targetOrigin = 
ConnectorMessageHost.extractOrigin(configurationUrl);
-        if (!targetOrigin) {
-            return;
-        }
-        const message: ParentToConnectorMessage = {
-            namespace: CONNECTOR_MESSAGE_NAMESPACE,
-            type: 'disconnected-node-acknowledgment',
-            payload: {
-                disconnectedNodeAcknowledged: 
this.clusterConnectionService.isDisconnectionAcknowledged()
-            }
-        };
-        iframe.contentWindow.postMessage(message, targetOrigin);
-    }
-
-    private getFrameSource(configurationUrl: string): SafeResourceUrl | null {
+    private getFrameSource(detailsUrl: string): SafeResourceUrl | null {
         const connectorId = this.connector?.id;
-        const urlWithParams = connectorId ? 
`${configurationUrl}?connectorId=${connectorId}` : configurationUrl;
-
-        const sanitizedUrl = this.domSanitizer.sanitize(SecurityContext.URL, 
urlWithParams);
+        const urlWithParams = connectorId ? 
`${detailsUrl}?connectorId=${connectorId}` : detailsUrl;
 
-        if (sanitizedUrl) {
-            return 
this.domSanitizer.bypassSecurityTrustResourceUrl(sanitizedUrl);
+        try {
+            const parsed = new URL(urlWithParams);
+            if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+                return null;
+            }
+        } catch {
+            return null;
         }
 
-        return null;
+        return this.domSanitizer.bypassSecurityTrustResourceUrl(urlWithParams);
     }
 
     returnToConnectorListing(): void {
         this.router.navigate(['/connectors']);
     }
-
-    protected readonly ErrorContextKey = ErrorContextKey;
 }
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.html
new file mode 100644
index 00000000000..fb913959036
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.html
@@ -0,0 +1,51 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="flex flex-col gap-y-6 px-5 pt-2 pb-5">
+    <!-- Header Section -->
+    <connector-detail-header 
[connector]="connector()"></connector-detail-header>
+
+    <!-- Configuration Sections -->
+    @if (connector().component.activeConfiguration) {
+        @if (filteredStepConfigurations().length > 0) {
+            @for (stepConfig of filteredStepConfigurations(); track 
stepConfig.configurationStepName) {
+                <section class="flex flex-col gap-y-4" data-qa="step-section">
+                    <div class="text-xl font-bold" data-qa="step-name">
+                        {{ stepConfig.configurationStepName }}
+                    </div>
+
+                    @for (group of stepConfig.propertyGroupConfigurations; 
track group.propertyGroupName) {
+                        <property-group-card
+                            [propertyGroup]="group"
+                            
[hideGroupName]="stepConfig.propertyGroupConfigurations.length === 1">
+                        </property-group-card>
+                    }
+                </section>
+            }
+        } @else {
+            <div class="flex flex-col items-center justify-center p-8 gap-y-4 
tertiary-color" data-qa="no-config-state">
+                <i class="fa fa-info-circle" aria-hidden="true"></i>
+                <p>No configuration steps available for this connector.</p>
+            </div>
+        }
+    } @else {
+        <div class="flex flex-col items-center justify-center p-8 gap-y-4 
tertiary-color" data-qa="empty-state">
+            <i class="fa fa-info-circle" aria-hidden="true"></i>
+            <p>No configuration data available.</p>
+        </div>
+    }
+</div>
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.scss
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.scss
new file mode 100644
index 00000000000..2944f981947
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.scss
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.spec.ts
new file mode 100644
index 00000000000..c2f1c6ade15
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.spec.ts
@@ -0,0 +1,448 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ConnectorDetailsContent } from 
'./connector-details-content.component';
+import { ConnectorEntity, ConnectorState } from '@nifi/shared';
+import { MatIconTestingModule } from '@angular/material/icon/testing';
+import { provideMockStore } from '@ngrx/store/testing';
+import { By } from '@angular/platform-browser';
+
+describe('ConnectorDetailsContent', () => {
+    let component: ConnectorDetailsContent;
+    let fixture: ComponentFixture<ConnectorDetailsContent>;
+
+    const mockConnector: ConnectorEntity = {
+        id: 'test-connector-id',
+        uri: 'http://localhost:4200/nifi-api/connectors/test-connector-id',
+        permissions: {
+            canRead: true,
+            canWrite: true
+        },
+        status: {
+            runStatus: 'RUNNING',
+            validationStatus: 'VALID'
+        },
+        component: {
+            id: 'test-connector-id',
+            name: 'Test Connector',
+            type: 'org.apache.nifi.connector.TestConnector',
+            state: ConnectorState.RUNNING,
+            bundle: {
+                group: 'org.apache.nifi',
+                artifact: 'nifi-test-connector',
+                version: '1.0.0'
+            },
+            activeConfiguration: {
+                configurationStepConfigurations: [
+                    {
+                        configurationStepName: 'Basic Configuration',
+                        dependencies: [],
+                        propertyGroupConfigurations: [
+                            {
+                                propertyGroupName: 'Connection Settings',
+                                propertyDescriptors: {
+                                    hostname: {
+                                        name: 'hostname',
+                                        description: 'The hostname to connect 
to',
+                                        type: 'STRING',
+                                        required: true
+                                    }
+                                },
+                                propertyValues: {
+                                    hostname: {
+                                        value: 'localhost',
+                                        valueType: 'STRING_LITERAL'
+                                    }
+                                }
+                            }
+                        ]
+                    }
+                ]
+            },
+            managedProcessGroupId: 'test-pg-id',
+            availableActions: [
+                { name: 'START', description: 'Start action', allowed: true },
+                { name: 'STOP', description: 'Stop action', allowed: true },
+                { name: 'CONFIGURE', description: 'Configure action', allowed: 
true },
+                { name: 'DELETE', description: 'Delete action', allowed: true }
+            ]
+        },
+        bulletins: [],
+        revision: {
+            version: 1
+        }
+    };
+
+    beforeEach(async () => {
+        await TestBed.configureTestingModule({
+            imports: [ConnectorDetailsContent, MatIconTestingModule],
+            providers: [provideMockStore()]
+        }).compileComponents();
+
+        fixture = TestBed.createComponent(ConnectorDetailsContent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create', () => {
+        fixture.componentRef.setInput('connector', mockConnector);
+        fixture.detectChanges();
+        expect(component).toBeTruthy();
+    });
+
+    describe('configuration sections', () => {
+        it('should display configuration step sections', () => {
+            fixture.componentRef.setInput('connector', mockConnector);
+            fixture.detectChanges();
+
+            const stepSection = 
fixture.nativeElement.querySelector('[data-qa="step-section"]');
+            expect(stepSection).toBeTruthy();
+        });
+
+        it('should display step name', () => {
+            fixture.componentRef.setInput('connector', mockConnector);
+            fixture.detectChanges();
+
+            const stepName = 
fixture.nativeElement.querySelector('[data-qa="step-name"]');
+            expect(stepName?.textContent?.trim()).toBe('Basic Configuration');
+        });
+
+        it('should show no-config-state when configuration steps are empty', 
() => {
+            const connectorWithEmptyConfig = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: {
+                        configurationStepConfigurations: []
+                    }
+                }
+            };
+            fixture.componentRef.setInput('connector', 
connectorWithEmptyConfig);
+            fixture.detectChanges();
+
+            const noConfigState = 
fixture.nativeElement.querySelector('[data-qa="no-config-state"]');
+            expect(noConfigState).toBeTruthy();
+        });
+
+        it('should show empty-state when no activeConfiguration is present', 
() => {
+            const connectorWithoutConfig = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: undefined
+                }
+            };
+            fixture.componentRef.setInput('connector', connectorWithoutConfig);
+            fixture.detectChanges();
+
+            const emptyState = 
fixture.nativeElement.querySelector('[data-qa="empty-state"]');
+            expect(emptyState).toBeTruthy();
+        });
+    });
+
+    describe('hideGroupName binding', () => {
+        it('should set hideGroupName to true when step has a single property 
group', () => {
+            fixture.componentRef.setInput('connector', mockConnector);
+            fixture.detectChanges();
+
+            const card = 
fixture.nativeElement.querySelector('property-group-card');
+            expect(card).toBeTruthy();
+            const cardDebug = 
fixture.debugElement.query(By.css('property-group-card'));
+            expect(cardDebug.componentInstance.hideGroupName()).toBe(true);
+        });
+
+        it('should set hideGroupName to false when step has multiple property 
groups', () => {
+            const connectorWithMultipleGroups: any = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: {
+                        configurationStepConfigurations: [
+                            {
+                                configurationStepName: 'Config Step',
+                                dependencies: [],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Group 1',
+                                        propertyDescriptors: {
+                                            field1: { name: 'field1', type: 
'STRING', required: true }
+                                        },
+                                        propertyValues: {
+                                            field1: { value: 'val1', 
valueType: 'STRING_LITERAL' }
+                                        }
+                                    },
+                                    {
+                                        propertyGroupName: 'Group 2',
+                                        propertyDescriptors: {
+                                            field2: { name: 'field2', type: 
'STRING', required: false }
+                                        },
+                                        propertyValues: {
+                                            field2: { value: 'val2', 
valueType: 'STRING_LITERAL' }
+                                        }
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            };
+            fixture.componentRef.setInput('connector', 
connectorWithMultipleGroups);
+            fixture.detectChanges();
+
+            const cards = 
fixture.debugElement.queryAll(By.css('property-group-card'));
+            expect(cards.length).toBe(2);
+            expect(cards[0].componentInstance.hideGroupName()).toBe(false);
+            expect(cards[1].componentInstance.hideGroupName()).toBe(false);
+        });
+    });
+
+    describe('filteredStepConfigurations', () => {
+        it('should return empty array when connector has no 
activeConfiguration', () => {
+            const connectorWithoutConfig: ConnectorEntity = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: undefined
+                }
+            };
+            fixture.componentRef.setInput('connector', connectorWithoutConfig);
+
+            expect(component.filteredStepConfigurations()).toEqual([]);
+        });
+
+        it('should return all properties when none have dependencies', () => {
+            fixture.componentRef.setInput('connector', mockConnector);
+
+            const filtered = component.filteredStepConfigurations();
+            expect(filtered.length).toBe(1);
+            
expect(Object.keys(filtered[0].propertyGroupConfigurations[0].propertyDescriptors)).toContain('hostname');
+        });
+
+        it('should filter out hidden properties based on dependencies', () => {
+            const connectorWithDependencies: any = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: {
+                        configurationStepConfigurations: [
+                            {
+                                configurationStepName: 'Config Step',
+                                dependencies: [],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Settings',
+                                        propertyDescriptors: {
+                                            enableFeature: {
+                                                name: 'enableFeature',
+                                                description: 'Enable feature',
+                                                type: 'STRING',
+                                                required: true
+                                            },
+                                            featureOption: {
+                                                name: 'featureOption',
+                                                description: 'Feature option',
+                                                type: 'STRING',
+                                                required: false,
+                                                dependencies: [
+                                                    { propertyName: 
'enableFeature', dependentValues: ['yes'] }
+                                                ]
+                                            }
+                                        },
+                                        propertyValues: {
+                                            enableFeature: { value: 'no', 
valueType: 'STRING_LITERAL' }
+                                        }
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            };
+            fixture.componentRef.setInput('connector', 
connectorWithDependencies);
+
+            const filtered = component.filteredStepConfigurations();
+            const descriptors = 
filtered[0].propertyGroupConfigurations[0].propertyDescriptors;
+
+            expect(Object.keys(descriptors)).toContain('enableFeature');
+            expect(Object.keys(descriptors)).not.toContain('featureOption');
+        });
+
+        it('should include visible dependent properties when condition is 
met', () => {
+            const connectorWithDependencies: any = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: {
+                        configurationStepConfigurations: [
+                            {
+                                configurationStepName: 'Config Step',
+                                dependencies: [],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Settings',
+                                        propertyDescriptors: {
+                                            enableFeature: {
+                                                name: 'enableFeature',
+                                                description: 'Enable feature',
+                                                type: 'STRING',
+                                                required: true
+                                            },
+                                            featureOption: {
+                                                name: 'featureOption',
+                                                description: 'Feature option',
+                                                type: 'STRING',
+                                                required: false,
+                                                dependencies: [
+                                                    { propertyName: 
'enableFeature', dependentValues: ['yes'] }
+                                                ]
+                                            }
+                                        },
+                                        propertyValues: {
+                                            enableFeature: { value: 'yes', 
valueType: 'STRING_LITERAL' }
+                                        }
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            };
+            fixture.componentRef.setInput('connector', 
connectorWithDependencies);
+
+            const filtered = component.filteredStepConfigurations();
+            const descriptors = 
filtered[0].propertyGroupConfigurations[0].propertyDescriptors;
+
+            expect(Object.keys(descriptors)).toContain('enableFeature');
+            expect(Object.keys(descriptors)).toContain('featureOption');
+        });
+
+        it('should filter out hidden steps based on step dependencies', () => {
+            const connectorWithStepDependencies: any = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: {
+                        configurationStepConfigurations: [
+                            {
+                                configurationStepName: 'Step 1',
+                                dependencies: [],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Settings',
+                                        propertyDescriptors: {
+                                            enableAdvanced: {
+                                                name: 'enableAdvanced',
+                                                type: 'STRING',
+                                                required: true
+                                            }
+                                        },
+                                        propertyValues: {
+                                            enableAdvanced: { value: 'no', 
valueType: 'STRING_LITERAL' }
+                                        }
+                                    }
+                                ]
+                            },
+                            {
+                                configurationStepName: 'Step 2 - Advanced',
+                                dependencies: [
+                                    { stepName: 'Step 1', propertyName: 
'enableAdvanced', dependentValues: ['yes'] }
+                                ],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Advanced Settings',
+                                        propertyDescriptors: {
+                                            advancedOption: {
+                                                name: 'advancedOption',
+                                                type: 'STRING',
+                                                required: false
+                                            }
+                                        },
+                                        propertyValues: {}
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            };
+            fixture.componentRef.setInput('connector', 
connectorWithStepDependencies);
+
+            const filtered = component.filteredStepConfigurations();
+
+            expect(filtered.length).toBe(1);
+            expect(filtered[0].configurationStepName).toBe('Step 1');
+        });
+
+        it('should include visible steps when step dependency is met', () => {
+            const connectorWithStepDependencies: any = {
+                ...mockConnector,
+                component: {
+                    ...mockConnector.component,
+                    activeConfiguration: {
+                        configurationStepConfigurations: [
+                            {
+                                configurationStepName: 'Step 1',
+                                dependencies: [],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Settings',
+                                        propertyDescriptors: {
+                                            enableAdvanced: {
+                                                name: 'enableAdvanced',
+                                                type: 'STRING',
+                                                required: true
+                                            }
+                                        },
+                                        propertyValues: {
+                                            enableAdvanced: { value: 'yes', 
valueType: 'STRING_LITERAL' }
+                                        }
+                                    }
+                                ]
+                            },
+                            {
+                                configurationStepName: 'Step 2 - Advanced',
+                                dependencies: [
+                                    { stepName: 'Step 1', propertyName: 
'enableAdvanced', dependentValues: ['yes'] }
+                                ],
+                                propertyGroupConfigurations: [
+                                    {
+                                        propertyGroupName: 'Advanced Settings',
+                                        propertyDescriptors: {
+                                            advancedOption: {
+                                                name: 'advancedOption',
+                                                type: 'STRING',
+                                                required: false
+                                            }
+                                        },
+                                        propertyValues: {}
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                }
+            };
+            fixture.componentRef.setInput('connector', 
connectorWithStepDependencies);
+
+            const filtered = component.filteredStepConfigurations();
+
+            expect(filtered.length).toBe(2);
+            expect(filtered[0].configurationStepName).toBe('Step 1');
+            expect(filtered[1].configurationStepName).toBe('Step 2 - 
Advanced');
+        });
+    });
+});
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.ts
new file mode 100644
index 00000000000..3c5fd0e70d2
--- /dev/null
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/connectors/ui/connector-details-content/connector-details-content.component.ts
@@ -0,0 +1,61 @@
+/*
+ * 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, computed, input } from '@angular/core';
+
+import {
+    PropertyGroupCard,
+    ConnectorEntity,
+    ConfigurationStepConfiguration,
+    filterPropertyVisibility,
+    ConnectorDetailHeader,
+    getVisibleStepNames
+} from '@nifi/shared';
+
+@Component({
+    selector: 'connector-details-content',
+    imports: [PropertyGroupCard, ConnectorDetailHeader],
+    templateUrl: './connector-details-content.component.html',
+    styleUrls: ['./connector-details-content.component.scss']
+})
+export class ConnectorDetailsContent {
+    connector = input.required<ConnectorEntity>();
+
+    /**
+     * Computed signal that returns configuration steps filtered by step 
visibility,
+     * with property groups filtered to only visible properties.
+     * Step visibility is based on step dependencies (a step can depend on 
properties from previous steps).
+     * Property visibility is scoped to each step (dependencies can only 
reference properties within the same step).
+     */
+    filteredStepConfigurations = computed(() => {
+        const steps = 
this.connector().component.activeConfiguration?.configurationStepConfigurations;
+        if (!steps || steps.length === 0) {
+            return [];
+        }
+
+        const stepConfigurations: { [stepName: string]: 
ConfigurationStepConfiguration } = {};
+        for (const step of steps) {
+            stepConfigurations[step.configurationStepName] = step;
+        }
+
+        const stepNames = steps.map((s) => s.configurationStepName);
+        const visibleStepNames = getVisibleStepNames(stepNames, 
stepConfigurations, {});
+
+        const visibleSteps = steps.filter((step) => 
visibleStepNames.includes(step.configurationStepName));
+        return filterPropertyVisibility(visibleSteps);
+    });
+}
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.html
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.html
index 757c93335f3..92b4573b325 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.html
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-configuration-step/connector-configuration-step.component.html
@@ -52,7 +52,7 @@
                             mode="indeterminate"
                             diameter="40"
                             data-qa="step-spinner"></mat-progress-spinner>
-                        <p class="text-base" 
data-qa="step-loading-message">Loading step configuration...</p>
+                        <div data-qa="step-loading-message">Loading step 
configuration...</div>
                     </div>
                 } @else {
                     @let config = stepConfiguration();
@@ -137,7 +137,7 @@
                     } @else {
                         <!-- No Data State -->
                         <div class="flex items-center justify-center p-8">
-                            <p class="text-base opacity-70">No configuration 
data available for this step.</p>
+                            <div class="unset">No configuration data available 
for this step.</div>
                         </div>
                     }
                 }
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
index 89723da6622..258b15e4917 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/connector-wizard.component.html
@@ -29,7 +29,7 @@
             <div class="flex-1 flex items-center justify-center" 
data-qa="error-container">
                 <div class="flex flex-col items-center gap-y-4">
                     <i class="fa fa-warning error-color text-4xl" 
data-qa="error-icon" aria-hidden="true"></i>
-                    <p class="text-base" data-qa="error-message">{{ 
wizardStore.error() }}</p>
+                    <div data-qa="error-message">{{ wizardStore.error() 
}}</div>
                     <button mat-flat-button (click)="navigateBack.emit()" 
data-qa="return-button">
                         <ng-content select="[returnButtonLabel]">Return to 
Connectors</ng-content>
                     </button>
@@ -112,9 +112,9 @@
                     <div class="flex-1 flex items-center justify-center" 
data-qa="no-steps-container">
                         <div class="flex flex-col items-center gap-4">
                             <i class="fa fa-info-circle text-4xl" 
data-qa="no-steps-icon" aria-hidden="true"></i>
-                            <p class="text-base" data-qa="no-steps-message">
+                            <div class="unset" data-qa="no-steps-message">
                                 No configuration steps are available for this 
connector.
-                            </p>
+                            </div>
                         </div>
                     </div>
                 }
diff --git 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/wizard-step-documentation-panel/wizard-step-documentation-panel.component.html
 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/wizard-step-documentation-panel/wizard-step-documentation-panel.component.html
index 3607df7507b..4d6c4500652 100644
--- 
a/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/wizard-step-documentation-panel/wizard-step-documentation-panel.component.html
+++ 
b/nifi-frontend/src/main/frontend/libs/shared/src/components/connector-wizard/wizard-step-documentation-panel/wizard-step-documentation-panel.component.html
@@ -36,7 +36,7 @@
         } @else if (getErrorMessage()) {
             <div class="error-container flex flex-col items-center 
justify-center gap-4 p-8" data-qa="markdown-error">
                 <i class="fa fa-warning error-color text-4xl" 
aria-hidden="true"></i>
-                <p class="text-base text-center wrap-anywhere">{{ 
getErrorMessage() }}</p>
+                <div class="text-center wrap-anywhere">{{ getErrorMessage() 
}}</div>
             </div>
         } @else if (getStepDocumentation()) {
             <div class="markdown-content" data-qa="markdown-content">
@@ -45,7 +45,7 @@
         } @else {
             <div class="empty-container flex flex-col items-center 
justify-center gap-4 p-8" data-qa="markdown-empty">
                 <i class="fa fa-info-circle text-4xl opacity-50" 
aria-hidden="true"></i>
-                <p class="text-base text-center opacity-70">No documentation 
available.</p>
+                <div class="text-center unset">No documentation 
available.</div>
             </div>
         }
     </div>

Reply via email to