[NIFI-12758] create remote process group (#8376)

* [NIFI-12758] create remote process group

* address review feedback

* update unit test

This closes #8376
This commit is contained in:
Scott Aslan 2024-02-12 15:27:14 -05:00 committed by GitHub
parent bf86103abf
commit 14fcc42d16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 436 additions and 3 deletions

View File

@ -26,7 +26,6 @@
// Get the color palette from the color-config.
$primary-palette: map.get($color-config, 'primary');
$accent-palette: map.get($color-config, 'accent');
$warn-palette: map.get($color-config, 'warn');
$canvas-primary-palette: map.get($canvas-color-config, 'primary');
$canvas-accent-palette: map.get($canvas-color-config, 'accent');
@ -34,7 +33,6 @@
$primary-palette-300: mat.get-color-from-palette($primary-palette, 300);
$primary-palette-500: mat.get-color-from-palette($primary-palette, 500);
$accent-palette-A400: mat.get-color-from-palette($accent-palette, 'A400');
$warn-palette-500: mat.get-color-from-palette($warn-palette, 500);
$canvas-primary-palette-A200: mat.get-color-from-palette($canvas-primary-palette, 'A200');
$canvas-accent-palette-500: mat.get-color-from-palette($canvas-accent-palette, 500);

View File

@ -25,6 +25,7 @@ import {
CreatePortRequest,
CreateProcessGroupRequest,
CreateProcessorRequest,
CreateRemoteProcessGroupRequest,
DeleteComponentRequest,
ProcessGroupRunStatusRequest,
ReplayLastProvenanceEventRequest,
@ -158,6 +159,32 @@ export class FlowService implements PropertyDescriptorRetriever {
return this.httpClient.post(`${FlowService.API}/process-groups/${processGroupId}/process-groups`, payload);
}
createRemoteProcessGroup(
processGroupId = 'root',
createRemoteProcessGroup: CreateRemoteProcessGroupRequest
): Observable<any> {
const payload: any = {
revision: createRemoteProcessGroup.revision,
component: {
position: createRemoteProcessGroup.position,
targetUris: createRemoteProcessGroup.targetUris,
transportProtocol: createRemoteProcessGroup.transportProtocol,
localNetworkInterface: createRemoteProcessGroup.localNetworkInterface,
proxyHost: createRemoteProcessGroup.proxyHost,
proxyPort: createRemoteProcessGroup.proxyPort,
proxyUser: createRemoteProcessGroup.proxyUser,
proxyPassword: createRemoteProcessGroup.proxyPassword,
communicationsTimeout: createRemoteProcessGroup.communicationsTimeout,
yieldDuration: createRemoteProcessGroup.yieldDuration
}
};
return this.httpClient.post(
`${FlowService.API}/process-groups/${processGroupId}/remote-process-groups`,
payload
);
}
uploadProcessGroup(processGroupId = 'root', uploadProcessGroup: UploadProcessGroupRequest): Observable<any> {
const payload = new FormData();
payload.append('id', processGroupId);

View File

@ -24,6 +24,7 @@ import {
CreateConnectionDialogRequest,
CreateConnectionRequest,
CreatePortRequest,
CreateRemoteProcessGroupRequest,
CreateProcessGroupDialogRequest,
CreateProcessGroupRequest,
CreateProcessorRequest,
@ -210,11 +211,21 @@ export const createFunnel = createAction(
export const createLabel = createAction(`${CANVAS_PREFIX} Create Label`, props<{ request: CreateComponentRequest }>());
export const createRemoteProcessGroup = createAction(
`${CANVAS_PREFIX} Create Remote Process Group`,
props<{ request: CreateRemoteProcessGroupRequest }>()
);
export const openNewProcessGroupDialog = createAction(
`${CANVAS_PREFIX} Open New Process Group Dialog`,
props<{ request: CreateProcessGroupDialogRequest }>()
);
export const openNewRemoteProcessGroupDialog = createAction(
`${CANVAS_PREFIX} Open New Remote Process Group Dialog`,
props<{ request: CreateComponentRequest }>()
);
export const createProcessGroup = createAction(
`${CANVAS_PREFIX} Create Process Group`,
props<{ request: CreateProcessGroupRequest }>()

View File

@ -80,6 +80,7 @@ import { NiFiState } from '../../../../state';
import { CreateProcessor } from '../../ui/canvas/items/processor/create-processor/create-processor.component';
import { EditProcessor } from '../../ui/canvas/items/processor/edit-processor/edit-processor.component';
import { BirdseyeView } from '../../service/birdseye-view.service';
import { CreateRemoteProcessGroup } from '../../ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component';
import { CreateProcessGroup } from '../../ui/canvas/items/process-group/create-process-group/create-process-group.component';
import { CreateConnection } from '../../ui/canvas/items/connection/create-connection/create-connection.component';
import { EditConnectionComponent } from '../../ui/canvas/items/connection/edit-connection/edit-connection.component';
@ -222,6 +223,8 @@ export class FlowEffects {
}),
catchError((error) => of(FlowActions.flowApiError({ error: error.error })))
);
case ComponentType.RemoteProcessGroup:
return of(FlowActions.openNewRemoteProcessGroupDialog({ request }));
case ComponentType.Funnel:
return of(FlowActions.createFunnel({ request }));
case ComponentType.Label:
@ -293,6 +296,47 @@ export class FlowEffects {
)
);
openNewRemoteProcessGroupDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.openNewRemoteProcessGroupDialog),
map((action) => action.request),
tap((request) => {
this.dialog
.open(CreateRemoteProcessGroup, {
data: request,
panelClass: 'large-dialog'
})
.afterClosed()
.subscribe(() => {
this.store.dispatch(FlowActions.setDragging({ dragging: false }));
});
})
),
{ dispatch: false }
);
createRemoteProcessGroup$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.createRemoteProcessGroup),
map((action) => action.request),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
switchMap(([request, processGroupId]) =>
from(this.flowService.createRemoteProcessGroup(processGroupId, request)).pipe(
map((response) =>
FlowActions.createComponentSuccess({
response: {
type: request.type,
payload: response
}
})
),
catchError((error) => of(FlowActions.flowApiError({ error: error.error })))
)
)
)
);
openNewProcessGroupDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -219,6 +219,18 @@ export interface UploadProcessGroupRequest extends CreateComponentRequest {
flowDefinition: File;
}
export interface CreateRemoteProcessGroupRequest extends CreateComponentRequest {
targetUris: string;
transportProtocol: string;
localNetworkInterface: string;
proxyHost: string;
proxyPort: string;
proxyUser: string;
proxyPassword: string;
communicationsTimeout: string;
yieldDuration: string;
}
export interface CreatePortRequest extends CreateComponentRequest {
name: string;
allowRemoteAccess: boolean;

View File

@ -0,0 +1,30 @@
/*
* 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) {
// Get the color config from the theme.
$color-config: mat.get-color-config($theme);
// Get the color palette from the color-config.
$accent-palette: map.get($color-config, 'accent');
// Get hues from palette
$accent-palette-A400: mat.get-color-from-palette($accent-palette, 'A400');
}

View File

@ -0,0 +1,115 @@
<!--
~ 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>Create Remote Process Group</h2>
<form class="create-remote-process-group-form" [formGroup]="createRemoteProcessGroupForm">
<error-banner></error-banner>
<mat-dialog-content>
<div class="tab-content py-4 flex gap-x-4">
<div class="w-full">
<mat-form-field>
<mat-label>URLs</mat-label>
<input matInput formControlName="urls" type="text" placeholder="https://remotehost:8443/nifi" />
</mat-form-field>
</div>
</div>
<div class="flex gap-x-4">
<div class="w-full">
<mat-form-field>
<mat-label>Transport Protocol</mat-label>
<mat-select formControlName="transportProtocol">
<mat-option
value="RAW"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getOptionTipData('RAW')"
[delayClose]="false">
RAW
</mat-option>
<mat-option
value="HTTP"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getOptionTipData('HTTP')"
[delayClose]="false">
HTTP
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>Local Network Interface</mat-label>
<input matInput formControlName="localNetworkInterface" type="text" />
</mat-form-field>
</div>
</div>
<div class="flex gap-x-4">
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy Server Hostname</mat-label>
<input matInput formControlName="httpProxyServerHostname" type="text" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy Server Port</mat-label>
<input matInput formControlName="httpProxyServerPort" type="text" />
</mat-form-field>
</div>
</div>
<div class="flex gap-x-4">
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy User</mat-label>
<input matInput formControlName="httpProxyUser" type="text" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy Password</mat-label>
<input matInput formControlName="httpProxyPassword" type="text" />
</mat-form-field>
</div>
</div>
<div class="flex gap-x-4">
<div class="w-full">
<mat-form-field>
<mat-label>Communications Timeout</mat-label>
<input matInput formControlName="communicationsTimeout" type="text" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>Yield Duration</mat-label>
<input matInput formControlName="yieldDuration" type="text" />
</mat-form-field>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<button color="primary" mat-stroked-button mat-dialog-close>Cancel</button>
<button
[disabled]="!createRemoteProcessGroupForm.dirty || createRemoteProcessGroupForm.invalid || saving.value"
type="button"
color="primary"
(click)="createRemoteProcessGroup()"
mat-raised-button>
<span *nifiSpinner="saving.value">Add</span>
</button>
</mat-dialog-actions>
</form>

View File

@ -0,0 +1,26 @@
/*
* 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;
.create-remote-process-group-form {
@include mat.button-density(-1);
.mat-mdc-form-field {
width: 100%;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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 { CreateRemoteProcessGroup } from './create-remote-process-group.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentType } from '../../../../../../../state/shared';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CreateComponentRequest } from '../../../../../state/flow';
describe('CreateRemoteProcessGroup', () => {
let component: CreateRemoteProcessGroup;
let fixture: ComponentFixture<CreateRemoteProcessGroup>;
const data: CreateComponentRequest = {
revision: {
clientId: 'a6482293-7fe8-43b4-8ab4-ee95b3b27721',
version: 0
},
type: ComponentType.ProcessGroup,
position: {
x: -4,
y: -698.5
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateRemoteProcessGroup, BrowserAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CreateRemoteProcessGroup);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,110 @@
/*
* 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 { Store } from '@ngrx/store';
import { CanvasState } from '../../../../../state';
import { createRemoteProcessGroup } from '../../../../../state/flow/flow.actions';
import { TextTipInput } from '../../../../../../../state/shared';
import { selectSaving } from '../../../../../state/flow/flow.selectors';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { TextTip } from '../../../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { MatIconModule } from '@angular/material/icon';
import { CreateComponentRequest } from '../../../../../state/flow';
@Component({
selector: 'create-process-group',
standalone: true,
imports: [
AsyncPipe,
ErrorBanner,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
NgIf,
NifiSpinnerDirective,
ReactiveFormsModule,
MatOptionModule,
MatSelectModule,
NgForOf,
NifiTooltipDirective,
MatIconModule
],
templateUrl: './create-remote-process-group.component.html',
styleUrls: ['./create-remote-process-group.component.scss']
})
export class CreateRemoteProcessGroup {
saving$ = this.store.select(selectSaving);
protected readonly TextTip = TextTip;
createRemoteProcessGroupForm: FormGroup;
constructor(
@Inject(MAT_DIALOG_DATA) private dialogRequest: CreateComponentRequest,
private formBuilder: FormBuilder,
private store: Store<CanvasState>
) {
this.createRemoteProcessGroupForm = this.formBuilder.group({
urls: new FormControl('', Validators.required),
transportProtocol: new FormControl('RAW', Validators.required),
localNetworkInterface: new FormControl(''),
httpProxyServerHostname: new FormControl(''),
httpProxyServerPort: new FormControl(''),
httpProxyUser: new FormControl(''),
httpProxyPassword: new FormControl(''),
communicationsTimeout: new FormControl('30 sec', Validators.required),
yieldDuration: new FormControl('10 sec', Validators.required)
});
}
getOptionTipData(tip: string): TextTipInput {
return {
text: tip
};
}
createRemoteProcessGroup(): void {
this.store.dispatch(
createRemoteProcessGroup({
request: {
...this.dialogRequest,
targetUris: this.createRemoteProcessGroupForm.get('urls')?.value,
transportProtocol: this.createRemoteProcessGroupForm.get('transportProtocol')?.value,
localNetworkInterface: this.createRemoteProcessGroupForm.get('localNetworkInterface')?.value,
proxyHost: this.createRemoteProcessGroupForm.get('httpProxyServerHostname')?.value,
proxyPort: this.createRemoteProcessGroupForm.get('httpProxyServerPort')?.value,
proxyUser: this.createRemoteProcessGroupForm.get('httpProxyUser')?.value,
proxyPassword: this.createRemoteProcessGroupForm.get('httpProxyPassword')?.value,
communicationsTimeout: this.createRemoteProcessGroupForm.get('communicationsTimeout')?.value,
yieldDuration: this.createRemoteProcessGroupForm.get('yieldDuration')?.value
}
})
);
}
}

View File

@ -38,6 +38,7 @@
@use 'app/pages/flow-designer/ui/canvas/header/search/search.component-theme' as search;
@use 'app/pages/flow-designer/ui/canvas/items/connection/prioritizers/prioritizers.component-theme' as prioritizers;
@use 'app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component-theme' as create-process-group;
@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/login/feature/login.component-theme' as login;
@ -384,7 +385,7 @@ $appFontPath: '~roboto-fontface/fonts';
.large-dialog {
max-height: 72%;
max-width: 55%;
min-height: 560px;
min-height: 520px;
min-width: 760px;
}
@ -490,6 +491,7 @@ $appFontPath: '~roboto-fontface/fonts';
@include search.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include prioritizers.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include create-process-group.nifi-theme($material-theme-light);
@include create-remote-process-group.nifi-theme($material-theme-light);
@include login.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include login-form.nifi-theme($material-theme-light);
@include provenance-event-table.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@ -539,6 +541,7 @@ $appFontPath: '~roboto-fontface/fonts';
@include search.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);
@include prioritizers.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);
@include create-process-group.nifi-theme($material-theme-dark);
@include create-remote-process-group.nifi-theme($material-theme-dark);
@include login.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);
@include login-form.nifi-theme($material-theme-dark);
@include provenance-event-table.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);