rfellows commented on code in PR #11582:
URL: https://github.com/apache/nifi/pull/11582#discussion_r4028687684


##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.html:
##########
@@ -0,0 +1,149 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>{{ title }}</h2>
+<mat-dialog-content>
+<!--    <div class="flex flex-col gap-y-4">-->
+    <div class="dialog-content flex flex-col h-full">
+        <div class="tertiary-color font-medium">Selected Component<br>
+            <i class="icon component-type-icon" 
[class]="componentIcon(componentType)"></i>
+            {{ componentName }}
+        </div>

Review Comment:
   The “Selected Component” header (icon + name) is a good place to reuse the 
shared `component-context` widget instead of a one-off icon/name block.
   
   That widget already maps `ComponentType` to the canvas icon (including 
`icon-group-remote`), shows the type label, and offers a copyable id. Cluster 
Summary uses it the same way at the top of a dialog:
   
   ```html
   <component-context
       [type]="componentType"
       [name]="componentName"
       [id]="componentId"></component-context>
   ```
   
   (`cluster-summary-dialog.component.html`; also the Operation panel.) Import 
`ComponentContext` from `@nifi/shared` and add it to this component’s `imports`.
   
   `ViewComponentConnectionsRequest` already has `id` / `name` / `type`. Those 
just need to be forwarded on `ComponentConnectionsDialogRequest` so the header 
can bind `[id]` as well. The name fallback you already have in 
`requestComponentConnections` (readable name, else the component id) is the 
right `[name]` — `component-context` does not itself handle `canRead`.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts:
##########
@@ -287,25 +289,23 @@ export class CanvasContextMenu implements 
ContextMenuDefinitionProvider {
         id: 'upstream-downstream',
         menuItems: [
             {
-                condition: () => {
-                    // TODO - hasUpstream
-                    return false;
+                condition: (selection: d3.Selection<any, any, any, any>) => {
+                    return this.canvasUtils.hasUpstream(selection);
                 },

Review Comment:
   View Connections never appears on empty-canvas right-click. `hasUpstream` / 
`hasDownstream` require `selection.size() === 1`, so a click on the canvas (no 
component selected) yields an empty submenu and the parent item is hidden 
(`context-menu.component.ts` keeps a submenu only when it has visible children).
   
   Empty canvas is the current process group — the Operation panel already 
treats `selection.size() === 0` that way (`getContextType` → `ProcessGroup`, 
name from breadcrumbs). Several canvas actions do the same, for example 
Enable/Disable All Controller Services:
   
   ```typescript
   condition: (selection) => {
       return this.canvasUtils.isProcessGroup(selection) || 
this.canvasUtils.emptySelection(selection);
   }
   ```
   
   If the intent is to restore 1.x “view connections for the group I’m in,” 
allow empty selection here and, when `selection.empty()`, use the current 
process group id (`canvasUtils.getProcessGroupId()`) instead of 
`selection.datum()`. That matches how those other current-PG actions are wired.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.html:
##########
@@ -0,0 +1,149 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>{{ title }}</h2>
+<mat-dialog-content>
+<!--    <div class="flex flex-col gap-y-4">-->
+    <div class="dialog-content flex flex-col h-full">
+        <div class="tertiary-color font-medium">Selected Component<br>
+            <i class="icon component-type-icon" 
[class]="componentIcon(componentType)"></i>
+            {{ componentName }}
+        </div>
+        @if (rows.length === 0) {
+            <div class="neutral-contrast">{{ emptyMessage }}</div>
+        } @else {
+            <div class="listing-table component-connections-table flex-1 
relative">
+                <div class="absolute inset-0 overflow-y-auto 
overflow-x-hidden">
+                    <table mat-table [dataSource]="rows">
+                        <ng-container matColumnDef="sourceProcessGroup">
+                            <th mat-header-cell *matHeaderCellDef>Source 
<br>Process Group</th>
+                            <td mat-cell *matCellDef="let row">
+                            @if (isCurrentProcessGroup(row.source.groupId)) {
+                                <span class="component-connection-cell 
neutral-contrast" [matTooltip]="resolveGroupName(row.source.groupId)">
+                                    <i class="icon component-type-icon" 
[class]="componentIcon(processGroupType)"></i>
+                                    {{ resolveGroupName(row.source.groupId) }}
+                                </span>
+                            } @else {
+                                <a
+                                    class="component-connection-cell 
neutral-contrast"
+                                    
[matTooltip]="resolveGroupName(row.source.groupId)"
+                                    (click)="navigateTo(row.source.groupId, 
dialogRequestGroupId, processGroupType)">
+                                    <i class="icon component-type-icon 
flex-none" [class]="componentIcon(processGroupType)"></i>
+                                    {{ resolveGroupName(row.source.groupId) }}
+                                </a>

Review Comment:
   Remote ports are correctly non-clickable now, but the Source/Destination 
Process Group cells still always call `navigateTo(..., processGroupType)`:
   
   ```html
   (click)="navigateTo(row.source.groupId, dialogRequestGroupId, 
processGroupType)"
   ```
   
   For `REMOTE_INPUT_PORT` / `REMOTE_OUTPUT_PORT`, `groupId` is the remote 
process group id, so this still routes to 
`/process-groups/{current}/ProcessGroup/{rpgId}` instead of 
`RemoteProcessGroup/{rpgId}` — the same class of bug as navigating the port 
itself. The icon is also `icon-group` rather than `icon-group-remote`.
   
   Go To Source already uses `ComponentType.RemoteProcessGroup` when the 
connectable is remote (`canvas-context-menu.service.ts`). 
`remoteProcessGroupIds` is already on the dialog request and never read; wire 
that (or `endpoint.type === RemoteProcessGroup`) for the group-cell type and 
icon, and add a click test for that cell. The destination process-group link 
(lines 103–109) has the same issue.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.html:
##########
@@ -0,0 +1,149 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>{{ title }}</h2>
+<mat-dialog-content>
+<!--    <div class="flex flex-col gap-y-4">-->
+    <div class="dialog-content flex flex-col h-full">
+        <div class="tertiary-color font-medium">Selected Component<br>
+            <i class="icon component-type-icon" 
[class]="componentIcon(componentType)"></i>
+            {{ componentName }}
+        </div>
+        @if (rows.length === 0) {
+            <div class="neutral-contrast">{{ emptyMessage }}</div>
+        } @else {
+            <div class="listing-table component-connections-table flex-1 
relative">
+                <div class="absolute inset-0 overflow-y-auto 
overflow-x-hidden">
+                    <table mat-table [dataSource]="rows">

Review Comment:
   This listing table is not sortable. Other dialog tables (Change Version, 
Local Changes) use Angular Material sort on the `mat-table`. A compact version 
of that pattern:
   
   ```html
   <table
       mat-table
       [dataSource]="dataSource"
       matSort
       matSortDisableClear
       (matSortChange)="sortData($event)"
       [matSortActive]="initialSortColumn"
       [matSortDirection]="initialSortDirection">
       ...
       <th mat-header-cell *matHeaderCellDef mat-sort-header>Connection</th>
   ```
   
   See `change-version-dialog.html` (73–80, 99–100) and 
`local-changes-table.html` / `local-changes-table.ts` (`sortData` + 
`nifiCommon.compareString`).
   
   Practical defaults for this dialog:
   - Use `MatTableDataSource` (or keep the array and re-assign on sort).
   - Import `MatSortModule`.
   - Default `[matSortActive]="'connection'"` and `[matSortDirection]="'asc'"`.
   - Sort each column by the visible label (`resolveGroupName(...)`, endpoint 
name, connection name), with `nifiCommon.compareString`, so unauthorized / 
unnamed rows still have a stable key.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts:
##########
@@ -19,6 +19,7 @@ import { flowFeatureKey, FlowState, SelectedComponent } from 
'./index';
 import { createSelector } from '@ngrx/store';
 import { CanvasState, selectCanvasState } from '../index';
 import { ComponentType, selectCurrentRoute } from '@nifi/shared';
+import { BreadcrumbEntity } from '../../../../state/shared';

Review Comment:
   Unused `BreadcrumbEntity` import (`@typescript-eslint/no-unused-vars`), 
leftover from moving the name map onto the dialog request. Please drop it (same 
lint pass as the trailing comma in `flow.effects.ts`).



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts:
##########
@@ -3180,6 +3183,96 @@ export class FlowEffects {
         { dispatch: false }
     );
 
+    /**
+     * Loads the flow of the group that defines the requested component's 
connections and retains only
+     * the connections attached to that component in the requested direction. 
The group is the one on
+     * the canvas for most components, and the parent group for the two port 
cases whose connections
+     * cross the enclosing group's boundary.
+     *
+     * Matching goes through the canvas' own endpoint resolvers rather than 
comparing the raw ids. A
+     * connection drawn to a Process Group or Remote Process Group actually 
terminates at a port inside
+     * it, and 
getConnectionSourceComponentId/getConnectionDestinationComponentId are what 
collapse that
+     * port back to the group the user sees and selects. They compare against 
the group currently on the
+     * canvas, which is the right frame of reference for the parent-group 
searches too: a connection
+     * into an Input Port carries that port's own group as its destination 
group, so it resolves to the
+     * port rather than to the group.
+     *
+     *
+     * Both resolvers read the ids on the connection entity rather than on its 
component, so a
+     * connection the current user cannot read — the kind most worth reporting 
— is still matched.
+     */
+    viewComponentConnections$ = createEffect(() =>
+        this.actions$.pipe(
+            ofType(FlowActions.viewComponentConnections),
+            map((action) => action.request),
+            switchMap((request) => {
+                const attachedTo = (connection: ConnectionEntity): boolean =>
+                    request.direction === 'upstream'
+                        ? 
this.canvasUtils.getConnectionDestinationComponentId(connection) === request.id
+                        : 
this.canvasUtils.getConnectionSourceComponentId(connection) === request.id;
+
+                return from(this.flowService.getFlow(request.groupId)).pipe(
+                    map((flowEntity: ProcessGroupFlowEntity) =>
+                        FlowActions.openComponentConnectionsDialog({
+                            request: {
+                                componentName: request.name,
+                                componentType: request.type,
+                                groupId: request.groupId,
+                                direction: request.direction,
+                                connections: 
flowEntity.processGroupFlow.flow.connections.filter(attachedTo),
+                                groupIdToName: 
this.buildProcessGroupIdToNameMap(flowEntity),
+                                remoteProcessGroupIds: 
this.buildRemoteProcessGroupIdSet(flowEntity),

Review Comment:
   `npx nx run nifi:lint` fails on this change set:
   
   1. Here — prettier wants the trailing comma after `remoteProcessGroupIds: 
this.buildRemoteProcessGroupIdSet(flowEntity)` removed.
   2. `flow.selectors.ts:22` — unused `BreadcrumbEntity` import 
(`@typescript-eslint/no-unused-vars`), leftover from moving the name map onto 
the dialog request.
   
   Please fix both so CI lint stays green.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.html:
##########
@@ -0,0 +1,149 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>{{ title }}</h2>
+<mat-dialog-content>
+<!--    <div class="flex flex-col gap-y-4">-->
+    <div class="dialog-content flex flex-col h-full">
+        <div class="tertiary-color font-medium">Selected Component<br>
+            <i class="icon component-type-icon" 
[class]="componentIcon(componentType)"></i>
+            {{ componentName }}
+        </div>
+        @if (rows.length === 0) {
+            <div class="neutral-contrast">{{ emptyMessage }}</div>
+        } @else {
+            <div class="listing-table component-connections-table flex-1 
relative">
+                <div class="absolute inset-0 overflow-y-auto 
overflow-x-hidden">
+                    <table mat-table [dataSource]="rows">
+                        <ng-container matColumnDef="sourceProcessGroup">
+                            <th mat-header-cell *matHeaderCellDef>Source 
<br>Process Group</th>
+                            <td mat-cell *matCellDef="let row">
+                            @if (isCurrentProcessGroup(row.source.groupId)) {
+                                <span class="component-connection-cell 
neutral-contrast" [matTooltip]="resolveGroupName(row.source.groupId)">
+                                    <i class="icon component-type-icon" 
[class]="componentIcon(processGroupType)"></i>
+                                    {{ resolveGroupName(row.source.groupId) }}
+                                </span>
+                            } @else {
+                                <a
+                                    class="component-connection-cell 
neutral-contrast"
+                                    
[matTooltip]="resolveGroupName(row.source.groupId)"
+                                    (click)="navigateTo(row.source.groupId, 
dialogRequestGroupId, processGroupType)">
+                                    <i class="icon component-type-icon 
flex-none" [class]="componentIcon(processGroupType)"></i>
+                                    {{ resolveGroupName(row.source.groupId) }}
+                                </a>
+                            }
+                            </td>
+                        </ng-container>
+
+                        <ng-container matColumnDef="sourceComponent">
+                            <th mat-header-cell *matHeaderCellDef>Source 
<br>Component</th>
+                            <td mat-cell *matCellDef="let row">
+                            @if (row.source.name === null) {
+                                <span class="component-connection-cell 
neutral-contrast" [matTooltip]="row.source.id">
+                                    <i class="icon component-type-icon" 
[class]="componentIcon(row.source.type)"></i>
+                                    Unauthorized
+                                </span>
+                            } @else if (isRemoteProcessGroupPort(row.source)) {
+                                <span class="component-connection-cell 
neutral-contrast" [matTooltip]="row.source.name">
+                                    <i class="icon component-type-icon" 
[class]="componentIcon(row.source.type)"></i>
+                                    {{ row.source.name }}
+                                </span>
+                            } @else {
+                                <a
+                                    class="component-connection-cell 
neutral-contrast"
+                                    [matTooltip]="row.source.name"
+                                    (click)="navigateTo(row.source.id, 
row.source.groupId, row.source.type)">
+                                    <i class="icon component-type-icon" 
[class]="componentIcon(row.source.type)"></i>
+                                    {{ row.source.name }}
+                                </a>

Review Comment:
   The underline on these links starts a character before the icon/text. That 
comes from whitespace in the template becoming text nodes inside the `<a>`. For 
example:
   
   ```html
   <a
       class="component-connection-cell neutral-contrast"
       ...>
       <i class="icon component-type-icon" 
[class]="componentIcon(row.source.type)"></i>
       {{ row.source.name }}
   </a>
   ```
   
   The newline/indent between `>` / `<i>` / `{{ ... }}` is rendered as spaces, 
so the underline is longer than the visible content. Icon-to-label spacing is 
already handled in SCSS (`.component-type-icon { margin-right: 0.25rem; }`), so 
the extra DOM spaces are not needed.
   
   Please keep the opening tag, icon, interpolation, and closing tag flush (all 
five link/span cells):
   
   ```html
   <a
       class="component-connection-cell neutral-contrast"
       [matTooltip]="row.source.name"
       (click)="navigateTo(row.source.id, row.source.groupId, row.source.type)"
       ><i class="icon component-type-icon" 
[class]="componentIcon(row.source.type)"></i>{{ row.source.name }}</a>
   ```



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.html:
##########
@@ -0,0 +1,149 @@
+<!--
+  ~ 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.
+  -->
+
+<h2 mat-dialog-title>{{ title }}</h2>
+<mat-dialog-content>
+<!--    <div class="flex flex-col gap-y-4">-->

Review Comment:
   A couple of leftovers from the iteration:
   
   1. This commented-out wrapper: `<!--    <div class="flex flex-col 
gap-y-4">-->`. Please delete it rather than leaving it in the shipped template.
   2. `component-connections-dialog.component.spec.ts` 674–697 and 699–722 are 
the same test (`navigates to the connection in the group that defines the 
dialog request`). Please keep one.



##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.spec.ts:
##########
@@ -0,0 +1,744 @@
+/*
+ * 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { By } from '@angular/platform-browser';
+import { of } from 'rxjs';
+import { ComponentType } from '@nifi/shared';
+
+import { ComponentConnectionsDialog, ComponentConnectionRow } from 
'./component-connections-dialog.component';
+import { ComponentConnectionsDialogRequest, ConnectionDirection, 
ConnectionEntity } from '../../../state/flow';
+import { navigateToComponent } from '../../../state/flow/flow.actions';
+import { CanvasUtils } from '../../../service/canvas-utils.service';
+
+const REQUEST_GROUP_ID = 'request-group-id';
+const SOURCE_GROUP_ID = 'source-group-id';
+const DESTINATION_GROUP_ID = 'destination-group-id';
+const UNKNOWN_GROUP_ID = 'unknown-group-id';
+
+const SOURCE_ID = 'source-id';
+const DESTINATION_ID = 'destination-id';
+const CONNECTION_ID = 'connection-id';
+
+interface ConnectableStub {
+    id: string;
+    name: string;
+}
+
+interface ConnectionOptions {
+    id?: string;
+    source?: ConnectableStub;
+    destination?: ConnectableStub;
+    sourceGroupId?: string;
+    destinationGroupId?: string;
+    sourceType?: string;
+    destinationType?: string;
+    canRead?: boolean;
+    name?: string;
+    selectedRelationships?: string[];
+    component?: any | null;
+}
+
+interface CreatedDialog {
+    component: ComponentConnectionsDialog;
+    fixture: ComponentFixture<ComponentConnectionsDialog>;
+    store: MockStore;
+    dialogRef: {
+        close: ReturnType<typeof vi.fn>;
+        keydownEvents: () => ReturnType<typeof of>;
+    };
+}
+
+function readableConnection(options: ConnectionOptions = {}): ConnectionEntity 
{
+    const source = options.source ?? { id: SOURCE_ID, name: 'GenerateFlowFile' 
};
+    const destination = options.destination ?? { id: DESTINATION_ID, name: 
'LogAttribute' };
+
+    return {
+        id: options.id ?? CONNECTION_ID,
+        permissions: { canRead: options.canRead ?? true, canWrite: true },
+        position: { x: 0, y: 0 },
+        revision: { version: 0 },
+        sourceId: source.id,
+        sourceGroupId: options.sourceGroupId ?? SOURCE_GROUP_ID,
+        sourceType: options.sourceType ?? 'PROCESSOR',
+        destinationId: destination.id,
+        destinationGroupId: options.destinationGroupId ?? DESTINATION_GROUP_ID,
+        destinationType: options.destinationType ?? 'INPUT_PORT',
+        component:
+            options.component === undefined
+                ? {
+                      id: options.id ?? CONNECTION_ID,
+                      source,
+                      destination,
+                      name: options.name,
+                      selectedRelationships: options.selectedRelationships
+                  }
+                : options.component
+    };
+}
+
+function unreadableConnection(options: ConnectionOptions = {}): 
ConnectionEntity {
+    return {
+        id: options.id ?? CONNECTION_ID,
+        permissions: { canRead: false, canWrite: false },
+        position: { x: 0, y: 0 },
+        revision: { version: 0 },
+        sourceId: options.source?.id ?? SOURCE_ID,
+        sourceGroupId: options.sourceGroupId ?? SOURCE_GROUP_ID,
+        sourceType: options.sourceType ?? 'PROCESSOR',
+        destinationId: options.destination?.id ?? DESTINATION_ID,
+        destinationGroupId: options.destinationGroupId ?? DESTINATION_GROUP_ID,
+        destinationType: options.destinationType ?? 'INPUT_PORT',
+        component: null
+    };
+}
+
+function createDialog(
+    direction: ConnectionDirection,
+    connections: ConnectionEntity[],
+    overrides: Partial<ComponentConnectionsDialogRequest> = {}
+): CreatedDialog {
+    const dialogRequest: ComponentConnectionsDialogRequest = {
+        componentName: 'Selected Component',
+        componentType: ComponentType.InputPort,
+        groupId: REQUEST_GROUP_ID,
+        direction,
+        connections,
+        groupIdToName: new Map([
+            [REQUEST_GROUP_ID, 'Current Process Group'],
+            [SOURCE_GROUP_ID, 'Source Process Group'],
+            [DESTINATION_GROUP_ID, 'Destination Process Group']
+        ]),
+        remoteProcessGroupIds: new Set(),
+        ...overrides
+    };
+
+    const dialogRef = {
+        close: vi.fn(),
+        keydownEvents: () => of()
+    };
+
+    const canvasUtils = {
+        formatConnectionName: (component: any): string => {
+            if (component?.name) {
+                return component.name;
+            }
+            if (component?.selectedRelationships) {
+                return component.selectedRelationships.join(', ');
+            }
+            return '';
+        }
+    };
+
+    TestBed.resetTestingModule();
+    TestBed.configureTestingModule({
+        imports: [ComponentConnectionsDialog],
+        providers: [
+            { provide: MAT_DIALOG_DATA, useValue: dialogRequest },
+            { provide: MatDialogRef, useValue: dialogRef },
+            { provide: CanvasUtils, useValue: canvasUtils },
+            provideMockStore({})
+        ]
+    });
+
+    const fixture = TestBed.createComponent(ComponentConnectionsDialog);
+    fixture.detectChanges();
+
+    return {
+        component: fixture.componentInstance,
+        fixture,
+        store: TestBed.inject(MockStore),
+        dialogRef
+    };
+}
+
+function textContent(fixture: ComponentFixture<ComponentConnectionsDialog>): 
string {
+    return (fixture.nativeElement.textContent as string).replace(/\s+/g, ' 
').trim();
+}
+
+function getCells(fixture: ComponentFixture<ComponentConnectionsDialog>, 
columnClass: string): HTMLElement[] {
+    return 
fixture.debugElement.queryAll(By.css(`td.${columnClass}`)).map((debugElement) 
=> debugElement.nativeElement);
+}
+
+describe('ComponentConnectionsDialog', () => {
+    it('creates the dialog', () => {
+        const { component } = createDialog('upstream', []);
+
+        expect(component).toBeTruthy();
+    });
+
+    describe('dialog metadata', () => {
+        it('sets upstream title and empty message', () => {
+            const { component, fixture } = createDialog('upstream', []);
+
+            expect(component.title).toBe('Upstream Connections');
+            expect(component.emptyMessage).toBe('No upstream connections were 
found.');
+            expect(textContent(fixture)).toContain('Upstream Connections');
+            expect(textContent(fixture)).toContain('No upstream connections 
were found.');
+        });
+
+        it('sets downstream title and empty message', () => {
+            const { component, fixture } = createDialog('downstream', []);
+
+            expect(component.title).toBe('Downstream Connections');
+            expect(component.emptyMessage).toBe('No downstream connections 
were found.');
+            expect(textContent(fixture)).toContain('Downstream Connections');
+            expect(textContent(fixture)).toContain('No downstream connections 
were found.');
+        });
+
+        it('renders the selected component name and icon', () => {
+            const { fixture } = createDialog('upstream', [], {
+                componentName: 'Input Port A',
+                componentType: ComponentType.InputPort
+            });
+
+            expect(textContent(fixture)).toContain('Selected Component');
+            expect(textContent(fixture)).toContain('Input Port A');
+            
expect(fixture.debugElement.query(By.css('.icon-port-in'))).not.toBeNull();
+        });
+    });
+
+    describe('row construction', () => {
+        it('builds a row for a readable connection with a connection name', () 
=> {
+            const connection = readableConnection({
+                id: 'named-connection-id',
+                name: 'Named Connection',
+                source: { id: 'processor-id', name: 'GenerateFlowFile' },
+                destination: { id: 'input-port-id', name: 'Input Port' }
+            });
+
+            const { component } = createDialog('upstream', [connection]);
+
+            expect(component.rows).toEqual<ComponentConnectionRow[]>([
+                {
+                    id: 'named-connection-id',
+                    name: 'Named Connection',
+                    source: {
+                        id: 'processor-id',
+                        groupId: SOURCE_GROUP_ID,
+                        type: ComponentType.Processor,
+                        name: 'GenerateFlowFile'
+                    },
+                    destination: {
+                        id: 'input-port-id',
+                        groupId: DESTINATION_GROUP_ID,
+                        type: ComponentType.InputPort,
+                        name: 'Input Port'
+                    }
+                }
+            ]);
+        });
+
+        it('uses selected relationships as the connection name when no 
explicit connection name is present', () => {
+            const connection = readableConnection({
+                selectedRelationships: ['success', 'retry']
+            });
+
+            const { component, fixture } = createDialog('upstream', 
[connection]);
+
+            expect(component.rows[0].name).toBe('success, retry');
+            expect(textContent(fixture)).toContain('success, retry');
+        });
+
+        it('uses a null connection name when the formatted name is empty', () 
=> {
+            const connection = readableConnection();
+
+            const { component } = createDialog('upstream', [connection]);
+
+            expect(component.rows[0].name).toBeNull();
+        });
+
+        it('keeps unreadable connections using top-level endpoint identifiers 
and null endpoint names', () => {
+            const connection = unreadableConnection({
+                id: 'unreadable-connection-id',
+                source: { id: 'hidden-source-id', name: 'Hidden Source' },
+                destination: { id: 'hidden-destination-id', name: 'Hidden 
Destination' }
+            });
+
+            const { component, fixture } = createDialog('upstream', 
[connection]);
+
+            expect(component.rows).toEqual<ComponentConnectionRow[]>([
+                {
+                    id: 'unreadable-connection-id',
+                    name: null,
+                    source: {
+                        id: 'hidden-source-id',
+                        groupId: SOURCE_GROUP_ID,
+                        type: ComponentType.Processor,
+                        name: null
+                    },
+                    destination: {
+                        id: 'hidden-destination-id',
+                        groupId: DESTINATION_GROUP_ID,
+                        type: ComponentType.InputPort,
+                        name: null
+                    }
+                }
+            ]);
+
+            expect(textContent(fixture)).toContain('Unauthorized');
+        });
+
+        it('maps remote input and output port endpoint types to 
RemoteProcessGroup', () => {
+            const remoteInputConnection = readableConnection({
+                id: 'remote-input-connection-id',
+                sourceType: 'REMOTE_INPUT_PORT',
+                destinationType: 'REMOTE_OUTPUT_PORT'
+            });
+
+            const { component } = createDialog('downstream', 
[remoteInputConnection]);
+
+            
expect(component.rows[0].source.type).toBe(ComponentType.RemoteProcessGroup);
+            
expect(component.rows[0].destination.type).toBe(ComponentType.RemoteProcessGroup);
+        });
+
+        it('maps unknown endpoint types to Connector', () => {
+            const unknownTypeConnection = readableConnection({
+                sourceType: 'UNKNOWN_SOURCE_TYPE',
+                destinationType: 'UNKNOWN_DESTINATION_TYPE'
+            });
+
+            const { component } = createDialog('downstream', 
[unknownTypeConnection]);
+
+            
expect(component.rows[0].source.type).toBe(ComponentType.Connector);
+            
expect(component.rows[0].destination.type).toBe(ComponentType.Connector);
+        });
+    });
+
+    describe('rendering', () => {
+        it('renders the expected table columns', () => {
+            const { component, fixture } = createDialog('upstream', 
[readableConnection()]);
+
+            expect(component.displayedColumns).toEqual([
+                'sourceProcessGroup',
+                'sourceComponent',
+                'connection',
+                'destinationProcessGroup',
+                'destinationComponent'
+            ]);
+
+            const renderedText = textContent(fixture);
+            expect(renderedText).toContain('Source Process Group');
+            expect(renderedText).toContain('Source Component');
+            expect(renderedText).toContain('Connection');
+            expect(renderedText).toContain('Destination Process Group');
+            expect(renderedText).toContain('Destination Component');
+        });
+
+        it('renders process group names resolved from the request map', () => {
+            const { fixture } = createDialog('upstream', 
[readableConnection()]);
+
+            expect(textContent(fixture)).toContain('Source Process Group');
+            expect(textContent(fixture)).toContain('Destination Process 
Group');
+        });
+
+        it('renders unknown process group ids when no name is available', () 
=> {
+            const connection = readableConnection({
+                sourceGroupId: UNKNOWN_GROUP_ID,
+                destinationGroupId: UNKNOWN_GROUP_ID
+            });
+
+            const { fixture } = createDialog('upstream', [connection]);
+
+            expect(textContent(fixture)).toContain(UNKNOWN_GROUP_ID);
+        });
+
+        it('renders component names and the formatted connection name', () => {
+            const connection = readableConnection({
+                name: 'Connection Name',
+                source: { id: 'source-component-id', name: 'Source Component 
Name' },
+                destination: { id: 'destination-component-id', name: 
'Destination Component Name' }
+            });
+
+            const { fixture } = createDialog('upstream', [connection]);
+
+            const renderedText = textContent(fixture);
+            expect(renderedText).toContain('Source Component Name');
+            expect(renderedText).toContain('Connection Name');
+            expect(renderedText).toContain('Destination Component Name');
+        });
+
+        it('renders "Connection" for an unnamed connection', () => {
+            const { component, fixture } = createDialog('upstream', 
[readableConnection()]);
+
+            expect(component.rows[0].name).toBeNull();
+            expect(textContent(fixture)).toContain('Connection');
+        });
+
+        it('marks the header as sticky and applies striped row classes', () => 
{
+            const { fixture } = createDialog('upstream', [
+                readableConnection({ id: 'connection-1' }),
+                readableConnection({ id: 'connection-2' })
+            ]);
+
+            
expect(fixture.debugElement.query(By.css('tr.mat-mdc-header-row'))).not.toBeNull();
+
+            const rows = 
fixture.debugElement.queryAll(By.css('tr.mat-mdc-row'));
+            expect(rows.length).toBe(2);
+            
expect(rows[0].nativeElement.classList.contains('even')).toBeTruthy();
+            
expect(rows[1].nativeElement.classList.contains('even')).toBeFalsy();
+        });
+
+        it('renders table cells using component-connection-cell wrappers for 
truncation styling', () => {
+            const { fixture } = createDialog('upstream', [readableConnection({ 
name: 'Named Connection' })]);
+
+            
expect(fixture.debugElement.queryAll(By.css('.component-connection-cell')).length).toBeGreaterThan(0);
+        });
+    });
+
+    describe('process group name resolution', () => {
+        it('resolves process group names from the dialog request map', () => {
+            const { component } = createDialog('upstream', []);
+
+            expect(component.resolveGroupName(REQUEST_GROUP_ID)).toBe('Current 
Process Group');
+            expect(component.resolveGroupName(SOURCE_GROUP_ID)).toBe('Source 
Process Group');
+            
expect(component.resolveGroupName(DESTINATION_GROUP_ID)).toBe('Destination 
Process Group');
+        });
+
+        it('falls back to the group id when no process group name is 
available', () => {
+            const { component } = createDialog('upstream', []);
+
+            
expect(component.resolveGroupName(UNKNOWN_GROUP_ID)).toBe(UNKNOWN_GROUP_ID);
+        });
+
+        it('identifies the current process group from the dialog request group 
id', () => {
+            const { component } = createDialog('upstream', []);
+
+            
expect(component.isCurrentProcessGroup(REQUEST_GROUP_ID)).toBeTruthy();
+            
expect(component.isCurrentProcessGroup(SOURCE_GROUP_ID)).toBeFalsy();
+        });
+    });
+
+    describe('navigation', () => {
+        it('dispatches navigation and closes the dialog when navigateTo is 
called', () => {
+            const { component, store, dialogRef } = createDialog('upstream', 
[]);
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            component.navigateTo('target-id', 'target-group-id', 
ComponentType.Processor);
+
+            expect(dispatch).toHaveBeenCalledWith(
+                navigateToComponent({
+                    request: {
+                        id: 'target-id',
+                        processGroupId: 'target-group-id',
+                        type: ComponentType.Processor
+                    }
+                })
+            );
+            expect(dialogRef.close).toHaveBeenCalled();
+        });
+
+        it('renders the current source process group as non-clickable', () => {
+            const connection = readableConnection({
+                sourceGroupId: REQUEST_GROUP_ID,
+                destinationGroupId: DESTINATION_GROUP_ID
+            });
+
+            const { fixture } = createDialog('upstream', [connection]);
+
+            const sourceProcessGroupCell = getCells(fixture, 
'mat-column-sourceProcessGroup')[0];
+            
expect(sourceProcessGroupCell.querySelector('span')).not.toBeNull();
+            expect(sourceProcessGroupCell.querySelector('a')).toBeNull();
+        });
+
+        it('renders a non-current source process group as clickable and 
navigates to it', () => {
+            const connection = readableConnection({
+                sourceGroupId: SOURCE_GROUP_ID
+            });
+
+            const { fixture, store, dialogRef } = createDialog('upstream', 
[connection]);
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const sourceProcessGroupCell = getCells(fixture, 
'mat-column-sourceProcessGroup')[0];
+            const link = sourceProcessGroupCell.querySelector('a') as 
HTMLAnchorElement;
+            link.click();
+
+            expect(dispatch).toHaveBeenCalledWith(
+                navigateToComponent({
+                    request: {
+                        id: SOURCE_GROUP_ID,
+                        processGroupId: REQUEST_GROUP_ID,
+                        type: ComponentType.ProcessGroup
+                    }
+                })
+            );
+            expect(dialogRef.close).toHaveBeenCalled();
+        });
+
+        it('renders the current destination process group as non-clickable', 
() => {
+            const connection = readableConnection({
+                sourceGroupId: SOURCE_GROUP_ID,
+                destinationGroupId: REQUEST_GROUP_ID
+            });
+
+            const { fixture } = createDialog('upstream', [connection]);
+
+            const destinationProcessGroupCell = getCells(fixture, 
'mat-column-destinationProcessGroup')[0];
+            
expect(destinationProcessGroupCell.querySelector('span')).not.toBeNull();
+            expect(destinationProcessGroupCell.querySelector('a')).toBeNull();
+        });
+
+        it('navigates to the readable source component using the source 
component group id', () => {
+            const connection = readableConnection({
+                source: { id: 'source-component-id', name: 'Source Component' 
},
+                sourceGroupId: SOURCE_GROUP_ID,
+                sourceType: 'PROCESSOR'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('upstream', 
[connection]);
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const sourceComponentCell = getCells(fixture, 
'mat-column-sourceComponent')[0];
+            const link = sourceComponentCell.querySelector('a') as 
HTMLAnchorElement;
+            link.click();
+
+            expect(dispatch).toHaveBeenCalledWith(
+                navigateToComponent({
+                    request: {
+                        id: 'source-component-id',
+                        processGroupId: SOURCE_GROUP_ID,
+                        type: ComponentType.Processor
+                    }
+                })
+            );
+            expect(dialogRef.close).toHaveBeenCalled();
+        });
+
+        it('navigates to the readable destination component using the 
destination component group id', () => {
+            const connection = readableConnection({
+                destination: { id: 'destination-component-id', name: 
'Destination Component' },
+                destinationGroupId: DESTINATION_GROUP_ID,
+                destinationType: 'OUTPUT_PORT'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('downstream', 
[connection]);
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const destinationComponentCell = getCells(fixture, 
'mat-column-destinationComponent')[0];
+            const link = destinationComponentCell.querySelector('a') as 
HTMLAnchorElement;
+            link.click();
+
+            expect(dispatch).toHaveBeenCalledWith(
+                navigateToComponent({
+                    request: {
+                        id: 'destination-component-id',
+                        processGroupId: DESTINATION_GROUP_ID,
+                        type: ComponentType.OutputPort
+                    }
+                })
+            );
+            expect(dialogRef.close).toHaveBeenCalled();
+        });
+
+        it('does not render unreadable components as clickable', () => {
+            const { fixture } = createDialog('upstream', 
[unreadableConnection()]);
+
+            const sourceComponentCell = getCells(fixture, 
'mat-column-sourceComponent')[0];
+            const destinationComponentCell = getCells(fixture, 
'mat-column-destinationComponent')[0];
+
+            expect(sourceComponentCell.querySelector('a')).toBeNull();
+            expect(destinationComponentCell.querySelector('a')).toBeNull();
+            expect(sourceComponentCell.textContent).toContain('Unauthorized');
+            
expect(destinationComponentCell.textContent).toContain('Unauthorized');
+        });
+
+        it('renders a remote input port source component as non-clickable', () 
=> {
+            const connection = readableConnection({
+                source: { id: 'remote-input-port-id', name: 'Remote Input 
Port' },
+                sourceGroupId: 'remote-process-group-id',
+                sourceType: 'REMOTE_INPUT_PORT',
+                destination: { id: 'processor-id', name: 'Processor' },
+                destinationType: 'PROCESSOR'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('downstream', 
[connection], {
+                remoteProcessGroupIds: new Set(['remote-process-group-id'])
+            });
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const sourceComponentCell = getCells(fixture, 
'mat-column-sourceComponent')[0];
+
+            expect(sourceComponentCell.textContent).toContain('Remote Input 
Port');
+            expect(sourceComponentCell.querySelector('span')).not.toBeNull();
+            expect(sourceComponentCell.querySelector('a')).toBeNull();
+            expect(dispatch).not.toHaveBeenCalled();
+            expect(dialogRef.close).not.toHaveBeenCalled();
+        });
+
+        it('renders a remote output port source component as non-clickable', 
() => {
+            const connection = readableConnection({
+                source: { id: 'remote-output-port-id', name: 'Remote Output 
Port' },
+                sourceGroupId: 'remote-process-group-id',
+                sourceType: 'REMOTE_OUTPUT_PORT',
+                destination: { id: 'processor-id', name: 'Processor' },
+                destinationType: 'PROCESSOR'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('downstream', 
[connection], {
+                remoteProcessGroupIds: new Set(['remote-process-group-id'])
+            });
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const sourceComponentCell = getCells(fixture, 
'mat-column-sourceComponent')[0];
+
+            expect(sourceComponentCell.textContent).toContain('Remote Output 
Port');
+            expect(sourceComponentCell.querySelector('span')).not.toBeNull();
+            expect(sourceComponentCell.querySelector('a')).toBeNull();
+            expect(dispatch).not.toHaveBeenCalled();
+            expect(dialogRef.close).not.toHaveBeenCalled();
+        });
+
+        it('renders a remote input port destination component as 
non-clickable', () => {
+            const connection = readableConnection({
+                source: { id: 'processor-id', name: 'Processor' },
+                sourceType: 'PROCESSOR',
+                destination: { id: 'remote-input-port-id', name: 'Remote Input 
Port' },
+                destinationGroupId: 'remote-process-group-id',
+                destinationType: 'REMOTE_INPUT_PORT'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('upstream', 
[connection], {
+                remoteProcessGroupIds: new Set(['remote-process-group-id'])
+            });
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const destinationComponentCell = getCells(fixture, 
'mat-column-destinationComponent')[0];
+
+            expect(destinationComponentCell.textContent).toContain('Remote 
Input Port');
+            
expect(destinationComponentCell.querySelector('span')).not.toBeNull();
+            expect(destinationComponentCell.querySelector('a')).toBeNull();
+            expect(dispatch).not.toHaveBeenCalled();
+            expect(dialogRef.close).not.toHaveBeenCalled();
+        });
+
+        it('renders a remote output port destination component as 
non-clickable', () => {
+            const connection = readableConnection({
+                source: { id: 'processor-id', name: 'Processor' },
+                sourceType: 'PROCESSOR',
+                destination: { id: 'remote-output-port-id', name: 'Remote 
Output Port' },
+                destinationGroupId: 'remote-process-group-id',
+                destinationType: 'REMOTE_OUTPUT_PORT'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('upstream', 
[connection], {
+                remoteProcessGroupIds: new Set(['remote-process-group-id'])
+            });
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const destinationComponentCell = getCells(fixture, 
'mat-column-destinationComponent')[0];
+
+            expect(destinationComponentCell.textContent).toContain('Remote 
Output Port');
+            
expect(destinationComponentCell.querySelector('span')).not.toBeNull();
+            expect(destinationComponentCell.querySelector('a')).toBeNull();
+            expect(dispatch).not.toHaveBeenCalled();
+            expect(dialogRef.close).not.toHaveBeenCalled();
+        });
+
+        it('continues rendering standard input and output port components as 
clickable', () => {
+            const connection = readableConnection({
+                source: { id: 'output-port-id', name: 'Output Port' },
+                sourceGroupId: SOURCE_GROUP_ID,
+                sourceType: 'OUTPUT_PORT',
+                destination: { id: 'input-port-id', name: 'Input Port' },
+                destinationGroupId: DESTINATION_GROUP_ID,
+                destinationType: 'INPUT_PORT'
+            });
+
+            const { fixture } = createDialog('downstream', [connection]);
+
+            const sourceComponentCell = getCells(fixture, 
'mat-column-sourceComponent')[0];
+            const destinationComponentCell = getCells(fixture, 
'mat-column-destinationComponent')[0];
+
+            expect(sourceComponentCell.querySelector('a')).not.toBeNull();
+            expect(sourceComponentCell.querySelector('span')).toBeNull();
+            expect(destinationComponentCell.querySelector('a')).not.toBeNull();
+            expect(destinationComponentCell.querySelector('span')).toBeNull();
+        });
+
+        it('navigates to the connection in the group that defines the dialog 
request', () => {
+            const connection = readableConnection({
+                id: 'connection-to-navigate-to',
+                name: 'Connection To Navigate To'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('upstream', 
[connection]);
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const connectionCell = getCells(fixture, 
'mat-column-connection')[0];
+            const link = connectionCell.querySelector('a') as 
HTMLAnchorElement;
+            link.click();
+
+            expect(dispatch).toHaveBeenCalledWith(
+                navigateToComponent({
+                    request: {
+                        id: 'connection-to-navigate-to',
+                        processGroupId: REQUEST_GROUP_ID,
+                        type: ComponentType.Connection
+                    }
+                })
+            );
+            expect(dialogRef.close).toHaveBeenCalled();
+        });
+
+        it('navigates to the connection in the group that defines the dialog 
request', () => {
+            const connection = readableConnection({
+                id: 'connection-to-navigate-to',
+                name: 'Connection To Navigate To'
+            });
+
+            const { fixture, store, dialogRef } = createDialog('upstream', 
[connection]);
+            const dispatch = vi.spyOn(store, 'dispatch');
+
+            const connectionCell = getCells(fixture, 
'mat-column-connection')[0];
+            const link = connectionCell.querySelector('a') as 
HTMLAnchorElement;
+            link.click();
+
+            expect(dispatch).toHaveBeenCalledWith(
+                navigateToComponent({
+                    request: {
+                        id: 'connection-to-navigate-to',
+                        processGroupId: REQUEST_GROUP_ID,
+                        type: ComponentType.Connection
+                    }
+                })
+            );
+            expect(dialogRef.close).toHaveBeenCalled();
+        });

Review Comment:
   This test is a duplicate of the one immediately above (same name and 
assertions). Please keep one.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to