rfellows commented on code in PR #11582: URL: https://github.com/apache/nifi/pull/11582#discussion_r3982018111
########## 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,337 @@ +/* + * 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 { provideMockStore, MockStore } from '@ngrx/store/testing'; +import { of } from 'rxjs'; +import { ComponentType } from '@nifi/shared'; + +import { ComponentConnectionsDialog } 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 COMPONENT_ID = 'a1b2c3d4-0000-0000-0000-000000000000'; +const GROUP_ID = 'e5f6a7b8-0000-0000-0000-000000000000'; +const CHILD_GROUP_ID = 'c9d0e1f2-0000-0000-0000-000000000000'; + +interface ConnectableStub { + id: string; + name: string; +} + +/** + * Builds a readable connection between the two supplied endpoints. The endpoints are reported as + * living in the group being viewed, which is the common case; {@link connectionIntoChildGroup} + * covers an endpoint inside a child group. + */ +function connection( + id: string, + source: ConnectableStub, + destination: ConnectableStub, + component: { name?: string; selectedRelationships?: string[] } = {} +): ConnectionEntity { + return { + id, + permissions: { canRead: true, canWrite: true }, + position: { x: 0, y: 0 }, + revision: { version: 0 }, + sourceId: source.id, + sourceGroupId: GROUP_ID, + sourceType: 'PROCESSOR', + destinationId: destination.id, + destinationGroupId: GROUP_ID, + destinationType: 'INPUT_PORT', + component: { + id, + source, + destination, + ...component + } + }; +} + +/** + * Builds a connection the current user cannot read. The API omits the component entirely in that case + * but still reports the endpoints at the top level of the entity. + */ +function unreadableConnection(id: string, sourceId: string, destinationId: string): ConnectionEntity { + return { + id, + permissions: { canRead: false, canWrite: false }, + position: { x: 0, y: 0 }, + revision: { version: 0 }, + sourceId, + sourceGroupId: GROUP_ID, + sourceType: 'PROCESSOR', + destinationId, + destinationGroupId: GROUP_ID, + destinationType: 'INPUT_PORT', + component: null + }; +} + +/** + * Builds a connection from a component in the viewed group to an Input Port inside a child group. + * The canvas draws this as terminating at the child group, but the entity names the port. + */ +function connectionIntoChildGroup( + id: string, + source: ConnectableStub, + innerPort: ConnectableStub, + name: string +): ConnectionEntity { + return { + id, + permissions: { canRead: true, canWrite: true }, + position: { x: 0, y: 0 }, + revision: { version: 0 }, + sourceId: source.id, + sourceGroupId: GROUP_ID, + sourceType: 'PROCESSOR', + destinationId: innerPort.id, + destinationGroupId: CHILD_GROUP_ID, + destinationType: 'INPUT_PORT', + component: { + id, + name, + source, + destination: innerPort + } + }; +} + +interface CreatedDialog { + component: ComponentConnectionsDialog; + fixture: ComponentFixture<ComponentConnectionsDialog>; + store: MockStore; + dialogRef: { close: ReturnType<typeof vi.fn>; keydownEvents: () => ReturnType<typeof of> }; +} + +function createDialog( + direction: ConnectionDirection, + connections: ConnectionEntity[], + componentName = 'In' +): CreatedDialog { + const dialogRequest: ComponentConnectionsDialogRequest = { + componentName, + groupId: GROUP_ID, + direction, + connections + }; + const dialogRef = { close: vi.fn(), keydownEvents: () => of() }; + + // only formatConnectionName is exercised; the real CanvasUtils subscribes to canvas state on + // construction, which this dialog has no need 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({}) Review Comment: These tests currently fail (`npx nx test nifi`: 6 failed in this file). `provideMockStore({})` does not satisfy `selectProcessGroupIdToNameMap`, so `detectChanges()` throws `Cannot read properties of undefined (reading 'flowState')` from `flow.selectors.ts`. The row assertions also omit `groupId`/`type`, and the navigation test still calls `component.goTo(...)`, which this component no longer has (`navigateTo` is the method). Please override `selectProcessGroupIdToNameMap` before `detectChanges()`, update the expected row shape, and call `navigateTo(...)` for the connection (and ideally the other clickable cells). ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.ts: ########## @@ -0,0 +1,203 @@ +/* + * 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, inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { Store } from '@ngrx/store'; +import { CloseOnEscapeDialog, ComponentType } from '@nifi/shared'; +import { CanvasState } from '../../../state'; +import { ComponentConnectionsDialogRequest, ConnectionEntity } from '../../../state/flow'; +import { CanvasUtils } from '../../../service/canvas-utils.service'; +import { navigateToComponent } from '../../../state/flow/flow.actions'; +import { selectProcessGroupIdToNameMap } from '../../../state/flow/flow.selectors'; + +/** + * One end of a connection, with enough information to render a cell and navigate to it. + * - {@code id}: the component's own id. + * - {@code groupId}: the id of the process group that directly contains the component. + * - {@code type}: the component type, used to tell {@code navigateToComponent} what it's looking at. + * - {@code name}: the component name, or {@code null} when the current user cannot read the + * connection, in which case the cell renders an "Unauthorized" placeholder and is not clickable. + */ +export interface ConnectionEndpoint { + id: string; + groupId: string; + type: ComponentType; + name: string | null; +} + +/** + * Row in the connections table. + * - {@code id}: the connection id. + * - {@code name}: the connection name, or the relationships it carries when it has no name. + * {@code null} when it has neither, so the cell renders an "Unnamed" placeholder. + * + * Both ends are listed, along with each end's process group, rather than only the far end. When the + * selected component is a Process Group or Remote Process Group the connection actually terminates at + * a port inside it, and which group and port that is matters as much as the component on the other side. + */ +export interface ComponentConnectionRow { + id: string; + name: string | null; + source: ConnectionEndpoint; + destination: ConnectionEndpoint; +} + +/** + * Lists the connections attached to a component in one direction. For most components those + * connections are already drawn on the canvas, so this is a way to reach one whose other end sits + * somewhere else entirely. For an Input Port's upstream connections and an Output Port's downstream + * connections it is the only way, since those are defined in the parent process group and are not drawn + * alongside the port at all. Each of the 5 cells in a row is independently clickable and navigates + * to the process group, component, or connection it represents. + */ +@Component({ + selector: 'component-connections-dialog', + imports: [MatButtonModule, MatDialogModule, MatTableModule, MatTooltipModule], + templateUrl: './component-connections-dialog.component.html', + styleUrls: ['./component-connections-dialog.component.scss'] +}) +export class ComponentConnectionsDialog extends CloseOnEscapeDialog { + private dialogRequest = inject<ComponentConnectionsDialogRequest>(MAT_DIALOG_DATA); + private componentConnectionsDialogRef = inject<MatDialogRef<ComponentConnectionsDialog>>(MatDialogRef); + private store = inject<Store<CanvasState>>(Store); + private canvasUtils = inject(CanvasUtils); + // Signal-based snapshot — reads current value synchronously, no manual subscribe/unsubscribe. + private groupIdToName = this.store.selectSignal(selectProcessGroupIdToNameMap); + + // Maps the string type returned by the NiFi API to the ComponentType enum used for navigation. + private static readonly TYPE_MAP: Record<string, ComponentType> = { + PROCESSOR: ComponentType.Processor, + INPUT_PORT: ComponentType.InputPort, + OUTPUT_PORT: ComponentType.OutputPort, + REMOTE_INPUT_PORT: ComponentType.RemoteProcessGroup, + REMOTE_OUTPUT_PORT: ComponentType.RemoteProcessGroup, + FUNNEL: ComponentType.Funnel + }; Review Comment: `REMOTE_INPUT_PORT` / `REMOTE_OUTPUT_PORT` are mapped to `ComponentType.RemoteProcessGroup`, but `buildRow` still stores `connection.sourceId` / `destinationId` (the remote *port* id). The source/destination cells then call `navigateTo(row.*.id, row.*.groupId, row.*.type)`, which becomes `/process-groups/{rpgId}/RemoteProcessGroup/{remotePortId}`. Existing connectable mapping in `apps/nifi/src/app/ui/common/utils/component-state.utils.ts` (`getComponentTypeForSource` / `getComponentTypeForDestination`) treats those types as the containing RPG. Canvas endpoint resolution (`CanvasUtils.getConnectionSourceComponentId` / `getConnectionDestinationComponentId`) likewise collapses to the group/RPG id. For remote ports, navigate with the RPG id as `id` and the process group that contains that RPG as `processGroupId` (or open Manage Remote Ports). Please add a remote-port navigation test. ########## 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,134 @@ +<!-- + ~ 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="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="unset neutral-color">{{ emptyMessage }}</div> + } @else { + <div class="listing-table component-connections-table"> + <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"> + <!-- TODO: translate row.source.groupId to process group name --> + <button + type="button" + mat-button + class="link-cell" + [matTooltip]="resolveGroupName(row.source.groupId)" + (click)="navigateTo(row.source.groupId, dialogRequestGroupId, processGroupType)"> + <i class="icon component-type-icon" [class]="componentIcon(processGroupType)"></i> + {{ resolveGroupName(row.source.groupId) }} + </button> + </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="unset neutral-color" [matTooltip]="row.source.id"> + <i class="icon component-type-icon" [class]="componentIcon(row.source.type)"></i> + Unauthorized + </span> + } @else { + <button + type="button" + mat-button + class="link-cell" + [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 }} + </button> Review Comment: Listing tables don’t put Material buttons in data cells. `mat-button` adds padded chrome and fights the ellipsis / `table-layout: fixed` treatment that `.listing-table` already applies to cell text. When a name is itself the navigation target, existing UIs use an `<a>`: - `apps/nifi/src/app/ui/common/controller-service/controller-service-references/controller-service-references.component.html` - `apps/nifi/src/app/ui/common/parameter-references/parameter-references.component.html` - `apps/nifi/src/app/ui/common/process-group-references/process-group-references.component.html` Row actions belong in an overflow menu (`local-changes-table.html`). Please replace all five `mat-button` cells with `<a>` (or `routerLink`) and drop `class="link-cell"`. ```suggestion <a [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,134 @@ +<!-- + ~ 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="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="unset neutral-color">{{ emptyMessage }}</div> + } @else { + <div class="listing-table component-connections-table"> + <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"> + <!-- TODO: translate row.source.groupId to process group name --> + <button + type="button" + mat-button + class="link-cell" + [matTooltip]="resolveGroupName(row.source.groupId)" + (click)="navigateTo(row.source.groupId, dialogRequestGroupId, processGroupType)"> Review Comment: This always navigates with `(id, processGroupId, ProcessGroup) = (row.source.groupId, dialogRequestGroupId, ProcessGroup)`. For connections whose endpoint lives in the group currently on the canvas, those two ids are the same, so `navigateToComponent` routes to `/process-groups/{groupId}/ProcessGroup/{groupId}`. The current process group is not a child in that group’s `processGroups` collection, so nothing is selected or centered. The destination process-group cell (lines 87–92) has the same problem. Please treat “this is the group we’re already in” as non-interactive, or navigate to `/process-groups/{groupId}` without a selected component. Also drop the leftover TODO on line 30 — `resolveGroupName` already does that lookup. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts: ########## @@ -294,3 +295,35 @@ export const selectOverlappingConnections = createSelector( return detectOverlappingConnections(connections, processGroupId); } ); + +// maps the id of every group whose name is known in the current flow to that group's name: the current +// process group, its ancestors via the breadcrumb chain, and the child process groups and remote process +// groups it contains. Remote process groups are included alongside process groups because a connection to +// one reports the remote process group's own id as the source/destination group id of the remote port it +// terminates at, so the two kinds of id are looked up the same way. +export const selectProcessGroupIdToNameMap = createSelector( Review Comment: This map only includes the current breadcrumb chain plus *children of the currently loaded canvas group*. For Input Port upstream / Output Port downstream, `viewComponentConnections$` fetches the *parent* flow (`flow.effects.ts` ~3213–3221) and never puts that parent’s child groups/RPGs into canvas state. `resolveGroupName()` then falls back to the raw UUID (`component-connections-dialog.component.ts` 147–148). That is exactly the cross-boundary case this feature is meant to clarify. Please build the name map from the fetched `ProcessGroupFlowEntity` (plus breadcrumbs as needed) and pass it into the dialog request, rather than reading only current canvas state. ########## 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,134 @@ +<!-- + ~ 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="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="unset neutral-color">{{ emptyMessage }}</div> + } @else { + <div class="listing-table component-connections-table"> Review Comment: This table is an unbounded `listing-table` with no scroll viewport, no sticky header, and no striped rows. With more than a handful of connections, the header and “Selected Component” block scroll away. Please match the existing dialog/listing-table pattern: - Bounded scroll + sticky header: `apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/local-changes-dialog/local-changes-table/local-changes-table.html` (44–45, 104) and `apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.html` (71–72, 124). Pattern: `listing-table flex-1 relative` wrapping `absolute inset-0 overflow-y-auto`, and `*matHeaderRowDef="displayedColumns; sticky: true"`. - Striped rows (`let even = even` + `[class.even]="even"`), same two files plus `apps/nifi/src/app/pages/summary/ui/common/cluster-summary-dialog/connection-cluster-table/connection-cluster-table.component.html` (18–19, 121–125). Keep truncation/tooltips; put the table in that scroll container. ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.html.orig: ########## @@ -0,0 +1,90 @@ +<!-- Review Comment: This looks like a leftover backup of an earlier template (Go To column, no per-cell navigation). Please remove it from the PR so it is not shipped. ########## 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,134 @@ +<!-- + ~ 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="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="unset neutral-color">{{ emptyMessage }}</div> + } @else { + <div class="listing-table component-connections-table"> + <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"> + <!-- TODO: translate row.source.groupId to process group name --> + <button + type="button" + mat-button + class="link-cell" + [matTooltip]="resolveGroupName(row.source.groupId)" + (click)="navigateTo(row.source.groupId, dialogRequestGroupId, processGroupType)"> + <i class="icon component-type-icon" [class]="componentIcon(processGroupType)"></i> + {{ resolveGroupName(row.source.groupId) }} + </button> + </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="unset neutral-color" [matTooltip]="row.source.id"> + <i class="icon component-type-icon" [class]="componentIcon(row.source.type)"></i> + Unauthorized + </span> + } @else { + <button + type="button" + mat-button + class="link-cell" + [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 }} + </button> + } + </td> + </ng-container> + + <ng-container matColumnDef="connection"> + <th mat-header-cell *matHeaderCellDef>Connection</th> + <td mat-cell *matCellDef="let row"> + <button + type="button" + mat-button + class="link-cell" + [matTooltip]="row.name" + (click)="navigateTo(row.id, dialogRequestGroupId, connectionType)"> + <i class="icon component-type-icon" [class]="componentIcon(connectionType)"></i> + @if (row.name === null) { + <span class="unset neutral-color">Unnamed</span> + } @else { + {{ row.name }} + } + </button> + </td> + </ng-container> + + <ng-container matColumnDef="destinationProcessGroup"> + <th mat-header-cell *matHeaderCellDef>Destination<br>Process Group</th> + <td mat-cell *matCellDef="let row"> + <button + type="button" + mat-button + class="link-cell" + [matTooltip]="resolveGroupName(row.destination.groupId)" + (click)="navigateTo(row.destination.groupId, dialogRequestGroupId, processGroupType)"> + <i class="icon component-type-icon" [class]="componentIcon(processGroupType)"></i> + {{ resolveGroupName(row.destination.groupId) }} + </button> + </td> + </ng-container> + + <ng-container matColumnDef="destinationComponent"> + <th mat-header-cell *matHeaderCellDef>Destination<br>Component</th> + <td mat-cell *matCellDef="let row"> + @if (row.destination.name === null) { + <span class="unset neutral-color" [matTooltip]="row.destination.id"> + <i + class="icon component-type-icon" + [class]="componentIcon(row.destination.type)"></i> + Unauthorized + </span> + } @else { + <button + type="button" + mat-button + class="link-cell" + [matTooltip]="row.destination.name" + (click)="navigateTo(row.destination.id, row.destination.groupId, row.destination.type)"> + <i + class="icon component-type-icon" + [class]="componentIcon(row.destination.type)"></i> + {{ row.destination.name }} + </button> + } + </td> + </ng-container> + + <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> + <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> Review Comment: Add the sticky header and striped rows used by other listing tables. ```suggestion <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr> <tr mat-row *matRowDef="let row; let even = even; columns: displayedColumns" [class.even]="even"></tr> ``` ########## nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/component-connections-dialog/component-connections-dialog.component.scss: ########## @@ -0,0 +1,53 @@ +/* + * 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. + */ + +.component-connections-table { + width: 100%; + + table { + table-layout: fixed; + width: 100%; + } + + // flexible columns: truncate instead of pushing siblings out + .mat-column-source, + .mat-column-destination, + .mat-column-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 0; // works with table-layout: fixed to force shrinking + } Review Comment: These selectors (`.mat-column-source`, `.mat-column-destination`, `.mat-column-name`) do not match `displayedColumns` (`sourceProcessGroup`, `sourceComponent`, `connection`, `destinationProcessGroup`, `destinationComponent`), so the truncation rules never apply. Global `.listing-table` in `libs/shared/src/assets/themes/components/_table.scss` already sets `table-layout: fixed` and cell ellipsis. Either retarget the real `mat-column-*` classes or drop the dead rules. The commented-out `.link-cell` / `.cell-text` remnants below should go too. ```suggestion ``` -- 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]
