[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
This commit is contained in:
Scott Aslan 2024-02-29 16:09:32 -05:00 committed by GitHub
parent 455159f6ac
commit f9c1c3f042
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1843 additions and 8 deletions

View File

@ -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] }
];

View File

@ -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
}
})
);
}
},
{

View File

@ -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
);
}
}

View File

@ -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 }>()

View File

@ -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(

View File

@ -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;
}

View File

@ -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';
}

View File

@ -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 }>()
);

View File

@ -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 }
);
}

View File

@ -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;
});
})
);

View File

@ -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));

View File

@ -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

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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%;
}
}

View File

@ -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();
});
});

View File

@ -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
}
})
);
}
}

View File

@ -0,0 +1,40 @@
/*
* 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 { RouterModule, Routes } from '@angular/router';
import { ManageRemotePorts } from './manage-remote-ports.component';
const routes: Routes = [
{
path: '',
component: ManageRemotePorts,
children: [
{
path: ':id',
component: ManageRemotePorts,
children: [{ path: 'edit', component: ManageRemotePorts }]
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ManageRemotePortsRoutingModule {}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -0,0 +1,56 @@
/*
* 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 { ManageRemotePorts } from './manage-remote-ports.component';
import { provideMockStore } from '@ngrx/store/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { initialState } from '../../state/manage-remote-ports/manage-remote-ports.reducer';
describe('ManageRemotePorts', () => {
let component: ManageRemotePorts;
let fixture: ComponentFixture<ManageRemotePorts>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ManageRemotePorts],
imports: [RouterTestingModule, MockNavigation, HttpClientTestingModule],
providers: [
provideMockStore({
initialState
})
]
});
fixture = TestBed.createComponent(ManageRemotePorts);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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());
}
}

View File

@ -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 {}

View File

@ -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.

View File

@ -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, text.connection-to-run-status, text.expiration-icon, text.load-balance-icon, text.penalized-icon, g.connection rect.backpressure-tick.data-size-prediction.prediction-down, g.connection rect.backpressure-tick.object-prediction.prediction-down, text.version-control, .breadcrumb-container, #birdseye, .controller-bulletins .fa, .search-container:hover, .search-container.open, .login-background, table th, .mat-sort-header-arrow, .CodeMirror, #status-history-chart-container, #status-history-chart-control-container, #status-history-chart-control-container,
// some analog colors for headers and hover states, inputs, stats, etc

View File

@ -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, text.connection-to-run-status, text.expiration-icon, text.load-balance-icon, text.penalized-icon, g.connection rect.backpressure-tick.data-size-prediction.prediction-down, g.connection rect.backpressure-tick.object-prediction.prediction-down, text.version-control, .breadcrumb-container, #birdseye, .controller-bulletins .fa, .search-container:hover, .search-container.open, .login-background, table th, .mat-sort-header-arrow, .CodeMirror, #status-history-chart-container, #status-history-chart-control-container, #status-history-chart-control-container,
// some analog colors for headers and hover states, inputs, stats, etc

View File

@ -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);