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 <scottyas...@users.noreply.github.com> 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);