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 f9c1c3f042 [NIFI-12778] manage remote ports (#8433)
f9c1c3f042 is described below
commit f9c1c3f04291fc7de8add93ce4d485a38fed6b9d
Author: Scott Aslan <[email protected]>
AuthorDate: Thu Feb 29 16:09:32 2024 -0500
[NIFI-12778] manage remote ports (#8433)
* [NIFI-12778] manage remote ports
* update last refreshed timestamp and loadedTimestamp
* address review feedback
* final touches
* address addition review comments
* formatDuration check isDurationBlank
This closes #8433
---
.../feature/flow-designer-routing.module.ts | 13 +
.../service/canvas-context-menu.service.ts | 15 +-
.../service/manage-remote-port.service.ts | 72 +++++
.../pages/flow-designer/state/flow/flow.actions.ts | 8 +-
.../pages/flow-designer/state/flow/flow.effects.ts | 12 +
.../app/pages/flow-designer/state/flow/index.ts | 8 +
.../state/manage-remote-ports/index.ts | 100 ++++++
.../manage-remote-ports.actions.ts | 77 +++++
.../manage-remote-ports.effects.ts | 277 ++++++++++++++++
.../manage-remote-ports.reducer.ts | 75 +++++
.../manage-remote-ports.selectors.ts | 55 ++++
.../controller-services.component.spec.ts | 3 +-
.../_manage-remote-ports.component-theme.scss | 45 +++
.../edit-remote-port.component.html | 72 +++++
.../edit-remote-port.component.scss | 27 ++
.../edit-remote-port.component.spec.ts | 63 ++++
.../edit-remote-port/edit-remote-port.component.ts | 107 ++++++
.../manage-remote-ports-routing.module.ts} | 25 +-
.../manage-remote-ports.component.html | 240 ++++++++++++++
.../manage-remote-ports.component.scss | 38 +++
.../manage-remote-ports.component.spec.ts} | 17 +-
.../manage-remote-ports.component.ts | 358 +++++++++++++++++++++
.../manage-remote-ports.module.ts | 52 +++
.../nifi/src/app/service/nifi-common.service.ts | 27 ++
.../src/main/nifi/src/assets/themes/nifi.scss | 2 +-
.../src/main/nifi/src/assets/themes/purple.scss | 2 +-
.../src/main/nifi/src/styles.scss | 7 +
27 files changed, 1763 insertions(+), 34 deletions(-)
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
index b19a2fa793..5e1c862606 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
@@ -39,6 +39,19 @@ const routes: Routes = [
}
]
},
+ {
+ path: 'remote-process-group/:rpgId',
+ component: FlowDesigner,
+ children: [
+ {
+ path: 'manage-remote-ports',
+ loadChildren: () =>
+
import('../ui/manage-remote-ports/manage-remote-ports.module').then(
+ (m) => m.ManageRemotePortsModule
+ )
+ }
+ ]
+ },
{ path: '', component: RootGroupRedirector, canActivate: [rootGroupGuard] }
];
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
index 01e3f8e440..d7e091bd0c 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts
@@ -32,6 +32,7 @@ import {
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
navigateToManageComponentPolicies,
+ navigateToManageRemotePorts,
navigateToProvenanceForComponent,
navigateToQueueListing,
navigateToViewStatusHistoryForComponent,
@@ -771,12 +772,20 @@ export class CanvasContextMenu implements
ContextMenuDefinitionProvider {
},
{
condition: (selection: any) => {
- return this.canvasUtils.isRemoteProcessGroup(selection);
+ return this.canvasUtils.canRead(selection) &&
this.canvasUtils.isRemoteProcessGroup(selection);
},
clazz: 'fa fa-cloud',
text: 'Manage remote ports',
- action: () => {
- // TODO - remotePorts
+ action: (selection: any) => {
+ const selectionData = selection.datum();
+
+ this.store.dispatch(
+ navigateToManageRemotePorts({
+ request: {
+ id: selectionData.id
+ }
+ })
+ );
}
},
{
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manage-remote-port.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manage-remote-port.service.ts
new file mode 100644
index 0000000000..f923cdb8b2
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manage-remote-port.service.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { ConfigureRemotePortRequest, ToggleRemotePortTransmissionRequest }
from '../state/manage-remote-ports';
+import { Client } from '../../../service/client.service';
+import { ComponentType } from '../../../state/shared';
+
+@Injectable({ providedIn: 'root' })
+export class ManageRemotePortService {
+ private static readonly API: string = '../nifi-api';
+
+ constructor(
+ private httpClient: HttpClient,
+ private client: Client,
+ private nifiCommon: NiFiCommon
+ ) {}
+
+ getRemotePorts(rpgId: string): Observable<any> {
+ return
this.httpClient.get(`${ManageRemotePortService.API}/remote-process-groups/${rpgId}`);
+ }
+
+ updateRemotePort(configureRemotePortRequest: ConfigureRemotePortRequest):
Observable<any> {
+ const type =
+ configureRemotePortRequest.payload.type ===
ComponentType.InputPort ? 'input-ports' : 'output-ports';
+ return this.httpClient.put(
+
`${this.nifiCommon.stripProtocol(configureRemotePortRequest.uri)}/${type}/${
+ configureRemotePortRequest.payload.remoteProcessGroupPort.id
+ }`,
+ {
+ revision: configureRemotePortRequest.payload.revision,
+ remoteProcessGroupPort:
configureRemotePortRequest.payload.remoteProcessGroupPort,
+ disconnectedNodeAcknowledged:
configureRemotePortRequest.payload.disconnectedNodeAcknowledged
+ }
+ );
+ }
+
+ updateRemotePortTransmission(
+ toggleRemotePortTransmissionRequest:
ToggleRemotePortTransmissionRequest
+ ): Observable<any> {
+ const payload: any = {
+ revision:
this.client.getRevision(toggleRemotePortTransmissionRequest.rpg),
+ disconnectedNodeAcknowledged:
toggleRemotePortTransmissionRequest.disconnectedNodeAcknowledged,
+ state: toggleRemotePortTransmissionRequest.state
+ };
+
+ const type =
+ toggleRemotePortTransmissionRequest.type ===
ComponentType.InputPort ? 'input-ports' : 'output-ports';
+
+ return this.httpClient.put(
+
`${ManageRemotePortService.API}/remote-process-groups/${toggleRemotePortTransmissionRequest.rpg.id}/${type}/${toggleRemotePortTransmissionRequest.portId}/run-status`,
+ payload
+ );
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 7dfb10a634..f2276dca01 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -76,7 +76,8 @@ import {
ImportFromRegistryDialogRequest,
ImportFromRegistryRequest,
GoToRemoteProcessGroupRequest,
- RefreshRemoteProcessGroupRequest
+ RefreshRemoteProcessGroupRequest,
+ RpgManageRemotePortsRequest
} from './index';
import { StatusHistoryRequest } from '../../../../state/status-history';
@@ -400,6 +401,11 @@ export const openEditRemoteProcessGroupDialog =
createAction(
props<{ request: EditComponentDialogRequest }>()
);
+export const navigateToManageRemotePorts = createAction(
+ `${CANVAS_PREFIX} Open Remote Process Group Manage Remote Ports`,
+ props<{ request: RpgManageRemotePortsRequest }>()
+);
+
export const updateComponent = createAction(
`${CANVAS_PREFIX} Update Component`,
props<{ request: UpdateComponentRequest }>()
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index cd0015d8df..8c0716c826 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -1319,6 +1319,18 @@ export class FlowEffects {
{ dispatch: false }
);
+ navigateToManageRemotePorts$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(FlowActions.navigateToManageRemotePorts),
+ map((action) => action.request),
+ tap((request) => {
+ this.router.navigate(['/remote-process-group', request.id,
'manage-remote-ports']);
+ })
+ ),
+ { dispatch: false }
+ );
+
openEditRemoteProcessGroupDialog$ = createEffect(
() =>
this.actions$.pipe(
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
index 384451a8bc..2d929ced3b 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -270,6 +270,14 @@ export interface EditComponentDialogRequest {
entity: any;
}
+export interface EditRemotePortDialogRequest extends
EditComponentDialogRequest {
+ rpg?: any;
+}
+
+export interface RpgManageRemotePortsRequest {
+ id: string;
+}
+
export interface NavigateToControllerServicesRequest {
id: string;
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/index.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/index.ts
new file mode 100644
index 0000000000..4f547e56a9
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/index.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { ComponentType } from '../../../../state/shared';
+
+export const remotePortsFeatureKey = 'remotePortListing';
+
+export interface PortSummary {
+ batchSettings: {
+ count?: number;
+ size?: string;
+ duration?: string;
+ };
+ comments: string;
+ concurrentlySchedulableTaskCount: number;
+ connected: boolean;
+ exists: boolean;
+ groupId: string;
+ id: string;
+ name: string;
+ targetId: string;
+ targetRunning: boolean;
+ transmitting: boolean;
+ useCompression: boolean;
+ versionedComponentId: string;
+ type?: ComponentType.InputPort | ComponentType.OutputPort;
+}
+
+export interface EditRemotePortDialogRequest {
+ id: string;
+ port: PortSummary;
+ rpg: any;
+}
+
+export interface ToggleRemotePortTransmissionRequest {
+ rpg: any;
+ portId: string;
+ disconnectedNodeAcknowledged: boolean;
+ state: string;
+ type: ComponentType.InputPort | ComponentType.OutputPort | undefined;
+}
+
+export interface StartRemotePortTransmissionRequest {
+ rpg: any;
+ port: PortSummary;
+}
+
+export interface StopRemotePortTransmissionRequest {
+ rpg: any;
+ port: PortSummary;
+}
+
+export interface LoadRemotePortsRequest {
+ rpgId: string;
+}
+
+export interface LoadRemotePortsResponse {
+ ports: PortSummary[];
+ rpg: any;
+ loadedTimestamp: string;
+}
+
+export interface ConfigureRemotePortRequest {
+ id: string;
+ uri: string;
+ payload: any;
+ postUpdateNavigation?: string[];
+}
+
+export interface ConfigureRemotePortSuccess {
+ id: string;
+ port: any;
+}
+
+export interface SelectRemotePortRequest {
+ rpgId: string;
+ id: string;
+}
+
+export interface RemotePortsState {
+ ports: PortSummary[];
+ saving: boolean;
+ rpg: any;
+ loadedTimestamp: string;
+ status: 'pending' | 'loading' | 'success';
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.actions.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.actions.ts
new file mode 100644
index 0000000000..a084dc5b2d
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.actions.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { createAction, props } from '@ngrx/store';
+import {
+ ConfigureRemotePortRequest,
+ ConfigureRemotePortSuccess,
+ EditRemotePortDialogRequest,
+ LoadRemotePortsRequest,
+ LoadRemotePortsResponse,
+ SelectRemotePortRequest,
+ StartRemotePortTransmissionRequest,
+ StopRemotePortTransmissionRequest
+} from './index';
+
+export const resetRemotePortsState = createAction('[Manage Remote Ports] Reset
Remote Ports State');
+
+export const loadRemotePorts = createAction(
+ '[Manage Remote Ports] Load Remote Ports',
+ props<{ request: LoadRemotePortsRequest }>()
+);
+
+export const loadRemotePortsSuccess = createAction(
+ '[Manage Remote Ports] Load Remote Ports Success',
+ props<{ response: LoadRemotePortsResponse }>()
+);
+
+export const remotePortsBannerApiError = createAction(
+ '[Manage Remote Ports] Remote Ports Banner Api Error',
+ props<{ error: string }>()
+);
+
+export const navigateToEditPort = createAction('[Manage Remote Ports] Navigate
To Edit Port', props<{ id: string }>());
+
+export const openConfigureRemotePortDialog = createAction(
+ '[Manage Remote Ports] Open Configure Port Dialog',
+ props<{ request: EditRemotePortDialogRequest }>()
+);
+
+export const configureRemotePort = createAction(
+ '[Manage Remote Ports] Configure Port',
+ props<{ request: ConfigureRemotePortRequest }>()
+);
+
+export const configureRemotePortSuccess = createAction(
+ '[Manage Remote Ports] Configure Port Success',
+ props<{ response: ConfigureRemotePortSuccess }>()
+);
+
+export const startRemotePortTransmission = createAction(
+ '[Manage Remote Ports] Start Port Transmission',
+ props<{ request: StartRemotePortTransmissionRequest }>()
+);
+
+export const stopRemotePortTransmission = createAction(
+ '[Manage Remote Ports] Stop Port Transmission',
+ props<{ request: StopRemotePortTransmissionRequest }>()
+);
+
+export const selectRemotePort = createAction(
+ '[Manage Remote Ports] Select Port Summary',
+ props<{ request: SelectRemotePortRequest }>()
+);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.effects.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.effects.ts
new file mode 100644
index 0000000000..7866350688
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.effects.ts
@@ -0,0 +1,277 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Actions, concatLatestFrom, createEffect, ofType } from
'@ngrx/effects';
+import * as ManageRemotePortsActions from './manage-remote-ports.actions';
+import { catchError, from, map, of, switchMap, tap } from 'rxjs';
+import { MatDialog } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { Router } from '@angular/router';
+import { selectRpg, selectRpgIdFromRoute, selectStatus } from
'./manage-remote-ports.selectors';
+import * as ErrorActions from '../../../../state/error/error.actions';
+import { ErrorHelper } from '../../../../service/error-helper.service';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ManageRemotePortService } from
'../../service/manage-remote-port.service';
+import { PortSummary } from './index';
+import { EditRemotePortComponent } from
'../../ui/manage-remote-ports/edit-remote-port/edit-remote-port.component';
+import { EditRemotePortDialogRequest } from '../flow';
+import { ComponentType, isDefinedAndNotNull } from '../../../../state/shared';
+import { selectTimeOffset } from
'../../../../state/flow-configuration/flow-configuration.selectors';
+import { selectAbout } from '../../../../state/about/about.selectors';
+
+@Injectable()
+export class ManageRemotePortsEffects {
+ constructor(
+ private actions$: Actions,
+ private store: Store<NiFiState>,
+ private manageRemotePortService: ManageRemotePortService,
+ private errorHelper: ErrorHelper,
+ private dialog: MatDialog,
+ private router: Router
+ ) {}
+
+ loadRemotePorts$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.loadRemotePorts),
+ map((action) => action.request),
+ concatLatestFrom(() => [
+ this.store.select(selectStatus),
+
this.store.select(selectTimeOffset).pipe(isDefinedAndNotNull()),
+ this.store.select(selectAbout).pipe(isDefinedAndNotNull())
+ ]),
+ switchMap(([request, status, timeOffset, about]) => {
+ return
this.manageRemotePortService.getRemotePorts(request.rpgId).pipe(
+ map((response) => {
+ // get the current user time to properly convert the
server time
+ const now: Date = new Date();
+
+ // convert the user offset to millis
+ const userTimeOffset: number = now.getTimezoneOffset()
* 60 * 1000;
+
+ // create the proper date by adjusting by the offsets
+ const date: Date = new Date(Date.now() +
userTimeOffset + timeOffset);
+
+ const ports: PortSummary[] = [];
+
+
response.component.contents.inputPorts.forEach((inputPort: PortSummary) => {
+ const port = {
+ ...inputPort,
+ type: ComponentType.InputPort
+ } as PortSummary;
+
+ ports.push(port);
+ });
+
+
response.component.contents.outputPorts.forEach((outputPort: PortSummary) => {
+ const port = {
+ ...outputPort,
+ type: ComponentType.OutputPort
+ } as PortSummary;
+
+ ports.push(port);
+ });
+
+ return
ManageRemotePortsActions.loadRemotePortsSuccess({
+ response: {
+ ports,
+ rpg: response,
+ loadedTimestamp:
`${date.getHours()}:${date.getMinutes()}:${date.getSeconds()} ${
+ about.timezone
+ }`
+ }
+ });
+ }),
+ catchError((errorResponse: HttpErrorResponse) =>
+ of(this.errorHelper.handleLoadingError(status,
errorResponse))
+ )
+ );
+ })
+ )
+ );
+
+ navigateToEditPort$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.navigateToEditPort),
+ map((action) => action.id),
+ concatLatestFrom(() =>
this.store.select(selectRpgIdFromRoute)),
+ tap(([id, rpgId]) => {
+ this.router.navigate(['/remote-process-group', rpgId,
'manage-remote-ports', id, 'edit']);
+ })
+ ),
+ { dispatch: false }
+ );
+
+ remotePortsBannerApiError$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.remotePortsBannerApiError),
+ map((action) => action.error),
+ switchMap((error) => of(ErrorActions.addBannerError({ error })))
+ )
+ );
+
+ startRemotePortTransmission$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.startRemotePortTransmission),
+ map((action) => action.request),
+ switchMap((request) => {
+ return this.manageRemotePortService
+ .updateRemotePortTransmission({
+ portId: request.port.id,
+ rpg: request.rpg,
+ disconnectedNodeAcknowledged: false,
+ type: request.port.type,
+ state: 'TRANSMITTING'
+ })
+ .pipe(
+ map((response) => {
+ return ManageRemotePortsActions.loadRemotePorts({
+ request: {
+ rpgId:
response.remoteProcessGroupPort.groupId
+ }
+ });
+ }),
+ catchError((errorResponse: HttpErrorResponse) =>
+ of(ErrorActions.snackBarError({ error:
errorResponse.error }))
+ )
+ );
+ })
+ )
+ );
+
+ stopRemotePortTransmission$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.stopRemotePortTransmission),
+ map((action) => action.request),
+ switchMap((request) => {
+ return this.manageRemotePortService
+ .updateRemotePortTransmission({
+ portId: request.port.id,
+ rpg: request.rpg,
+ disconnectedNodeAcknowledged: false,
+ type: request.port.type,
+ state: 'STOPPED'
+ })
+ .pipe(
+ map((response) => {
+ return ManageRemotePortsActions.loadRemotePorts({
+ request: {
+ rpgId:
response.remoteProcessGroupPort.groupId
+ }
+ });
+ }),
+ catchError((errorResponse: HttpErrorResponse) =>
+ of(ErrorActions.snackBarError({ error:
errorResponse.error }))
+ )
+ );
+ })
+ )
+ );
+
+ selectRemotePort$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.selectRemotePort),
+ map((action) => action.request),
+ tap((request) => {
+ this.router.navigate(['/remote-process-group',
request.rpgId, 'manage-remote-ports', request.id]);
+ })
+ ),
+ { dispatch: false }
+ );
+
+ openConfigureRemotePortDialog$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.openConfigureRemotePortDialog),
+ map((action) => action.request),
+ concatLatestFrom(() =>
[this.store.select(selectRpg).pipe(isDefinedAndNotNull())]),
+ tap(([request, rpg]) => {
+ const portId: string = request.id;
+
+ const editDialogReference =
this.dialog.open(EditRemotePortComponent, {
+ data: {
+ type: request.port.type,
+ entity: request.port,
+ rpg
+ } as EditRemotePortDialogRequest,
+ id: portId
+ });
+
+ editDialogReference.afterClosed().subscribe((response) => {
+ this.store.dispatch(ErrorActions.clearBannerErrors());
+ if (response != 'ROUTED') {
+ this.store.dispatch(
+ ManageRemotePortsActions.selectRemotePort({
+ request: {
+ rpgId: rpg.id,
+ id: portId
+ }
+ })
+ );
+ }
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ configureRemotePort$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.configureRemotePort),
+ map((action) => action.request),
+ switchMap((request) =>
+
from(this.manageRemotePortService.updateRemotePort(request)).pipe(
+ map((response) =>
+ ManageRemotePortsActions.configureRemotePortSuccess({
+ response: {
+ id: request.id,
+ port: response.remoteProcessGroupPort
+ }
+ })
+ ),
+ catchError((errorResponse: HttpErrorResponse) => {
+ if
(this.errorHelper.showErrorInContext(errorResponse.status)) {
+ return of(
+
ManageRemotePortsActions.remotePortsBannerApiError({
+ error: errorResponse.error
+ })
+ );
+ } else {
+
this.dialog.getDialogById(request.id)?.close('ROUTED');
+ return
of(this.errorHelper.fullScreenError(errorResponse));
+ }
+ })
+ )
+ )
+ )
+ );
+
+ configureRemotePortSuccess$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ManageRemotePortsActions.configureRemotePortSuccess),
+ map((action) => action.response),
+ tap(() => {
+ this.dialog.closeAll();
+ })
+ ),
+ { dispatch: false }
+ );
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.reducer.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.reducer.ts
new file mode 100644
index 0000000000..98b5f244d6
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.reducer.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import {
+ configureRemotePort,
+ configureRemotePortSuccess,
+ loadRemotePorts,
+ loadRemotePortsSuccess,
+ remotePortsBannerApiError,
+ resetRemotePortsState
+} from './manage-remote-ports.actions';
+import { produce } from 'immer';
+import { RemotePortsState } from './index';
+
+export const initialState: RemotePortsState = {
+ ports: [],
+ saving: false,
+ loadedTimestamp: '',
+ rpg: null,
+ status: 'pending'
+};
+
+export const manageRemotePortsReducer = createReducer(
+ initialState,
+ on(resetRemotePortsState, () => ({
+ ...initialState
+ })),
+ on(loadRemotePorts, (state) => ({
+ ...state,
+ status: 'loading' as const
+ })),
+ on(loadRemotePortsSuccess, (state, { response }) => ({
+ ...state,
+ ports: response.ports,
+ loadedTimestamp: response.loadedTimestamp,
+ rpg: response.rpg,
+ status: 'success' as const
+ })),
+ on(remotePortsBannerApiError, (state) => ({
+ ...state,
+ saving: false
+ })),
+ on(configureRemotePort, (state) => ({
+ ...state,
+ saving: true
+ })),
+ on(configureRemotePortSuccess, (state, { response }) => {
+ return produce(state, (draftState) => {
+ const componentIndex: number = draftState.ports.findIndex((f: any)
=> response.id === f.id);
+ const port = {
+ ...response.port,
+ type: state.ports[componentIndex].type
+ };
+ if (componentIndex > -1) {
+ draftState.ports[componentIndex] = port;
+ }
+ draftState.saving = false;
+ });
+ })
+);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.selectors.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.selectors.ts
new file mode 100644
index 0000000000..045f6c70c4
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/manage-remote-ports/manage-remote-ports.selectors.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { createFeatureSelector, createSelector } from '@ngrx/store';
+import { selectCurrentRoute } from '../../../../state/router/router.selectors';
+import { remotePortsFeatureKey, RemotePortsState } from './index';
+
+export const selectRemotePortsState =
createFeatureSelector<RemotePortsState>(remotePortsFeatureKey);
+
+export const selectSaving = createSelector(selectRemotePortsState, (state:
RemotePortsState) => state.saving);
+
+export const selectStatus = createSelector(selectRemotePortsState, (state:
RemotePortsState) => state.status);
+
+export const selectRpgIdFromRoute = createSelector(selectCurrentRoute, (route)
=> {
+ if (route) {
+ // always select the rpg id from the route
+ return route.params.rpgId;
+ }
+ return null;
+});
+
+export const selectPortIdFromRoute = createSelector(selectCurrentRoute,
(route) => {
+ if (route) {
+ // always select the port id from the route
+ return route.params.id;
+ }
+ return null;
+});
+
+export const selectSingleEditedPort = createSelector(selectCurrentRoute,
(route) => {
+ if (route?.routeConfig?.path == 'edit') {
+ return route.params.id;
+ }
+ return null;
+});
+
+export const selectPorts = createSelector(selectRemotePortsState, (state:
RemotePortsState) => state.ports);
+export const selectRpg = createSelector(selectRemotePortsState, (state:
RemotePortsState) => state.rpg);
+
+export const selectPort = (id: string) =>
+ createSelector(selectPorts, (port: any[]) => port.find((port) => id ==
port.id));
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
index fccb8a6661..c384ba515f 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
@@ -22,7 +22,6 @@ import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from
'../../state/controller-services/controller-services.reducer';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
-import { ControllerServicesModule } from './controller-services.module';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('ControllerServices', () => {
@@ -39,7 +38,7 @@ describe('ControllerServices', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ControllerServices],
- imports: [RouterTestingModule, MockNavigation,
ControllerServicesModule, HttpClientTestingModule],
+ imports: [RouterTestingModule, MockNavigation,
HttpClientTestingModule],
providers: [
provideMockStore({
initialState
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/_manage-remote-ports.component-theme.scss
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/_manage-remote-ports.component-theme.scss
new file mode 100644
index 0000000000..30dd724e16
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/_manage-remote-ports.component-theme.scss
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+@use 'sass:map';
+@use '@angular/material' as mat;
+
+@mixin nifi-theme($theme, $canvas-theme) {
+ // Get the color config from the theme.
+ $color-config: mat.get-color-config($theme);
+ $canvas-color-config: mat.get-color-config($canvas-theme);
+
+ // Get the color palette from the color-config.
+ $primary-palette: map.get($color-config, 'primary');
+ $canvas-accent-palette: map.get($canvas-color-config, 'accent');
+
+ // Get hues from palette
+ $primary-palette-500: mat.get-color-from-palette($primary-palette, 500);
+ $canvas-accent-palette-A200:
mat.get-color-from-palette($canvas-accent-palette, 'A200');
+
+ .manage-remote-ports-header {
+ color: $primary-palette-500;
+ }
+
+ .manage-remote-ports-table {
+ .listing-table {
+ .fa.fa-warning {
+ color: $canvas-accent-palette-A200;
+ }
+ }
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.html
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.html
new file mode 100644
index 0000000000..d2cf80e4d6
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.html
@@ -0,0 +1,72 @@
+<!--
+ ~ 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>Edit Remote {{ portTypeLabel }}</h2>
+<form class="edit-remote-port-form" [formGroup]="editPortForm">
+ <error-banner></error-banner>
+ <mat-dialog-content>
+ <div>
+ <div class="flex flex-col mb-5">
+ <div>Name</div>
+ <div class="overflow-ellipsis overflow-hidden
whitespace-nowrap value" [title]="request.entity.name">
+ {{ request.entity.name }}
+ </div>
+ </div>
+ </div>
+ <div>
+ <mat-form-field>
+ <mat-label>Concurrent Tasks</mat-label>
+ <input matInput formControlName="concurrentTasks" type="text"
/>
+ </mat-form-field>
+ </div>
+ <div class="mb-3.5">
+ <mat-label>Compressed</mat-label>
+ <mat-checkbox color="primary"
formControlName="compressed"></mat-checkbox>
+ </div>
+ <div>
+ <mat-form-field>
+ <mat-label>Batch Count</mat-label>
+ <input matInput formControlName="count" type="text" />
+ </mat-form-field>
+ </div>
+ <div>
+ <mat-form-field>
+ <mat-label>Batch Size</mat-label>
+ <input matInput formControlName="size" type="text" />
+ </mat-form-field>
+ </div>
+ <div>
+ <mat-form-field>
+ <mat-label>Batch Duration</mat-label>
+ <input matInput formControlName="duration" type="text" />
+ </mat-form-field>
+ </div>
+ </mat-dialog-content>
+ @if ({ value: (saving$ | async)! }; as saving) {
+ <mat-dialog-actions align="end">
+ <button color="primary" mat-stroked-button
mat-dialog-close>Cancel</button>
+ <button
+ [disabled]="!editPortForm.dirty || editPortForm.invalid ||
saving.value"
+ type="button"
+ color="primary"
+ (click)="editRemotePort()"
+ mat-raised-button>
+ <span *nifiSpinner="saving.value">Apply</span>
+ </button>
+ </mat-dialog-actions>
+ }
+</form>
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.scss
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.scss
new file mode 100644
index 0000000000..b4a795bb96
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.scss
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+@use '@angular/material' as mat;
+
+.edit-remote-port-form {
+ @include mat.button-density(-1);
+
+ width: 500px;
+ .mat-mdc-form-field {
+ width: 100%;
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.spec.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.spec.ts
new file mode 100644
index 0000000000..078fb0e334
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.spec.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { EditRemotePortComponent } from './edit-remote-port.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { provideMockStore } from '@ngrx/store/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { EditComponentDialogRequest } from '../../../state/flow';
+import { ComponentType } from '../../../../../state/shared';
+import { initialState } from
'../../../state/manage-remote-ports/manage-remote-ports.reducer';
+
+describe('EditRemotePortComponent', () => {
+ let component: EditRemotePortComponent;
+ let fixture: ComponentFixture<EditRemotePortComponent>;
+
+ const data: EditComponentDialogRequest = {
+ type: ComponentType.OutputPort,
+ uri:
'https://localhost:4200/nifi-api/remote-process-groups/95a4b210-018b-1000-772a-5a9ebfa03287',
+ entity: {
+ id: 'a687e30e-018b-1000-f904-849a9f8e6bdb',
+ groupId: '95a4b210-018b-1000-772a-5a9ebfa03287',
+ name: 'out',
+ transmitting: false,
+ concurrentlySchedulableTaskCount: 1,
+ useCompression: true,
+ batchSettings: {
+ count: '',
+ size: '',
+ duration: ''
+ }
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [EditRemotePortComponent, NoopAnimationsModule],
+ providers: [{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({ initialState })]
+ });
+ fixture = TestBed.createComponent(EditRemotePortComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.ts
new file mode 100644
index 0000000000..d07f664d0d
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/edit-remote-port/edit-remote-port.component.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 } from '@angular/material/dialog';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators
} from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatButtonModule } from '@angular/material/button';
+import { AsyncPipe } from '@angular/common';
+import { ErrorBanner } from
'../../../../../ui/common/error-banner/error-banner.component';
+import { NifiSpinnerDirective } from
'../../../../../ui/common/spinner/nifi-spinner.directive';
+import { selectSaving } from
'../../../state/manage-remote-ports/manage-remote-ports.selectors';
+import { EditRemotePortDialogRequest } from '../../../state/flow';
+import { Client } from '../../../../../service/client.service';
+import { ComponentType } from '../../../../../state/shared';
+import { PortSummary } from '../../../state/manage-remote-ports';
+import { configureRemotePort } from
'../../../state/manage-remote-ports/manage-remote-ports.actions';
+
+@Component({
+ standalone: true,
+ templateUrl: './edit-remote-port.component.html',
+ imports: [
+ ReactiveFormsModule,
+ ErrorBanner,
+ MatDialogModule,
+ MatInputModule,
+ MatCheckboxModule,
+ MatButtonModule,
+ AsyncPipe,
+ NifiSpinnerDirective
+ ],
+ styleUrls: ['./edit-remote-port.component.scss']
+})
+export class EditRemotePortComponent {
+ saving$ = this.store.select(selectSaving);
+
+ editPortForm: FormGroup;
+ portTypeLabel: string;
+
+ constructor(
+ @Inject(MAT_DIALOG_DATA) public request: EditRemotePortDialogRequest,
+ private formBuilder: FormBuilder,
+ private store: Store<CanvasState>,
+ private client: Client
+ ) {
+ // set the port type name
+ if (ComponentType.InputPort == this.request.type) {
+ this.portTypeLabel = 'Input Port';
+ } else {
+ this.portTypeLabel = 'Output Port';
+ }
+
+ // build the form
+ this.editPortForm = this.formBuilder.group({
+ concurrentTasks: new
FormControl(request.entity.concurrentlySchedulableTaskCount,
Validators.required),
+ compressed: new FormControl(request.entity.useCompression),
+ count: new FormControl(request.entity.batchSettings.count),
+ size: new FormControl(request.entity.batchSettings.size),
+ duration: new FormControl(request.entity.batchSettings.duration)
+ });
+ }
+
+ editRemotePort() {
+ const payload: any = {
+ revision: this.client.getRevision(this.request.rpg),
+ disconnectedNodeAcknowledged: false,
+ type: this.request.type,
+ remoteProcessGroupPort: {
+ concurrentlySchedulableTaskCount:
this.editPortForm.get('concurrentTasks')?.value,
+ useCompression: this.editPortForm.get('compressed')?.value,
+ batchSettings: {
+ count: this.editPortForm.get('count')?.value,
+ size: this.editPortForm.get('size')?.value,
+ duration: this.editPortForm.get('duration')?.value
+ },
+ id: this.request.entity.id,
+ groupId: this.request.entity.groupId
+ } as PortSummary
+ };
+
+ this.store.dispatch(
+ configureRemotePort({
+ request: {
+ id: this.request.entity.id,
+ uri: this.request.rpg.uri,
+ payload
+ }
+ })
+ );
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports-routing.module.ts
similarity index 56%
copy from
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
copy to
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports-routing.module.ts
index b19a2fa793..db63639f5c 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/feature/flow-designer-routing.module.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports-routing.module.ts
@@ -17,33 +17,24 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
-import { FlowDesigner } from './flow-designer.component';
-import { RootGroupRedirector } from
'../ui/root/redirector/root-group-redirector.component';
-import { rootGroupGuard } from '../ui/root/guard/root-group.guard';
+import { ManageRemotePorts } from './manage-remote-ports.component';
const routes: Routes = [
{
- path: 'process-groups/:processGroupId',
- component: FlowDesigner,
+ path: '',
+ component: ManageRemotePorts,
children: [
{
- path: 'controller-services',
- loadChildren: () =>
-
import('../ui/controller-service/controller-services.module').then(
- (m) => m.ControllerServicesModule
- )
- },
- {
- path: '',
- loadChildren: () =>
import('../ui/canvas/canvas.module').then((m) => m.CanvasModule)
+ path: ':id',
+ component: ManageRemotePorts,
+ children: [{ path: 'edit', component: ManageRemotePorts }]
}
]
- },
- { path: '', component: RootGroupRedirector, canActivate: [rootGroupGuard] }
+ }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
-export class FlowDesignerRoutingModule {}
+export class ManageRemotePortsRoutingModule {}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.html
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.html
new file mode 100644
index 0000000000..a4647849b5
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.html
@@ -0,0 +1,240 @@
+<!--
+ ~ 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="pb-5 flex flex-col h-screen justify-between gap-y-5">
+ <header class="nifi-header">
+ <navigation></navigation>
+ </header>
+ <div class="px-5 flex-1 flex flex-col">
+ <h3 class="text-xl bold manage-remote-ports-header pb-5">Manage Remote
Ports</h3>
+ @if (portsState$ | async; as portsState) {
+ <div class="grid-container grid grid-cols-2">
+ <div class="col-span-1 pr-5">
+ <div class="flex flex-col mb-5">
+ <div>Name</div>
+ <div
+ class="overflow-ellipsis overflow-hidden
whitespace-nowrap value"
+ [title]="portsState.rpg?.id">
+ {{ portsState.rpg?.id }}
+ </div>
+ </div>
+ </div>
+ <div class="col-span-1">
+ <div class="flex flex-col mb-5">
+ <div>Urls</div>
+ <div
+ class="overflow-ellipsis overflow-hidden
whitespace-nowrap value"
+ [title]="portsState.rpg?.component?.targetUri">
+ {{ portsState.rpg?.component?.targetUri }}
+ </div>
+ </div>
+ </div>
+ </div>
+ @if (isInitialLoading(portsState)) {
+ <div>
+ <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+ </div>
+ } @else {
+ <div class="flex flex-col h-full gap-y-2">
+ <div class="flex-1">
+ <div class="manage-remote-ports-table relative h-full
border">
+ <div class="listing-table absolute inset-0
overflow-y-auto">
+ <table
+ mat-table
+ [dataSource]="dataSource"
+ matSort
+ matSortDisableClear
+ (matSortChange)="sortData($event)"
+ [matSortActive]="initialSortColumn"
+ [matSortDirection]="initialSortDirection">
+ <!-- More Details Column -->
+ <ng-container matColumnDef="moreDetails">
+ <th mat-header-cell
*matHeaderCellDef></th>
+ <td mat-cell *matCellDef="let item">
+ <div class="flex items-center
gap-x-3">
+ @if (hasComments(item)) {
+ <div>
+ <div
+ class="pointer fa
fa-comment"
+ nifiTooltip
+
[delayClose]="false"
+
[tooltipComponentType]="TextTip"
+
[tooltipInputData]="getCommentsTipData(item)"></div>
+ </div>
+ }
+ @if (portExists(item)) {
+ <div>
+ <div
+ class="pointer fa
fa-warning"
+ nifiTooltip
+
[delayClose]="false"
+
[tooltipComponentType]="TextTip"
+
[tooltipInputData]="getDisconnectedTipData()"></div>
+ </div>
+ }
+ </div>
+ </td>
+ </ng-container>
+
+ <!-- Name Column -->
+ <ng-container matColumnDef="name">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">Name</div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatName(item)">
+ {{ formatName(item) }}
+ </td>
+ </ng-container>
+
+ <!-- Type Column -->
+ <ng-container matColumnDef="type">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">Type</div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatType(item)">
+ {{ formatType(item) }}
+ </td>
+ </ng-container>
+
+ <!-- Tasks Column -->
+ <ng-container matColumnDef="tasks">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">
+ Concurrent Tasks
+ </div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatTasks(item)">
+ <span
[class.blank]="!item.concurrentlySchedulableTaskCount">
+ {{ formatTasks(item) }}
+ </span>
+ </td>
+ </ng-container>
+
+ <!-- Compression Column -->
+ <ng-container matColumnDef="compression">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">
+ Compressed
+ </div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatCompression(item)">
+ {{ formatCompression(item) }}
+ </td>
+ </ng-container>
+
+ <!-- Batch Count Column -->
+ <ng-container matColumnDef="count">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">
+ Batch Count
+ </div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatCount(item)">
+ <span
[class.blank]="isCountBlank(item)">
+ {{ formatCount(item) }}
+ </span>
+ </td>
+ </ng-container>
+
+ <!-- Batch Size Column -->
+ <ng-container matColumnDef="size">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">
+ Batch Size
+ </div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatSize(item)">
+ <span
[class.blank]="isSizeBlank(item)">
+ {{ formatSize(item) }}
+ </span>
+ </td>
+ </ng-container>
+
+ <!-- Batch Duration Column -->
+ <ng-container matColumnDef="duration">
+ <th mat-header-cell *matHeaderCellDef
mat-sort-header>
+ <div class="overflow-ellipsis
overflow-hidden whitespace-nowrap">
+ Batch Duration
+ </div>
+ </th>
+ <td mat-cell *matCellDef="let item"
[title]="formatDuration(item)">
+ <span
[class.blank]="isDurationBlank(item)">
+ {{ formatDuration(item) }}
+ </span>
+ </td>
+ </ng-container>
+
+ <!-- Actions Column -->
+ <ng-container matColumnDef="actions">
+ <th mat-header-cell
*matHeaderCellDef></th>
+ <td mat-cell *matCellDef="let port">
+ <div class="flex items-center
gap-x-3">
+ @if (
+ port.exists === true &&
+ port.connected === true &&
+ port.transmitting === false
+ ) {
+ <div
+ class="pointer fa
fa-pencil"
+
(click)="configureClicked(port, $event)"
+ title="Edit
Port"></div>
+ }
+
+ @if (currentRpg) {
+ @if (port.transmitting) {
+ <div
+ class="pointer
transmitting fa fa-bullseye"
+
(click)="toggleTransmission(port)"
+
title="Transmitting: click to toggle port transmission"></div>
+ } @else {
+ @if (port.connected &&
port.exists) {
+ <div
+ class="pointer
not-transmitting icon icon-transmit-false"
+
(click)="toggleTransmission(port)"
+ title="Not
Transmitting: click to toggle port transmission"></div>
+ }
+ }
+ }
+ </div>
+ </td>
+ </ng-container>
+
+ <tr mat-header-row
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
+ <tr
+ mat-row
+ *matRowDef="let row; let even = even;
columns: displayedColumns"
+ (click)="select(row)"
+ [class.selected]="isSelected(row)"
+ [class.even]="even"></tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="flex justify-between">
+ <div class="refresh-container flex items-center
gap-x-2">
+ <button class="nifi-button"
(click)="refreshManageRemotePortsListing()">
+ <i class="fa fa-refresh"
[class.fa-spin]="portsState.status === 'loading'"></i>
+ </button>
+ <div>Last updated:</div>
+ <div class="refresh-timestamp">{{
portsState.loadedTimestamp }}</div>
+ </div>
+ </div>
+ </div>
+ }
+ }
+ </div>
+</div>
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.scss
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.scss
new file mode 100644
index 0000000000..c998c77801
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.scss
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+@use '@angular/material' as mat;
+
+.refresh-container {
+ line-height: normal;
+}
+
+.manage-remote-ports-table.listing-table {
+ @include mat.table-density(-4);
+
+ table {
+ .mat-column-moreDetails {
+ width: 32px;
+ min-width: 32px;
+ }
+
+ .mat-column-actions {
+ width: 75px;
+ min-width: 75px;
+ }
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.spec.ts
similarity index 73%
copy from
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
copy to
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.spec.ts
index fccb8a6661..987754a900 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/controller-service/controller-services.component.spec.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.spec.ts
@@ -17,17 +17,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ControllerServices } from './controller-services.component';
+import { ManageRemotePorts } from './manage-remote-ports.component';
import { provideMockStore } from '@ngrx/store/testing';
-import { initialState } from
'../../state/controller-services/controller-services.reducer';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
-import { ControllerServicesModule } from './controller-services.module';
import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { initialState } from
'../../state/manage-remote-ports/manage-remote-ports.reducer';
-describe('ControllerServices', () => {
- let component: ControllerServices;
- let fixture: ComponentFixture<ControllerServices>;
+describe('ManageRemotePorts', () => {
+ let component: ManageRemotePorts;
+ let fixture: ComponentFixture<ManageRemotePorts>;
@Component({
selector: 'navigation',
@@ -38,15 +37,15 @@ describe('ControllerServices', () => {
beforeEach(() => {
TestBed.configureTestingModule({
- declarations: [ControllerServices],
- imports: [RouterTestingModule, MockNavigation,
ControllerServicesModule, HttpClientTestingModule],
+ declarations: [ManageRemotePorts],
+ imports: [RouterTestingModule, MockNavigation,
HttpClientTestingModule],
providers: [
provideMockStore({
initialState
})
]
});
- fixture = TestBed.createComponent(ControllerServices);
+ fixture = TestBed.createComponent(ManageRemotePorts);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.ts
new file mode 100644
index 0000000000..6e7388e30d
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component.ts
@@ -0,0 +1,358 @@
+/*
+ * 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, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { filter, switchMap, take, tap } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import {
+ selectRemotePortsState,
+ selectPort,
+ selectPortIdFromRoute,
+ selectPorts,
+ selectRpg,
+ selectRpgIdFromRoute,
+ selectSingleEditedPort
+} from '../../state/manage-remote-ports/manage-remote-ports.selectors';
+import { RemotePortsState, PortSummary } from
'../../state/manage-remote-ports';
+import {
+ loadRemotePorts,
+ navigateToEditPort,
+ openConfigureRemotePortDialog,
+ resetRemotePortsState,
+ selectRemotePort,
+ startRemotePortTransmission,
+ stopRemotePortTransmission
+} from '../../state/manage-remote-ports/manage-remote-ports.actions';
+import { initialState } from
'../../state/manage-remote-ports/manage-remote-ports.reducer';
+import { isDefinedAndNotNull, TextTipInput } from '../../../../state/shared';
+import { selectCurrentUser } from
'../../../../state/current-user/current-user.selectors';
+import { NiFiState } from '../../../../state';
+import { NiFiCommon } from '../../../../service/nifi-common.service';
+import { MatTableDataSource } from '@angular/material/table';
+import { Sort } from '@angular/material/sort';
+import { TextTip } from
'../../../../ui/common/tooltips/text-tip/text-tip.component';
+import { concatLatestFrom } from '@ngrx/effects';
+import { loadFlowConfiguration } from
'../../../../state/flow-configuration/flow-configuration.actions';
+import {
+ selectFlowConfiguration,
+ selectTimeOffset
+} from '../../../../state/flow-configuration/flow-configuration.selectors';
+import { selectAbout } from '../../../../state/about/about.selectors';
+import { loadAbout } from '../../../../state/about/about.actions';
+
+@Component({
+ templateUrl: './manage-remote-ports.component.html',
+ styleUrls: ['./manage-remote-ports.component.scss']
+})
+export class ManageRemotePorts implements OnInit, OnDestroy {
+ initialSortColumn: 'name' | 'type' | 'tasks' | 'count' | 'size' |
'duration' | 'compression' | 'actions' = 'name';
+ initialSortDirection: 'asc' | 'desc' = 'asc';
+ activeSort: Sort = {
+ active: this.initialSortColumn,
+ direction: this.initialSortDirection
+ };
+ portsState$ = this.store.select(selectRemotePortsState);
+ selectedRpgId$ = this.store.select(selectRpgIdFromRoute);
+ selectedPortId!: string;
+ currentUser$ = this.store.select(selectCurrentUser);
+ flowConfiguration$ =
this.store.select(selectFlowConfiguration).pipe(isDefinedAndNotNull());
+ displayedColumns: string[] = [
+ 'moreDetails',
+ 'name',
+ 'type',
+ 'tasks',
+ 'count',
+ 'size',
+ 'duration',
+ 'compression',
+ 'actions'
+ ];
+ dataSource: MatTableDataSource<PortSummary> = new
MatTableDataSource<PortSummary>([]);
+ protected readonly TextTip = TextTip;
+
+ private currentRpgId!: string;
+ protected currentRpg: any | null = null;
+
+ constructor(
+ private store: Store<NiFiState>,
+ private nifiCommon: NiFiCommon
+ ) {
+ // load the ports after the flow configuration `timeOffset` and about
`timezone` are loaded into the store
+ this.store
+ .select(selectTimeOffset)
+ .pipe(
+ isDefinedAndNotNull(),
+ switchMap(() => this.store.select(selectAbout)),
+ isDefinedAndNotNull(),
+ switchMap(() => this.store.select(selectRpgIdFromRoute)),
+ tap((rpgId) => (this.currentRpgId = rpgId)),
+ takeUntilDestroyed()
+ )
+ .subscribe((rpgId) => {
+ this.store.dispatch(
+ loadRemotePorts({
+ request: {
+ rpgId
+ }
+ })
+ );
+ });
+
+ // track selection using the port id from the route
+ this.store
+ .select(selectPortIdFromRoute)
+ .pipe(isDefinedAndNotNull(), takeUntilDestroyed())
+ .subscribe((portId) => {
+ this.selectedPortId = portId;
+ });
+
+ // data for table
+ this.store
+ .select(selectPorts)
+ .pipe(isDefinedAndNotNull(), takeUntilDestroyed())
+ .subscribe((ports) => {
+ this.dataSource = new
MatTableDataSource<PortSummary>(this.sortEntities(ports, this.activeSort));
+ });
+
+ // the current RPG Entity
+ this.store
+ .select(selectRpg)
+ .pipe(
+ isDefinedAndNotNull(),
+ tap((rpg) => (this.currentRpg = rpg)),
+ takeUntilDestroyed()
+ )
+ .subscribe();
+
+ // handle editing remote port deep link
+ this.store
+ .select(selectSingleEditedPort)
+ .pipe(
+ isDefinedAndNotNull(),
+ switchMap((id: string) =>
+ this.store.select(selectPort(id)).pipe(
+ filter((entity) => entity != null),
+ take(1)
+ )
+ ),
+ concatLatestFrom(() =>
[this.store.select(selectRpg).pipe(isDefinedAndNotNull())]),
+ takeUntilDestroyed()
+ )
+ .subscribe(([entity, rpg]) => {
+ if (entity) {
+ this.store.dispatch(
+ openConfigureRemotePortDialog({
+ request: {
+ id: entity.id,
+ port: entity,
+ rpg
+ }
+ })
+ );
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ this.store.dispatch(loadFlowConfiguration());
+ this.store.dispatch(loadAbout());
+ }
+
+ isInitialLoading(state: RemotePortsState): boolean {
+ // using the current timestamp to detect the initial load event
+ return state.loadedTimestamp == initialState.loadedTimestamp;
+ }
+
+ refreshManageRemotePortsListing(): void {
+ this.store.dispatch(
+ loadRemotePorts({
+ request: {
+ rpgId: this.currentRpgId
+ }
+ })
+ );
+ }
+
+ formatName(entity: PortSummary): string {
+ return entity.name;
+ }
+
+ formatTasks(entity: PortSummary): string {
+ return entity.concurrentlySchedulableTaskCount ?
`${entity.concurrentlySchedulableTaskCount}` : 'No value set';
+ }
+
+ formatCount(entity: PortSummary): string {
+ if (!this.isCountBlank(entity)) {
+ return `${entity.batchSettings.count}`;
+ }
+ return 'No value set';
+ }
+
+ isCountBlank(entity: PortSummary): boolean {
+ return this.nifiCommon.isUndefined(entity.batchSettings.count);
+ }
+
+ formatSize(entity: PortSummary): string {
+ if (!this.isSizeBlank(entity)) {
+ return `${entity.batchSettings.size}`;
+ }
+ return 'No value set';
+ }
+
+ isSizeBlank(entity: PortSummary): boolean {
+ return this.nifiCommon.isBlank(entity.batchSettings.size);
+ }
+
+ formatDuration(entity: PortSummary): string {
+ if (!this.isDurationBlank(entity)) {
+ return `${entity.batchSettings.duration}`;
+ }
+ return 'No value set';
+ }
+
+ isDurationBlank(entity: PortSummary): boolean {
+ return this.nifiCommon.isBlank(entity.batchSettings.duration);
+ }
+
+ formatCompression(entity: PortSummary): string {
+ return entity.useCompression ? 'Yes' : 'No';
+ }
+
+ formatType(entity: PortSummary): string {
+ return entity.type || '';
+ }
+
+ configureClicked(port: PortSummary, event: MouseEvent): void {
+ event.stopPropagation();
+ this.store.dispatch(
+ navigateToEditPort({
+ id: port.id
+ })
+ );
+ }
+
+ hasComments(entity: PortSummary): boolean {
+ return !this.nifiCommon.isBlank(entity.comments);
+ }
+
+ portExists(entity: PortSummary): boolean {
+ return !entity.exists;
+ }
+
+ getCommentsTipData(entity: PortSummary): TextTipInput {
+ return {
+ text: entity.comments
+ };
+ }
+
+ getDisconnectedTipData(): TextTipInput {
+ return {
+ text: 'This port has been removed.'
+ };
+ }
+
+ toggleTransmission(port: PortSummary): void {
+ if (this.currentRpg) {
+ if (port.transmitting) {
+ this.store.dispatch(
+ stopRemotePortTransmission({
+ request: {
+ rpg: this.currentRpg,
+ port
+ }
+ })
+ );
+ } else {
+ if (port.connected && port.exists) {
+ this.store.dispatch(
+ startRemotePortTransmission({
+ request: {
+ rpg: this.currentRpg,
+ port
+ }
+ })
+ );
+ }
+ }
+ }
+ }
+
+ select(entity: PortSummary): void {
+ this.store.dispatch(
+ selectRemotePort({
+ request: {
+ rpgId: this.currentRpgId,
+ id: entity.id
+ }
+ })
+ );
+ }
+
+ isSelected(entity: any): boolean {
+ if (this.selectedPortId) {
+ return entity.id == this.selectedPortId;
+ }
+ return false;
+ }
+
+ sortData(sort: Sort) {
+ this.activeSort = sort;
+ this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
+ }
+
+ private sortEntities(data: PortSummary[], sort: Sort): PortSummary[] {
+ if (!data) {
+ return [];
+ }
+ return data.slice().sort((a, b) => {
+ const isAsc = sort.direction === 'asc';
+ let retVal = 0;
+
+ switch (sort.active) {
+ case 'name':
+ retVal = this.nifiCommon.compareString(this.formatName(a),
this.formatName(b));
+ break;
+ case 'type':
+ retVal = this.nifiCommon.compareString(this.formatType(a),
this.formatType(b));
+ break;
+ case 'tasks':
+ retVal =
this.nifiCommon.compareString(this.formatTasks(a), this.formatTasks(b));
+ break;
+ case 'compression':
+ retVal =
this.nifiCommon.compareString(this.formatCompression(a),
this.formatCompression(b));
+ break;
+ case 'count':
+ retVal =
this.nifiCommon.compareString(this.formatCount(a), this.formatCount(b));
+ break;
+ case 'size':
+ retVal = this.nifiCommon.compareString(this.formatSize(a),
this.formatSize(b));
+ break;
+ case 'duration':
+ retVal =
this.nifiCommon.compareString(this.formatDuration(a), this.formatDuration(b));
+ break;
+ default:
+ return 0;
+ }
+ return retVal * (isAsc ? 1 : -1);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.store.dispatch(resetRemotePortsState());
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.module.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.module.ts
new file mode 100644
index 0000000000..9578ec9ce8
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.module.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ManageRemotePorts } from './manage-remote-ports.component';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { ControllerServiceTable } from
'../../../../ui/common/controller-service/controller-service-table/controller-service-table.component';
+import { ManageRemotePortsRoutingModule } from
'./manage-remote-ports-routing.module';
+import { Breadcrumbs } from '../common/breadcrumbs/breadcrumbs.component';
+import { Navigation } from
'../../../../ui/common/navigation/navigation.component';
+import { MatTableModule } from '@angular/material/table';
+import { MatSortModule } from '@angular/material/sort';
+import { NifiTooltipDirective } from
'../../../../ui/common/tooltips/nifi-tooltip.directive';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { ManageRemotePortsEffects } from
'../../state/manage-remote-ports/manage-remote-ports.effects';
+import { remotePortsFeatureKey } from '../../state/manage-remote-ports';
+import { manageRemotePortsReducer } from
'../../state/manage-remote-ports/manage-remote-ports.reducer';
+
+@NgModule({
+ declarations: [ManageRemotePorts],
+ exports: [ManageRemotePorts],
+ imports: [
+ CommonModule,
+ NgxSkeletonLoaderModule,
+ ManageRemotePortsRoutingModule,
+ StoreModule.forFeature(remotePortsFeatureKey,
manageRemotePortsReducer),
+ EffectsModule.forFeature(ManageRemotePortsEffects),
+ ControllerServiceTable,
+ Breadcrumbs,
+ Navigation,
+ MatTableModule,
+ MatSortModule,
+ NifiTooltipDirective
+ ]
+})
+export class ManageRemotePortsModule {}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
index bbe69cfdcd..be8e1b4c12 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
@@ -184,6 +184,33 @@ export class NiFiCommon {
return true;
}
+ /**
+ * Determines if the specified object is defined and not null.
+ *
+ * @argument {object} obj The object to test
+ */
+ public isDefinedAndNotNull(obj: any) {
+ return !this.isUndefined(obj) && !this.isNull(obj);
+ }
+
+ /**
+ * Determines if the specified object is undefined.
+ *
+ * @argument {object} obj The object to test
+ */
+ public isUndefined(obj: any) {
+ return typeof obj === 'undefined';
+ }
+
+ /**
+ * Determines if the specified object is null.
+ *
+ * @argument {object} obj The object to test
+ */
+ public isNull(obj: any) {
+ return obj === null;
+ }
+
/**
* Determines if the specified array is empty. If the specified arg is not
an
* array, then true is returned.
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/nifi.scss
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/nifi.scss
index 34ada14216..b8ff67946d 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/nifi.scss
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/nifi.scss
@@ -170,7 +170,7 @@ $nifi-canvas-dark-palette: (
500: #acacac, // g.connection rect.backpressure-object, g.connection
rect.backpressure-data-size, .cdk-drag-disabled, .resizable-triangle
600: #545454, // .canvas-background, .navigation-control,
.operation-control, .lineage
700: #696060, // .canvas-background, g.component rect.body.unauthorized,
g.component rect.processor-icon-container.unauthorized, g.connection
rect.body.unauthorized, #birdseye, .lineage
- 800: rgba(#6b6464, 0.5), // .even, .remote-process-group-sent-stats,
.processor-stats-in-out, .process-group-queued-stats,
.process-group-read-write-stats
+ 800: rgba(#6b6464, 1), // .even, .remote-process-group-sent-stats,
.processor-stats-in-out, .process-group-queued-stats,
.process-group-read-write-stats
900: rgba(#252424, 0.97), // circle.flowfile-link,
.processor-read-write-stats, .process-group-stats-in-out, .tooltip,
.property-editor, .disabled, .enabled, .stopped, .running, .has-errors,
.invalid, .validating, .transmitting, .not-transmitting, .up-to-date,
.locally-modified, .sync-failure, .stale, .locally-modified-and-stale,
g.component rect.body, text.bulletin-icon, rect.processor-icon-container,
circle.restricted-background, circle.is-primary-background, g.connection
rect.body [...]
// some analog colors for headers and hover states, inputs, stats, etc
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/purple.scss
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/purple.scss
index fe59f65210..40eb88e23a 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/purple.scss
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/assets/themes/purple.scss
@@ -171,7 +171,7 @@ $nifi-canvas-dark-palette: (
500: #acacac, // g.connection rect.backpressure-object, g.connection
rect.backpressure-data-size, .cdk-drag-disabled, .resizable-triangle
600: #545454, // .canvas-background, .navigation-control,
.operation-control, .lineage
700: #696060, // .canvas-background, g.component rect.body.unauthorized,
g.component rect.processor-icon-container.unauthorized, g.connection
rect.body.unauthorized, #birdseye, .lineage
- 800: rgba(#6b6464, 0.5), // .even, .remote-process-group-sent-stats,
.processor-stats-in-out, .process-group-queued-stats,
.process-group-read-write-stats
+ 800: rgba(#6b6464, 1), // .even, .remote-process-group-sent-stats,
.processor-stats-in-out, .process-group-queued-stats,
.process-group-read-write-stats
900: rgba(#252424, 0.97), // circle.flowfile-link,
.processor-read-write-stats, .process-group-stats-in-out, .tooltip,
.property-editor, .disabled, .enabled, .stopped, .running, .has-errors,
.invalid, .validating, .transmitting, .not-transmitting, .up-to-date,
.locally-modified, .sync-failure, .stale, .locally-modified-and-stale,
g.component rect.body, text.bulletin-icon, rect.processor-icon-container,
circle.restricted-background, circle.is-primary-background, g.connection
rect.body [...]
// some analog colors for headers and hover states, inputs, stats, etc
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
index 63e2591dec..145863cc71 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
@@ -41,6 +41,7 @@
@use
'app/pages/flow-designer/ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component-theme'
as create-remote-process-group;
@use 'app/pages/flow-designer/ui/common/banner/banner.component-theme' as
banner;
@use
'app/pages/flow-designer/ui/controller-service/controller-services.component-theme'
as controller-service;
+@use
'app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component-theme'
as manage-remote-ports;
@use 'app/pages/login/feature/login.component-theme' as login;
@use 'app/pages/login/ui/login-form/login-form.component-theme' as login-form;
@use 'app/pages/provenance/feature/provenance.component-theme' as provenance;
@@ -460,6 +461,10 @@ $appFontPath: '~roboto-fontface/fonts';
cursor: pointer;
}
+ .disabled {
+ cursor: not-allowed;
+ }
+
.value,
.refresh-timestamp {
font-weight: 500;
@@ -483,6 +488,7 @@ $appFontPath: '~roboto-fontface/fonts';
@include canvas.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include banner.nifi-theme($material-theme-light);
@include controller-service.nifi-theme($material-theme-light);
+@include manage-remote-ports.nifi-theme($material-theme-light,
$nifi-canvas-theme-light);
@include footer.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include navigation-control.nifi-theme($material-theme-light,
$nifi-canvas-theme-light);
@include birdseye-control.nifi-theme($material-theme-light,
$nifi-canvas-theme-light);
@@ -535,6 +541,7 @@ $appFontPath: '~roboto-fontface/fonts';
@include canvas.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);
@include banner.nifi-theme($material-theme-dark);
@include controller-service.nifi-theme($material-theme-dark);
+ @include manage-remote-ports.nifi-theme($material-theme-dark,
$nifi-canvas-theme-dark);
@include footer.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);
@include navigation-control.nifi-theme($material-theme-dark,
$nifi-canvas-theme-dark);
@include birdseye-control.nifi-theme($material-theme-dark,
$nifi-canvas-theme-dark);