[NIFI-12622] - Parameter Providers - listing table (#8298)

* [NIFI-12622] - Parameter Providers - listing table
* Add a new Parameter Provider
* Delete parameter provider
* refactor to reduce duplicate code when creating new properties
* support editing parameter providers
* refactored inline service creation into PropertyTableHelperService
* added parameter provider linking to access policies.

* fix bugs in flow reducer not setting state properly when starting/stopping components and run once.

* address review feedback

* refactored nifiCommon to provide stripProtocol method that was implemented in loads of places. replaced all occurrences to the nifiCommon implementation.

This closes #8298
This commit is contained in:
Rob Fellows 2024-01-26 12:58:33 -05:00 committed by GitHub
parent bce14f573b
commit 0e87032ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 2598 additions and 1000 deletions

View File

@ -16,7 +16,12 @@
*/
package org.apache.nifi.web.api.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.apache.nifi.web.api.dto.util.TimeAdapter;
import java.util.Date;
import java.util.Set;
/**
@ -27,6 +32,8 @@ public class ParameterProvidersEntity extends Entity {
private Set<ParameterProviderEntity> parameterProviders;
private Date currentTime;
/**
* @return list of parameter providers that are being serialized
*/
@ -38,4 +45,18 @@ public class ParameterProvidersEntity extends Entity {
this.parameterProviders = parameterProviders;
}
/**
* @return current time on the server
*/
@XmlJavaTypeAdapter(TimeAdapter.class)
@Schema(description = "The current time on the system.",
type = "string"
)
public Date getCurrentTime() {
return currentTime;
}
public void setCurrentTime(Date currentTime) {
this.currentTime = currentTime;
}
}

View File

@ -23,6 +23,7 @@ import org.apache.nifi.web.api.entity.ParameterProviderEntity;
import org.apache.nifi.web.api.entity.ParameterProvidersEntity;
import java.net.URI;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -60,6 +61,7 @@ public class ParameterProvidersEndpointMerger extends AbstractSingleEntityEndpoi
}
clientEntity.setParameterProviders(new HashSet<>(providerEntities.values()));
clientEntity.setCurrentTime(new Date());
}
}

View File

@ -672,6 +672,7 @@ public class FlowResource extends ApplicationResource {
// create the response entity
final ParameterProvidersEntity entity = new ParameterProvidersEntity();
entity.setParameterProviders(parameterProviders);
entity.setCurrentTime(new Date());
// generate the response
return generateOkResponse(entity).build();

View File

@ -33,19 +33,6 @@ export class AccessPolicyService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
createAccessPolicy(resourceAction: ResourceAction): Observable<any> {
let resource: string = `/${resourceAction.resource}`;
if (resourceAction.resourceIdentifier) {
@ -92,12 +79,12 @@ export class AccessPolicyService {
}
};
return this.httpClient.put(this.stripProtocol(accessPolicy.uri), payload);
return this.httpClient.put(this.nifiCommon.stripProtocol(accessPolicy.uri), payload);
}
deleteAccessPolicy(accessPolicy: AccessPolicyEntity): Observable<any> {
const revision: any = this.client.getRevision(accessPolicy);
return this.httpClient.delete(this.stripProtocol(accessPolicy.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(accessPolicy.uri), { params: revision });
}
getUsers(): Observable<any> {

View File

@ -306,6 +306,7 @@ export class ComponentAccessPolicies implements OnInit, OnDestroy {
return 'icon-label';
case 'remote-process-groups':
return 'icon-group-remote';
case 'parameter-providers':
case 'parameter-contexts':
return 'icon-drop';
}
@ -329,6 +330,8 @@ export class ComponentAccessPolicies implements OnInit, OnDestroy {
return 'Remote Process Group';
case 'parameter-contexts':
return 'Parameter Contexts';
case 'parameter-providers':
return 'Parameter Provider';
}
return 'Process Group';

View File

@ -42,11 +42,13 @@ export class CounterTable implements AfterViewInit {
@Input() initialSortColumn: 'context' | 'name' | 'value' = 'context';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
activeSort: Sort = {
active: this.initialSortColumn,
direction: this.initialSortDirection
};
@Input() set counters(counterEntities: CounterEntity[]) {
this.dataSource.data = this.sortEntities(counterEntities, {
active: this.initialSortColumn,
direction: this.initialSortDirection
});
this.dataSource.data = this.sortEntities(counterEntities, this.activeSort);
this.dataSource.filterPredicate = (data: CounterEntity, filter: string) => {
const { filterTerm, filterColumn } = JSON.parse(filter);
@ -128,6 +130,7 @@ export class CounterTable implements AfterViewInit {
}
sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}

View File

@ -16,34 +16,22 @@
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { EMPTY, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ControllerServiceEntity } from '../../../state/shared';
import {
ConfigureControllerServiceRequest,
ControllerServiceCreator,
ControllerServiceEntity,
CreateControllerServiceRequest,
DeleteControllerServiceRequest
} from '../state/controller-services';
PropertyDescriptorRetriever
} from '../../../state/shared';
import { ConfigureControllerServiceRequest, DeleteControllerServiceRequest } from '../state/controller-services';
@Injectable({ providedIn: 'root' })
export class ControllerServiceService {
export class ControllerServiceService implements ControllerServiceCreator, PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
constructor(
private httpClient: HttpClient,
private client: Client,
@ -69,17 +57,20 @@ export class ControllerServiceService {
}
createControllerService(createControllerService: CreateControllerServiceRequest): Observable<any> {
const processGroupId: string = createControllerService.processGroupId;
return this.httpClient.post(
`${ControllerServiceService.API}/process-groups/${processGroupId}/controller-services`,
{
revision: createControllerService.revision,
component: {
bundle: createControllerService.controllerServiceBundle,
type: createControllerService.controllerServiceType
if (createControllerService.processGroupId) {
const processGroupId: string = createControllerService.processGroupId;
return this.httpClient.post(
`${ControllerServiceService.API}/process-groups/${processGroupId}/controller-services`,
{
revision: createControllerService.revision,
component: {
bundle: createControllerService.controllerServiceBundle,
type: createControllerService.controllerServiceType
}
}
}
);
);
}
return EMPTY;
}
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
@ -94,7 +85,7 @@ export class ControllerServiceService {
updateControllerService(configureControllerService: ConfigureControllerServiceRequest): Observable<any> {
return this.httpClient.put(
this.stripProtocol(configureControllerService.uri),
this.nifiCommon.stripProtocol(configureControllerService.uri),
configureControllerService.payload
);
}
@ -102,6 +93,6 @@ export class ControllerServiceService {
deleteControllerService(deleteControllerService: DeleteControllerServiceRequest): Observable<any> {
const entity: ControllerServiceEntity = deleteControllerService.controllerService;
const revision: any = this.client.getRevision(entity);
return this.httpClient.delete(this.stripProtocol(entity.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params: revision });
}
}

View File

@ -38,12 +38,12 @@ import {
UpdateComponentRequest,
UploadProcessGroupRequest
} from '../state/flow';
import { ComponentType } from '../../../state/shared';
import { ComponentType, PropertyDescriptorRetriever } from '../../../state/shared';
import { Client } from '../../../service/client.service';
import { NiFiCommon } from '../../../service/nifi-common.service';
@Injectable({ providedIn: 'root' })
export class FlowService {
export class FlowService implements PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
constructor(
@ -53,19 +53,6 @@ export class FlowService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getFlow(processGroupId = 'root'): Observable<any> {
// TODO - support uiOnly... this would mean that we need to load the entire resource prior to editing
return this.httpClient.get(`${FlowService.API}/flow/process-groups/${processGroupId}`);
@ -212,13 +199,13 @@ export class FlowService {
updateComponent(updateComponent: UpdateComponentRequest): Observable<any> {
// return throwError('API Error');
return this.httpClient.put(this.stripProtocol(updateComponent.uri), updateComponent.payload);
return this.httpClient.put(this.nifiCommon.stripProtocol(updateComponent.uri), updateComponent.payload);
}
deleteComponent(deleteComponent: DeleteComponentRequest): Observable<any> {
// return throwError('API Error');
const revision: any = this.client.getRevision(deleteComponent.entity);
return this.httpClient.delete(this.stripProtocol(deleteComponent.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(deleteComponent.uri), { params: revision });
}
createSnippet(snippet: Snippet): Observable<any> {
@ -250,7 +237,7 @@ export class FlowService {
disconnectedNodeAcknowledged: false,
state: 'RUN_ONCE'
};
return this.httpClient.put(`${this.stripProtocol(request.uri)}/run-status`, startRequest);
return this.httpClient.put(`${this.nifiCommon.stripProtocol(request.uri)}/run-status`, startRequest);
}
startComponent(request: StartComponentRequest): Observable<any> {
@ -259,7 +246,7 @@ export class FlowService {
disconnectedNodeAcknowledged: false,
state: request.type === ComponentType.RemoteProcessGroup ? 'TRANSMITTING' : 'RUNNING'
};
return this.httpClient.put(`${this.stripProtocol(request.uri)}/run-status`, startRequest);
return this.httpClient.put(`${this.nifiCommon.stripProtocol(request.uri)}/run-status`, startRequest);
}
stopComponent(request: StopComponentRequest): Observable<any> {
@ -268,7 +255,7 @@ export class FlowService {
disconnectedNodeAcknowledged: false,
state: 'STOPPED'
};
return this.httpClient.put(`${this.stripProtocol(request.uri)}/run-status`, stopRequest);
return this.httpClient.put(`${this.nifiCommon.stripProtocol(request.uri)}/run-status`, stopRequest);
}
startProcessGroup(request: StartProcessGroupRequest): Observable<any> {

View File

@ -30,19 +30,6 @@ export class ParameterService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getParameterContext(id: string, includeInheritedParameters: boolean): Observable<any> {
return this.httpClient.get(`${ParameterService.API}/parameter-contexts/${id}`, {
params: {
@ -59,10 +46,10 @@ export class ParameterService {
}
pollParameterContextUpdate(updateRequest: ParameterContextUpdateRequest): Observable<any> {
return this.httpClient.get(this.stripProtocol(updateRequest.uri));
return this.httpClient.get(this.nifiCommon.stripProtocol(updateRequest.uri));
}
deleteParameterContextUpdate(updateRequest: ParameterContextUpdateRequest): Observable<any> {
return this.httpClient.delete(this.stripProtocol(updateRequest.uri));
return this.httpClient.delete(this.nifiCommon.stripProtocol(updateRequest.uri));
}
}

View File

@ -30,19 +30,6 @@ export class QueueService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
submitEmptyQueueRequest(emptyQueueRequest: SubmitEmptyQueueRequest): Observable<any> {
return this.httpClient.post(
`${QueueService.API}/flowfile-queues/${emptyQueueRequest.connectionId}/drop-requests`,
@ -58,10 +45,10 @@ export class QueueService {
}
pollEmptyQueueRequest(dropRequest: DropRequest): Observable<any> {
return this.httpClient.get(this.stripProtocol(dropRequest.uri));
return this.httpClient.get(this.nifiCommon.stripProtocol(dropRequest.uri));
}
deleteEmptyQueueRequest(dropRequest: DropRequest): Observable<any> {
return this.httpClient.delete(this.stripProtocol(dropRequest.uri));
return this.httpClient.delete(this.nifiCommon.stripProtocol(dropRequest.uri));
}
}

View File

@ -19,7 +19,6 @@ import { createAction, props } from '@ngrx/store';
import {
ConfigureControllerServiceRequest,
ConfigureControllerServiceSuccess,
CreateControllerServiceRequest,
CreateControllerServiceSuccess,
DeleteControllerServiceRequest,
DeleteControllerServiceSuccess,
@ -28,6 +27,7 @@ import {
SelectControllerServiceRequest
} from './index';
import {
CreateControllerServiceRequest,
DisableControllerServiceDialogRequest,
EditControllerServiceDialogRequest,
SetEnableControllerServiceDialogRequest

View File

@ -18,20 +18,7 @@
import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import * as ControllerServicesActions from './controller-services.actions';
import {
catchError,
combineLatest,
filter,
from,
map,
NEVER,
Observable,
of,
switchMap,
take,
takeUntil,
tap
} from 'rxjs';
import { catchError, combineLatest, filter, from, map, NEVER, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
@ -45,17 +32,10 @@ import {
ControllerServiceReferencingComponent,
EditParameterRequest,
EditParameterResponse,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Parameter,
ParameterEntity,
Property,
PropertyDescriptor,
UpdateControllerServiceRequest
} from '../../../../state/shared';
import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
import { Router } from '@angular/router';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { selectCurrentProcessGroupId, selectSaving } from './controller-services.selectors';
@ -68,6 +48,7 @@ import * as ParameterActions from '../parameter/parameter.actions';
import { ParameterService } from '../../service/parameter.service';
import { EnableControllerService } from '../../../../ui/common/controller-service/enable-controller-service/enable-controller-service.component';
import { DisableControllerService } from '../../../../ui/common/controller-service/disable-controller-service/disable-controller-service.component';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@Injectable()
export class ControllerServicesEffects {
@ -80,7 +61,8 @@ export class ControllerServicesEffects {
private parameterService: ParameterService,
private extensionTypesService: ExtensionTypesService,
private dialog: MatDialog,
private router: Router
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
) {}
loadControllerServices$ = createEffect(() =>
@ -225,36 +207,8 @@ export class ControllerServicesEffects {
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.controllerServiceService
.getPropertyDescriptor(request.id, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(request.id, this.controllerServiceService);
const goTo = (commands: string[], destination: string): void => {
if (editDialogReference.componentInstance.editControllerServiceForm.dirty) {
@ -393,85 +347,21 @@ export class ControllerServicesEffects {
});
};
editDialogReference.componentInstance.createNewService = (
serviceRequest: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = serviceRequest.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.controllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
processGroupId,
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createResponse) => {
// dispatch an inline create service success action so the new service is in the state
this.store.dispatch(
ControllerServicesActions.inlineCreateControllerServiceSuccess(
{
response: {
controllerService: createResponse
}
}
)
);
// fetch an updated property descriptor
return this.controllerServiceService
.getPropertyDescriptor(serviceId, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createResponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
request.id,
this.controllerServiceService,
this.controllerServiceService,
processGroupId,
(createResponse) =>
this.store.dispatch(
ControllerServicesActions.inlineCreateControllerServiceSuccess({
response: {
controllerService: createResponse
}
})
)
);
editDialogReference.componentInstance.editControllerService
.pipe(takeUntil(editDialogReference.afterClosed()))

View File

@ -31,13 +31,6 @@ export interface LoadControllerServicesResponse {
loadedTimestamp: string;
}
export interface CreateControllerServiceRequest {
processGroupId: string;
controllerServiceType: string;
controllerServiceBundle: Bundle;
revision: Revision;
}
export interface CreateControllerServiceSuccess {
controllerService: ControllerServiceEntity;
}

View File

@ -69,14 +69,8 @@ import {
ComponentType,
EditParameterRequest,
EditParameterResponse,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Parameter,
ParameterEntity,
Property,
PropertyDescriptor
ParameterEntity
} from '../../../../state/shared';
import { Router } from '@angular/router';
import { Client } from '../../../../service/client.service';
@ -86,7 +80,6 @@ import { selectProcessorTypes } from '../../../../state/extension-types/extensio
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 { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
import { BirdseyeView } from '../../service/birdseye-view.service';
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';
@ -94,13 +87,13 @@ import { EditConnectionComponent } from '../../ui/canvas/items/connection/edit-c
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
import { GroupComponents } from '../../ui/canvas/items/process-group/group-components/group-components.component';
import { EditProcessGroup } from '../../ui/canvas/items/process-group/edit-process-group/edit-process-group.component';
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { ControllerServiceService } from '../../service/controller-service.service';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditParameterDialog } from '../../../../ui/common/edit-parameter-dialog/edit-parameter-dialog.component';
import { selectParameterSaving } from '../parameter/parameter.selectors';
import { ParameterService } from '../../service/parameter.service';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@Injectable()
export class FlowEffects {
@ -117,7 +110,8 @@ export class FlowEffects {
private birdseyeView: BirdseyeView,
private connectionManager: ConnectionManager,
private router: Router,
private dialog: MatDialog
private dialog: MatDialog,
private propertyTableHelperService: PropertyTableHelperService
) {}
reloadFlow$ = createEffect(() =>
@ -852,36 +846,8 @@ export class FlowEffects {
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.flowService
.getPropertyDescriptor(processorId, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(processorId, this.flowService);
const goTo = (commands: string[], destination: string): void => {
if (editDialogReference.componentInstance.editProcessorForm.dirty) {
@ -1013,74 +979,13 @@ export class FlowEffects {
});
};
editDialogReference.componentInstance.createNewService = (
serviceRequest: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = serviceRequest.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.controllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
processGroupId,
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createReponse) => {
// fetch an updated property descriptor
return this.flowService
.getPropertyDescriptor(processorId, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createReponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
processorId,
this.controllerServiceService,
this.flowService,
processGroupId
);
editDialogReference.componentInstance.editProcessor
.pipe(takeUntil(editDialogReference.afterClosed()))
@ -2331,7 +2236,7 @@ export class FlowEffects {
return from(this.flowService.getProcessGroup(request.id)).pipe(
map((response) =>
FlowActions.loadChildProcessGroupSuccess({
response: response.component
response
})
),
catchError((error) => of(FlowActions.flowApiError({ error: error.error })))

View File

@ -326,10 +326,7 @@ export const flowReducer = createReducer(
if (collection) {
const componentIndex: number = collection.findIndex((f: any) => response.component.id === f.id);
if (componentIndex > -1) {
collection[componentIndex] = {
...collection[componentIndex],
...response.component
};
collection[componentIndex] = response.component;
}
}
@ -343,10 +340,7 @@ export const flowReducer = createReducer(
if (collection) {
const componentIndex: number = collection.findIndex((f: any) => response.component.id === f.id);
if (componentIndex > -1) {
collection[componentIndex] = {
...collection[componentIndex],
...response.component
};
collection[componentIndex] = response.component;
}
}
@ -361,10 +355,7 @@ export const flowReducer = createReducer(
if (collection) {
const componentIndex: number = collection.findIndex((f: any) => response.id === f.id);
if (componentIndex > -1) {
collection[componentIndex] = {
...collection[componentIndex],
...response.component
};
collection[componentIndex] = response;
}
}

View File

@ -22,6 +22,8 @@ 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', () => {
let component: ControllerServices;
@ -37,7 +39,7 @@ describe('ControllerServices', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ControllerServices],
imports: [RouterTestingModule, MockNavigation],
imports: [RouterTestingModule, MockNavigation, ControllerServicesModule, HttpClientTestingModule],
providers: [
provideMockStore({
initialState

View File

@ -37,19 +37,6 @@ export class ParameterContextService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getParameterContexts(): Observable<any> {
return this.httpClient.get(`${ParameterContextService.API}/flow/parameter-contexts`);
}
@ -77,11 +64,11 @@ export class ParameterContextService {
}
pollParameterContextUpdate(updateRequest: ParameterContextUpdateRequest): Observable<any> {
return this.httpClient.get(this.stripProtocol(updateRequest.uri));
return this.httpClient.get(this.nifiCommon.stripProtocol(updateRequest.uri));
}
deleteParameterContextUpdate(updateRequest: ParameterContextUpdateRequest): Observable<any> {
return this.httpClient.delete(this.stripProtocol(updateRequest.uri));
return this.httpClient.delete(this.nifiCommon.stripProtocol(updateRequest.uri));
}
deleteParameterContext(deleteParameterContext: DeleteParameterContextRequest): Observable<any> {

View File

@ -28,7 +28,7 @@ import { RouterLink } from '@angular/router';
@NgModule({
declarations: [ParameterContextListing, ParameterContextTable],
exports: [ParameterContextListing],
exports: [ParameterContextListing, ParameterContextTable],
imports: [
CommonModule,
NgxSkeletonLoaderModule,

View File

@ -31,12 +31,13 @@ import { CurrentUser } from '../../../../../state/current-user';
export class ParameterContextTable {
@Input() initialSortColumn: 'name' | 'provider' | 'description' = 'name';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
activeSort: Sort = {
active: this.initialSortColumn,
direction: this.initialSortDirection
};
@Input() set parameterContexts(parameterContextEntities: ParameterContextEntity[]) {
this.dataSource.data = this.sortEntities(parameterContextEntities, {
active: this.initialSortColumn,
direction: this.initialSortDirection
});
this.dataSource.data = this.sortEntities(parameterContextEntities, this.activeSort);
}
@Input() selectedParameterContextId!: string;
@ -109,6 +110,7 @@ export class ParameterContextTable {
}
sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}

View File

@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ProvenanceRequest } from '../state/provenance-event-listing';
@ -31,19 +31,6 @@ export class ProvenanceService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getSearchOptions(): Observable<any> {
return this.httpClient.get(`${ProvenanceService.API}/provenance/search-options`);
}

View File

@ -30,25 +30,12 @@ export class QueueService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getConnection(connectionId: string): Observable<any> {
return this.httpClient.get(`${QueueService.API}/connections/${connectionId}`);
}
getFlowFile(flowfileSummary: FlowFileSummary): Observable<any> {
return this.httpClient.get(this.stripProtocol(flowfileSummary.uri));
return this.httpClient.get(this.nifiCommon.stripProtocol(flowfileSummary.uri));
}
submitQueueListingRequest(queueListingRequest: SubmitQueueListingRequest): Observable<any> {
@ -59,15 +46,15 @@ export class QueueService {
}
pollQueueListingRequest(listingRequest: ListingRequest): Observable<any> {
return this.httpClient.get(this.stripProtocol(listingRequest.uri));
return this.httpClient.get(this.nifiCommon.stripProtocol(listingRequest.uri));
}
deleteQueueListingRequest(listingRequest: ListingRequest): Observable<any> {
return this.httpClient.delete(this.stripProtocol(listingRequest.uri));
return this.httpClient.delete(this.nifiCommon.stripProtocol(listingRequest.uri));
}
downloadContent(flowfileSummary: FlowFileSummary): void {
let dataUri: string = `${this.stripProtocol(flowfileSummary.uri)}/content`;
let dataUri: string = `${this.nifiCommon.stripProtocol(flowfileSummary.uri)}/content`;
const queryParameters: any = {};
@ -83,7 +70,7 @@ export class QueueService {
viewContent(flowfileSummary: FlowFileSummary, contentViewerUrl: string): void {
// build the uri to the data
let dataUri: string = `${this.stripProtocol(flowfileSummary.uri)}/content`;
let dataUri: string = `${this.nifiCommon.stripProtocol(flowfileSummary.uri)}/content`;
const dataUriParameters: any = {};

View File

@ -99,7 +99,26 @@ const routes: Routes = [
}
]
},
{ path: 'parameter-providers', component: ParameterProviders }
{
path: 'parameter-providers',
component: ParameterProviders,
children: [
{
path: ':id',
component: ParameterProviders,
children: [
{
path: 'edit',
component: ParameterProviders
},
{
path: 'fetch',
component: ParameterProviders
}
]
}
]
}
]
}
];

View File

@ -35,6 +35,7 @@ import { ReportingTasksEffects } from '../state/reporting-tasks/reporting-tasks.
import { RegistryClientsEffects } from '../state/registry-clients/registry-clients.effects';
import { FlowAnalysisRulesEffects } from '../state/flow-analysis-rules/flow-analysis-rules.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { ParameterProvidersEffects } from '../state/parameter-providers/parameter-providers.effects';
@NgModule({
declarations: [Settings],
@ -54,7 +55,8 @@ import { Navigation } from '../../../ui/common/navigation/navigation.component';
ManagementControllerServicesEffects,
ReportingTasksEffects,
FlowAnalysisRulesEffects,
RegistryClientsEffects
RegistryClientsEffects,
ParameterProvidersEffects
),
MatTabsModule,
Navigation

View File

@ -27,24 +27,12 @@ import {
EnableFlowAnalysisRuleRequest,
FlowAnalysisRuleEntity
} from '../state/flow-analysis-rules';
import { PropertyDescriptorRetriever } from '../../../state/shared';
@Injectable({ providedIn: 'root' })
export class FlowAnalysisRuleService {
export class FlowAnalysisRuleService implements PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
constructor(
private httpClient: HttpClient,
private client: Client,
@ -68,7 +56,7 @@ export class FlowAnalysisRuleService {
deleteFlowAnalysisRule(deleteFlowAnalysisRule: DeleteFlowAnalysisRuleRequest): Observable<any> {
const entity: FlowAnalysisRuleEntity = deleteFlowAnalysisRule.flowAnalysisRule;
const revision: any = this.client.getRevision(entity);
return this.httpClient.delete(this.stripProtocol(entity.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params: revision });
}
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
@ -83,14 +71,14 @@ export class FlowAnalysisRuleService {
updateFlowAnalysisRule(configureFlowAnalysisRule: ConfigureFlowAnalysisRuleRequest): Observable<any> {
return this.httpClient.put(
this.stripProtocol(configureFlowAnalysisRule.uri),
this.nifiCommon.stripProtocol(configureFlowAnalysisRule.uri),
configureFlowAnalysisRule.payload
);
}
setEnable(flowAnalysisRule: EnableFlowAnalysisRuleRequest, enabled: boolean): Observable<any> {
const entity: FlowAnalysisRuleEntity = flowAnalysisRule.flowAnalysisRule;
return this.httpClient.put(`${this.stripProtocol(entity.uri)}/run-status`, {
return this.httpClient.put(`${this.nifiCommon.stripProtocol(entity.uri)}/run-status`, {
revision: this.client.getRevision(entity),
state: enabled ? 'ENABLED' : 'DISABLED',
uiOnly: true

View File

@ -16,34 +16,25 @@
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import {
ConfigureControllerServiceRequest,
CreateControllerServiceRequest,
DeleteControllerServiceRequest
} from '../state/management-controller-services';
import { Client } from '../../../service/client.service';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ControllerServiceEntity } from '../../../state/shared';
import {
ControllerServiceCreator,
ControllerServiceEntity,
CreateControllerServiceRequest,
PropertyDescriptorRetriever
} from '../../../state/shared';
@Injectable({ providedIn: 'root' })
export class ManagementControllerServiceService {
export class ManagementControllerServiceService implements ControllerServiceCreator, PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
constructor(
private httpClient: HttpClient,
private client: Client,
@ -79,7 +70,7 @@ export class ManagementControllerServiceService {
updateControllerService(configureControllerService: ConfigureControllerServiceRequest): Observable<any> {
return this.httpClient.put(
this.stripProtocol(configureControllerService.uri),
this.nifiCommon.stripProtocol(configureControllerService.uri),
configureControllerService.payload
);
}
@ -87,6 +78,6 @@ export class ManagementControllerServiceService {
deleteControllerService(deleteControllerService: DeleteControllerServiceRequest): Observable<any> {
const entity: ControllerServiceEntity = deleteControllerService.controllerService;
const revision: any = this.client.getRevision(entity);
return this.httpClient.delete(this.stripProtocol(entity.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params: revision });
}
}

View File

@ -0,0 +1,78 @@
/*
* 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 { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { Observable } from 'rxjs';
import {
ConfigureParameterProviderRequest,
CreateParameterProviderRequest,
DeleteParameterProviderRequest,
ParameterProviderEntity
} from '../state/parameter-providers';
import { PropertyDescriptorRetriever, Revision } from '../../../state/shared';
@Injectable({ providedIn: 'root' })
export class ParameterProviderService implements PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client,
private nifiCommon: NiFiCommon
) {}
getParameterProviders(): Observable<any> {
return this.httpClient.get(`${ParameterProviderService.API}/flow/parameter-providers`);
}
createParameterProvider(request: CreateParameterProviderRequest) {
return this.httpClient.post(`${ParameterProviderService.API}/controller/parameter-providers`, {
revision: request.revision,
component: {
bundle: request.parameterProviderBundle,
type: request.parameterProviderType
}
});
}
deleteParameterProvider(request: DeleteParameterProviderRequest) {
const entity: ParameterProviderEntity = request.parameterProvider;
const revision: any = this.client.getRevision(entity);
const params: any = {
...revision,
disconnectedNodeAcknowledged: false
};
return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params: revision });
}
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
const params: any = {
propertyName,
sensitive
};
return this.httpClient.get(`${ParameterProviderService.API}/parameter-providers/${id}/descriptors`, {
params
});
}
updateParameterProvider(configureRequest: ConfigureParameterProviderRequest): Observable<any> {
return this.httpClient.put(this.nifiCommon.stripProtocol(configureRequest.uri), configureRequest.payload);
}
}

View File

@ -26,24 +26,12 @@ import {
EditRegistryClientRequest,
RegistryClientEntity
} from '../state/registry-clients';
import { PropertyDescriptorRetriever } from '../../../state/shared';
@Injectable({ providedIn: 'root' })
export class RegistryClientService {
export class RegistryClientService implements PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
constructor(
private httpClient: HttpClient,
private client: Client,
@ -69,12 +57,12 @@ export class RegistryClientService {
}
updateRegistryClient(request: EditRegistryClientRequest): Observable<any> {
return this.httpClient.put(this.stripProtocol(request.uri), request.payload);
return this.httpClient.put(this.nifiCommon.stripProtocol(request.uri), request.payload);
}
deleteRegistryClient(deleteRegistryClient: DeleteRegistryClientRequest): Observable<any> {
const entity: RegistryClientEntity = deleteRegistryClient.registryClient;
const revision: any = this.client.getRevision(entity);
return this.httpClient.delete(this.stripProtocol(entity.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params: revision });
}
}

View File

@ -28,24 +28,12 @@ import {
StartReportingTaskRequest,
StopReportingTaskRequest
} from '../state/reporting-tasks';
import { PropertyDescriptorRetriever } from '../../../state/shared';
@Injectable({ providedIn: 'root' })
export class ReportingTaskService {
export class ReportingTaskService implements PropertyDescriptorRetriever {
private static readonly API: string = '../nifi-api';
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
constructor(
private httpClient: HttpClient,
private client: Client,
@ -69,7 +57,7 @@ export class ReportingTaskService {
deleteReportingTask(deleteReportingTask: DeleteReportingTaskRequest): Observable<any> {
const entity: ReportingTaskEntity = deleteReportingTask.reportingTask;
const revision: any = this.client.getRevision(entity);
return this.httpClient.delete(this.stripProtocol(entity.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(entity.uri), { params: revision });
}
startReportingTask(startReportingTask: StartReportingTaskRequest): Observable<any> {
@ -79,7 +67,7 @@ export class ReportingTaskService {
revision,
state: 'RUNNING'
};
return this.httpClient.put(`${this.stripProtocol(entity.uri)}/run-status`, payload);
return this.httpClient.put(`${this.nifiCommon.stripProtocol(entity.uri)}/run-status`, payload);
}
stopReportingTask(stopReportingTask: StopReportingTaskRequest): Observable<any> {
@ -89,7 +77,7 @@ export class ReportingTaskService {
revision,
state: 'STOPPED'
};
return this.httpClient.put(`${this.stripProtocol(entity.uri)}/run-status`, payload);
return this.httpClient.put(`${this.nifiCommon.stripProtocol(entity.uri)}/run-status`, payload);
}
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
@ -103,6 +91,9 @@ export class ReportingTaskService {
}
updateReportingTask(configureReportingTask: ConfigureReportingTaskRequest): Observable<any> {
return this.httpClient.put(this.stripProtocol(configureReportingTask.uri), configureReportingTask.payload);
return this.httpClient.put(
this.nifiCommon.stripProtocol(configureReportingTask.uri),
configureReportingTask.payload
);
}
}

View File

@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import * as FlowAnalysisRuleActions from './flow-analysis-rules.actions';
import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { catchError, from, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
@ -30,20 +30,11 @@ import { ManagementControllerServiceService } from '../../service/management-con
import { CreateFlowAnalysisRule } from '../../ui/flow-analysis-rules/create-flow-analysis-rule/create-flow-analysis-rule.component';
import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import {
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Property,
PropertyDescriptor,
UpdateControllerServiceRequest
} from '../../../../state/shared';
import { UpdateControllerServiceRequest } from '../../../../state/shared';
import { EditFlowAnalysisRule } from '../../ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component';
import { CreateFlowAnalysisRuleSuccess } from './index';
import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@Injectable()
export class FlowAnalysisRulesEffects {
@ -55,7 +46,8 @@ export class FlowAnalysisRulesEffects {
private extensionTypesService: ExtensionTypesService,
private flowAnalysisRuleService: FlowAnalysisRuleService,
private dialog: MatDialog,
private router: Router
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
) {}
loadFlowAnalysisRule$ = createEffect(() =>
@ -225,36 +217,8 @@ export class FlowAnalysisRulesEffects {
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.flowAnalysisRuleService
.getPropertyDescriptor(request.id, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(request.id, this.flowAnalysisRuleService);
const goTo = (commands: string[], destination: string): void => {
if (editDialogReference.componentInstance.editFlowAnalysisRuleForm.dirty) {
@ -285,73 +249,12 @@ export class FlowAnalysisRulesEffects {
goTo(commands, 'Controller Service');
};
editDialogReference.componentInstance.createNewService = (
request: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = request.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.managementControllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createResponse) => {
// fetch an updated property descriptor
return this.flowAnalysisRuleService
.getPropertyDescriptor(ruleId, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createResponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
request.id,
this.managementControllerServiceService,
this.flowAnalysisRuleService
);
editDialogReference.componentInstance.editFlowAnalysisRule
.pipe(takeUntil(editDialogReference.afterClosed()))

View File

@ -29,6 +29,8 @@ import { registryClientsFeatureKey, RegistryClientsState } from './registry-clie
import { registryClientsReducer } from './registry-clients/registry-clients.reducer';
import { flowAnalysisRulesFeatureKey, FlowAnalysisRulesState } from './flow-analysis-rules';
import { flowAnalysisRulesReducer } from './flow-analysis-rules/flow-analysis-rules.reducer';
import { parameterProvidersFeatureKey, ParameterProvidersState } from './parameter-providers';
import { parameterProvidersReducer } from './parameter-providers/parameter-providers.reducer';
export const settingsFeatureKey = 'settings';
@ -38,6 +40,7 @@ export interface SettingsState {
[reportingTasksFeatureKey]: ReportingTasksState;
[flowAnalysisRulesFeatureKey]: FlowAnalysisRulesState;
[registryClientsFeatureKey]: RegistryClientsState;
[parameterProvidersFeatureKey]: ParameterProvidersState;
}
export function reducers(state: SettingsState | undefined, action: Action) {
@ -46,7 +49,8 @@ export function reducers(state: SettingsState | undefined, action: Action) {
[managementControllerServicesFeatureKey]: managementControllerServicesReducer,
[reportingTasksFeatureKey]: reportingTasksReducer,
[flowAnalysisRulesFeatureKey]: flowAnalysisRulesReducer,
[registryClientsFeatureKey]: registryClientsReducer
[registryClientsFeatureKey]: registryClientsReducer,
[parameterProvidersFeatureKey]: parameterProvidersReducer
})(state, action);
}

View File

@ -24,12 +24,6 @@ export interface LoadManagementControllerServicesResponse {
loadedTimestamp: string;
}
export interface CreateControllerServiceRequest {
controllerServiceType: string;
controllerServiceBundle: Bundle;
revision: Revision;
}
export interface CreateControllerServiceSuccess {
controllerService: ControllerServiceEntity;
}

View File

@ -19,7 +19,6 @@ import { createAction, props } from '@ngrx/store';
import {
ConfigureControllerServiceRequest,
ConfigureControllerServiceSuccess,
CreateControllerServiceRequest,
CreateControllerServiceSuccess,
DeleteControllerServiceRequest,
DeleteControllerServiceSuccess,
@ -27,6 +26,7 @@ import {
SelectControllerServiceRequest
} from './index';
import {
CreateControllerServiceRequest,
DisableControllerServiceDialogRequest,
EditControllerServiceDialogRequest,
SetEnableControllerServiceDialogRequest

View File

@ -33,18 +33,15 @@ import {
ControllerServiceReferencingComponent,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Property,
PropertyDescriptor,
UpdateControllerServiceRequest
} from '../../../../state/shared';
import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
import { Router } from '@angular/router';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { selectSaving } from './management-controller-services.selectors';
import { EnableControllerService } from '../../../../ui/common/controller-service/enable-controller-service/enable-controller-service.component';
import { DisableControllerService } from '../../../../ui/common/controller-service/disable-controller-service/disable-controller-service.component';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@Injectable()
export class ManagementControllerServicesEffects {
@ -55,7 +52,8 @@ export class ManagementControllerServicesEffects {
private managementControllerServiceService: ManagementControllerServiceService,
private extensionTypesService: ExtensionTypesService,
private dialog: MatDialog,
private router: Router
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
) {}
loadManagementControllerServices$ = createEffect(() =>
@ -193,36 +191,11 @@ export class ManagementControllerServicesEffects {
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.managementControllerServiceService
.getPropertyDescriptor(request.id, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(
request.id,
this.managementControllerServiceService
);
};
const goTo = (commands: string[], destination: string): void => {
if (editDialogReference.componentInstance.editControllerServiceForm.dirty) {
@ -260,84 +233,21 @@ export class ManagementControllerServicesEffects {
goTo(route, component.referenceType);
};
editDialogReference.componentInstance.createNewService = (
request: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = request.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.managementControllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createResponse) => {
// dispatch an inline create service success action so the new service is in the state
this.store.dispatch(
ManagementControllerServicesActions.inlineCreateControllerServiceSuccess(
{
response: {
controllerService: createResponse
}
}
)
);
// fetch an updated property descriptor
return this.managementControllerServiceService
.getPropertyDescriptor(serviceId, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createResponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
request.id,
this.managementControllerServiceService,
this.managementControllerServiceService,
null,
(createResponse) =>
this.store.dispatch(
ManagementControllerServicesActions.inlineCreateControllerServiceSuccess({
response: {
controllerService: createResponse
}
})
)
);
editDialogReference.componentInstance.editControllerService
.pipe(takeUntil(editDialogReference.afterClosed()))

View File

@ -0,0 +1,117 @@
/*
* 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 {
Bundle,
DocumentedType,
ParameterContextReferenceEntity,
Permissions,
PropertyDescriptor,
Revision
} from '../../../../state/shared';
export const parameterProvidersFeatureKey = 'parameterProviders';
export interface ParameterProvider {
bundle: Bundle;
comments: string;
deprecated: boolean;
descriptors: { [key: string]: PropertyDescriptor };
extensionMissing: boolean;
id: string;
multipleVersionsAvailable: boolean;
name: string;
parameterGroupConfigurations: any[];
persistsState: boolean;
properties: { [key: string]: string };
referencingParameterContexts: ParameterContextReferenceEntity[];
restricted: boolean;
type: string;
validationStatus: string;
validationErrors?: string[];
}
export interface ParameterProviderEntity {
id: string;
bulletins: [];
component: ParameterProvider;
permissions: Permissions;
revision: Revision;
uri: string;
}
export interface ParameterProvidersState {
parameterProviders: ParameterProviderEntity[];
saving: boolean;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}
export interface LoadParameterProvidersResponse {
parameterProviders: ParameterProviderEntity[];
loadedTimestamp: string;
}
export interface SelectParameterProviderRequest {
id: string;
}
export interface CreateParameterProviderDialogRequest {
parameterProviderTypes: DocumentedType[];
}
export interface CreateParameterProviderRequest {
parameterProviderType: string;
parameterProviderBundle: Bundle;
revision: Revision;
}
export interface CreateParameterProviderSuccessResponse {
parameterProvider: ParameterProviderEntity;
}
export interface DeleteParameterProviderRequest {
parameterProvider: ParameterProviderEntity;
}
export interface DeleteParameterProviderSuccess {
parameterProvider: ParameterProviderEntity;
}
export interface EditParameterProviderRequest {
id: string;
parameterProvider: ParameterProviderEntity;
}
export interface ConfigureParameterProviderRequest {
id: string;
uri: string;
payload: any;
postUpdateNavigation?: string[];
}
export interface ConfigureParameterProviderSuccess {
id: string;
parameterProvider: ParameterProviderEntity;
postUpdateNavigation?: string[];
}
export interface UpdateParameterProviderRequest {
payload: any;
postUpdateNavigation?: string[];
}

View File

@ -0,0 +1,99 @@
/*
* 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 {
ConfigureParameterProviderRequest,
ConfigureParameterProviderSuccess,
CreateParameterProviderRequest,
CreateParameterProviderSuccessResponse,
DeleteParameterProviderRequest,
DeleteParameterProviderSuccess,
EditParameterProviderRequest,
LoadParameterProvidersResponse,
SelectParameterProviderRequest
} from './index';
const PARAMETER_PROVIDERS_PREFIX: string = '[Parameter Providers]';
export const resetParameterProvidersState = createAction(`${PARAMETER_PROVIDERS_PREFIX} Reset Parameter Providers`);
export const loadParameterProviders = createAction(`${PARAMETER_PROVIDERS_PREFIX} Load Parameter Providers`);
export const loadParameterProvidersSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Load Parameter Providers Success`,
props<{ response: LoadParameterProvidersResponse }>()
);
export const parameterProvidersApiError = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Load Parameter Providers Error`,
props<{ error: string }>()
);
export const selectParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Select Parameter Provider`,
props<{ request: SelectParameterProviderRequest }>()
);
export const openNewParameterProviderDialog = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Open New Parameter Provider Dialog`
);
export const createParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Create Parameter Provider`,
props<{ request: CreateParameterProviderRequest }>()
);
export const createParameterProviderSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Create Parameter Provider Success`,
props<{ response: CreateParameterProviderSuccessResponse }>()
);
export const promptParameterProviderDeletion = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Prompt Parameter Provider Deletion`,
props<{ request: DeleteParameterProviderRequest }>()
);
export const deleteParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Delete Parameter Provider`,
props<{ request: DeleteParameterProviderRequest }>()
);
export const deleteParameterProviderSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Delete Parameter Provider Success`,
props<{ response: DeleteParameterProviderSuccess }>()
);
export const navigateToEditParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Navigate To Edit Parameter Provider`,
props<{ id: string }>()
);
export const openConfigureParameterProviderDialog = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Open Configure Parameter Provider Dialog`,
props<{ request: EditParameterProviderRequest }>()
);
export const configureParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Configure Parameter Provider`,
props<{ request: ConfigureParameterProviderRequest }>()
);
export const configureParameterProviderSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Configure Parameter Provider Success`,
props<{ response: ConfigureParameterProviderSuccess }>()
);

View File

@ -0,0 +1,359 @@
/*
* 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 { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { Client } from '../../../../service/client.service';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { ParameterProviderService } from '../../service/parameter-provider.service';
import * as ParameterProviderActions from './parameter-providers.actions';
import { loadParameterProviders, selectParameterProvider } from './parameter-providers.actions';
import { catchError, from, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { selectSaving } from './parameter-providers.selectors';
import { selectParameterProviderTypes } from '../../../../state/extension-types/extension-types.selectors';
import { CreateParameterProvider } from '../../ui/parameter-providers/create-parameter-provider/create-parameter-provider.component';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditParameterProvider } from '../../ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import { UpdateParameterProviderRequest } from './index';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
@Injectable()
export class ParameterProvidersEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private client: Client,
private dialog: MatDialog,
private router: Router,
private parameterProviderService: ParameterProviderService,
private propertyTableHelperService: PropertyTableHelperService,
private managementControllerServiceService: ManagementControllerServiceService
) {}
loadParameterProviders$ = createEffect(() =>
this.actions$.pipe(
ofType(loadParameterProviders),
switchMap(() =>
from(this.parameterProviderService.getParameterProviders()).pipe(
map((response) =>
ParameterProviderActions.loadParameterProvidersSuccess({
response: {
parameterProviders: response.parameterProviders,
loadedTimestamp: response.currentTime
}
})
),
catchError((error) =>
of(ParameterProviderActions.parameterProvidersApiError({ error: error.error }))
)
)
)
)
);
selectParameterProvider$ = createEffect(
() =>
this.actions$.pipe(
ofType(selectParameterProvider),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/settings', 'parameter-providers', request.id]);
})
),
{ dispatch: false }
);
openNewParameterProviderDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.openNewParameterProviderDialog),
concatLatestFrom(() => this.store.select(selectParameterProviderTypes)),
tap(([action, parameterProviderTypes]) => {
const dialogReference = this.dialog.open(CreateParameterProvider, {
data: {
parameterProviderTypes
},
panelClass: 'medium-dialog'
});
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
dialogReference.componentInstance.createParameterProvider
.pipe(take(1))
.subscribe((parameterProviderType) => {
this.store.dispatch(
ParameterProviderActions.createParameterProvider({
request: {
parameterProviderType: parameterProviderType.type,
parameterProviderBundle: parameterProviderType.bundle,
revision: {
clientId: this.client.getClientId(),
version: 0
}
}
})
);
});
})
),
{ dispatch: false }
);
createParameterProvider$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.createParameterProvider),
map((action) => action.request),
switchMap((request) =>
from(this.parameterProviderService.createParameterProvider(request)).pipe(
map((response: any) =>
ParameterProviderActions.createParameterProviderSuccess({
response: {
parameterProvider: response
}
})
),
catchError((error) =>
of(ParameterProviderActions.parameterProvidersApiError({ error: error.error }))
)
)
)
)
);
createParameterProviderSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.createParameterProviderSuccess),
map((action) => action.response),
tap(() => {
this.dialog.closeAll();
}),
switchMap((response) =>
of(
ParameterProviderActions.selectParameterProvider({
request: {
id: response.parameterProvider.id
}
})
)
)
)
);
promptParameterProviderDeletion$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.promptParameterProviderDeletion),
map((action) => action.request),
tap((request) => {
const dialogReference = this.dialog.open(YesNoDialog, {
data: {
title: 'Delete Parameter Provider',
message: `Delete parameter provider ${request.parameterProvider.component.name}?`
},
panelClass: 'small-dialog'
});
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() =>
this.store.dispatch(
ParameterProviderActions.deleteParameterProvider({
request
})
)
);
})
),
{ dispatch: false }
);
deleteParameterProvider = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.deleteParameterProvider),
map((action) => action.request),
switchMap((request) =>
from(this.parameterProviderService.deleteParameterProvider(request)).pipe(
map((response: any) =>
ParameterProviderActions.deleteParameterProviderSuccess({
response: {
parameterProvider: response
}
})
),
catchError((error) =>
of(
ParameterProviderActions.parameterProvidersApiError({
error: error.error
})
)
)
)
)
)
);
navigateToEditParameterProvider$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.navigateToEditParameterProvider),
map((action) => action.id),
tap((id) => {
this.router.navigate(['settings', 'parameter-providers', id, 'edit']);
})
),
{ dispatch: false }
);
openConfigureParameterProviderDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.openConfigureParameterProviderDialog),
map((action) => action.request),
tap((request) => {
const id = request.id;
const editDialogReference = this.dialog.open(EditParameterProvider, {
data: {
parameterProvider: request.parameterProvider
},
id,
panelClass: 'large-dialog'
});
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
const goTo = (commands: string[], destination: string) => {
// confirm navigating away while changes are unsaved
if (editDialogReference.componentInstance.editParameterProviderForm.dirty) {
const promptSaveDialogRef = this.dialog.open(YesNoDialog, {
data: {
title: 'Parameter Provider Configuration',
message: `Save changes before going to this ${destination}`
},
panelClass: 'small-dialog'
});
promptSaveDialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
});
promptSaveDialogRef.componentInstance.no.pipe(take(1)).subscribe(() => {
editDialogReference.close('ROUTED');
this.router.navigate(commands);
});
} else {
editDialogReference.close('ROUTED');
this.router.navigate(commands);
}
};
editDialogReference.componentInstance.goToReferencingParameterContext = (id: string) => {
const commands: string[] = ['parameter-contexts', id];
goTo(commands, 'Parameter Context');
};
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
goTo(commands, 'Controller Service');
};
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(request.id, this.parameterProviderService);
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
request.id,
this.managementControllerServiceService,
this.parameterProviderService
);
editDialogReference.componentInstance.editParameterProvider
.pipe(takeUntil(editDialogReference.afterClosed()))
.subscribe((updateRequest: UpdateParameterProviderRequest) => {
this.store.dispatch(
ParameterProviderActions.configureParameterProvider({
request: {
id: request.parameterProvider.id,
uri: request.parameterProvider.uri,
payload: updateRequest.payload,
postUpdateNavigation: updateRequest.postUpdateNavigation
}
})
);
});
editDialogReference.afterClosed().subscribe((response) => {
if (response !== 'ROUTED') {
this.store.dispatch(
ParameterProviderActions.selectParameterProvider({
request: {
id
}
})
);
}
});
})
),
{ dispatch: false }
);
configureParameterProvider$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.configureParameterProvider),
map((action) => action.request),
switchMap((request) =>
from(this.parameterProviderService.updateParameterProvider(request)).pipe(
map((response) =>
ParameterProviderActions.configureParameterProviderSuccess({
response: {
id: request.id,
parameterProvider: response,
postUpdateNavigation: request.postUpdateNavigation
}
})
),
catchError((error) =>
of(
ParameterProviderActions.parameterProvidersApiError({
error: error.error
})
)
)
)
)
)
);
configureParameterProviderSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.configureParameterProviderSuccess),
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
this.dialog.getDialogById(response.id)?.close('ROUTED');
} else {
this.dialog.closeAll();
}
})
),
{ dispatch: false }
);
}

View File

@ -0,0 +1,106 @@
/*
* 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 { ParameterProvidersState } from './index';
import { createReducer, on } from '@ngrx/store';
import {
configureParameterProvider,
configureParameterProviderSuccess,
createParameterProvider,
createParameterProviderSuccess,
deleteParameterProvider,
deleteParameterProviderSuccess,
loadParameterProviders,
loadParameterProvidersSuccess,
parameterProvidersApiError,
resetParameterProvidersState
} from './parameter-providers.actions';
import { produce } from 'immer';
export const initialParameterProvidersState: ParameterProvidersState = {
parameterProviders: [],
saving: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
export const parameterProvidersReducer = createReducer(
initialParameterProvidersState,
on(resetParameterProvidersState, (state: ParameterProvidersState) => ({
...initialParameterProvidersState
})),
on(loadParameterProviders, (state: ParameterProvidersState) => ({
...state,
status: 'loading' as const
})),
on(loadParameterProvidersSuccess, (state: ParameterProvidersState, { response }) => ({
...state,
parameterProviders: response.parameterProviders,
loadedTimestamp: response.loadedTimestamp,
error: null,
status: 'success' as const
})),
on(parameterProvidersApiError, (state: ParameterProvidersState, { error }) => ({
...state,
saving: false,
error,
status: 'error' as const
})),
on(createParameterProvider, configureParameterProvider, deleteParameterProvider, (state, { request }) => ({
...state,
saving: true
})),
on(createParameterProviderSuccess, (state, { response }) => {
return produce(state, (draftState) => {
draftState.parameterProviders.push(response.parameterProvider);
draftState.saving = false;
});
}),
on(deleteParameterProviderSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const idx = draftState.parameterProviders.findIndex(
(provider) => provider.id === response.parameterProvider.id
);
if (idx > -1) {
draftState.parameterProviders.splice(idx, 1);
}
draftState.saving = false;
});
}),
on(configureParameterProviderSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const idx = draftState.parameterProviders.findIndex(
(provider) => provider.id === response.parameterProvider.id
);
if (idx > -1) {
draftState.parameterProviders[idx] = response.parameterProvider;
}
draftState.saving = false;
});
})
);

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 { createSelector } from '@ngrx/store';
import { selectSettingsState, SettingsState } from '../index';
import { ParameterProviderEntity, parameterProvidersFeatureKey, ParameterProvidersState } from './index';
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
export const selectParameterProvidersState = createSelector(
selectSettingsState,
(state: SettingsState) => state[parameterProvidersFeatureKey]
);
export const selectSaving = createSelector(
selectParameterProvidersState,
(state: ParameterProvidersState) => state.saving
);
export const selectParameterProviderIdFromRoute = createSelector(selectCurrentRoute, (route) => {
if (route) {
// always select the parameter provider from the route
return route.params.id;
}
return null;
});
export const selectSingleEditedParameterProvider = createSelector(selectCurrentRoute, (route) => {
if (route?.routeConfig?.path == 'edit') {
return route.params.id;
}
return null;
});
export const selectParameterProviders = createSelector(
selectParameterProvidersState,
(state: ParameterProvidersState) => state.parameterProviders
);
export const selectParameterProvider = (id: string) =>
createSelector(selectParameterProviders, (entities: ParameterProviderEntity[]) =>
entities.find((entity) => id == entity.id)
);

View File

@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import * as RegistryClientsActions from './registry-clients.actions';
import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { catchError, from, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
@ -29,20 +29,11 @@ import { RegistryClientService } from '../../service/registry-client.service';
import { CreateRegistryClient } from '../../ui/registry-clients/create-registry-client/create-registry-client.component';
import { selectSaving } from './registry-clients.selectors';
import { EditRegistryClient } from '../../ui/registry-clients/edit-registry-client/edit-registry-client.component';
import {
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Property,
PropertyDescriptor
} from '../../../../state/shared';
import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import { Client } from '../../../../service/client.service';
import { EditRegistryClientRequest } from './index';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@Injectable()
export class RegistryClientsEffects {
@ -54,7 +45,8 @@ export class RegistryClientsEffects {
private extensionTypesService: ExtensionTypesService,
private managementControllerServiceService: ManagementControllerServiceService,
private dialog: MatDialog,
private router: Router
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
) {}
loadRegistryClients$ = createEffect(() =>
@ -181,40 +173,8 @@ export class RegistryClientsEffects {
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.registryClientService
.getPropertyDescriptor(
registryClientId,
dialogResponse.name,
dialogResponse.sensitive
)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(registryClientId, this.registryClientService);
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
@ -242,77 +202,12 @@ export class RegistryClientsEffects {
}
};
editDialogReference.componentInstance.createNewService = (
request: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = request.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.managementControllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createReponse) => {
// fetch an updated property descriptor
return this.registryClientService
.getPropertyDescriptor(
registryClientId,
descriptor.name,
false
)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createReponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
registryClientId,
this.managementControllerServiceService,
this.registryClientService
);
editDialogReference.componentInstance.editRegistryClient
.pipe(takeUntil(editDialogReference.afterClosed()))

View File

@ -44,6 +44,7 @@ import { ExtensionTypesService } from '../../../../service/extension-types.servi
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import { Client } from '../../../../service/client.service';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@Injectable()
export class ReportingTasksEffects {
@ -55,7 +56,8 @@ export class ReportingTasksEffects {
private managementControllerServiceService: ManagementControllerServiceService,
private extensionTypesService: ExtensionTypesService,
private dialog: MatDialog,
private router: Router
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
) {}
loadReportingTasks$ = createEffect(() =>
@ -225,36 +227,8 @@ export class ReportingTasksEffects {
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.reportingTaskService
.getPropertyDescriptor(request.id, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
editDialogReference.componentInstance.createNewProperty =
this.propertyTableHelperService.createNewProperty(request.id, this.reportingTaskService);
const goTo = (commands: string[], destination: string): void => {
if (editDialogReference.componentInstance.editReportingTaskForm.dirty) {
@ -285,73 +259,12 @@ export class ReportingTasksEffects {
goTo(commands, 'Controller Service');
};
editDialogReference.componentInstance.createNewService = (
request: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = request.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.managementControllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createResponse) => {
// fetch an updated property descriptor
return this.reportingTaskService
.getPropertyDescriptor(taskId, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createResponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.createNewService =
this.propertyTableHelperService.createNewService(
request.id,
this.managementControllerServiceService,
this.reportingTaskService
);
editDialogReference.componentInstance.editReportingTask
.pipe(takeUntil(editDialogReference.afterClosed()))

View File

@ -0,0 +1,22 @@
<!--
~ 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.
-->
<extension-creation
[componentType]="'Parameter Provider'"
[documentedTypes]="parameterProviderTypes"
[saving]="(saving$ | async)!"
(extensionTypeSelected)="parameterProviderTypeSelected($event)"></extension-creation>

View File

@ -0,0 +1,16 @@
/*!
* 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.
*/

View File

@ -0,0 +1,81 @@
/*
* 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 { CreateParameterProvider } from './create-parameter-provider.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialParameterProvidersState } from '../../../state/parameter-providers/parameter-providers.reducer';
import {
CreateParameterProviderDialogRequest,
CreateParameterProviderRequest
} from '../../../state/parameter-providers';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CreateParameterProvider', () => {
let component: CreateParameterProvider;
let fixture: ComponentFixture<CreateParameterProvider>;
const data: CreateParameterProviderDialogRequest = {
parameterProviderTypes: [
{
type: 'org.apache.nifi.parameter.aws.AwsSecretsManagerParameterProvider',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-aws-nar',
version: '2.0.0-SNAPSHOT'
},
description:
'Fetches parameters from AWS SecretsManager. Each secret becomes a Parameter group, which can map to a Parameter Context, with key/value pairs in the secret mapping to Parameters in the group.',
restricted: false,
tags: ['secretsmanager', 'manager', 'aws', 'secrets']
},
{
type: 'org.apache.nifi.parameter.azure.AzureKeyVaultSecretsParameterProvider',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-azure-nar',
version: '2.0.0-SNAPSHOT'
},
description:
"Fetches parameters from Azure Key Vault Secrets. Each secret becomes a Parameter, which map to a Parameter Group byadding a secret tag named 'group-name'.",
restricted: false,
tags: ['keyvault', 'secrets', 'key', 'vault', 'azure']
}
]
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CreateParameterProvider, NoopAnimationsModule],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: data
},
provideMockStore({ initialState: initialParameterProvidersState })
]
});
fixture = TestBed.createComponent(CreateParameterProvider);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,46 @@
/*
* 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, EventEmitter, Inject, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable } from 'rxjs';
import { DocumentedType } from '../../../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CreateParameterProviderDialogRequest } from '../../../state/parameter-providers';
import { ExtensionCreation } from '../../../../../ui/common/extension-creation/extension-creation.component';
@Component({
selector: 'create-parameter-provider',
standalone: true,
imports: [CommonModule, ExtensionCreation],
templateUrl: './create-parameter-provider.component.html',
styleUrls: ['./create-parameter-provider.component.scss']
})
export class CreateParameterProvider {
@Input() saving$!: Observable<boolean>;
@Output() createParameterProvider: EventEmitter<DocumentedType> = new EventEmitter<DocumentedType>();
parameterProviderTypes: DocumentedType[];
constructor(@Inject(MAT_DIALOG_DATA) private dialogRequest: CreateParameterProviderDialogRequest) {
this.parameterProviderTypes = dialogRequest.parameterProviderTypes;
}
parameterProviderTypeSelected(parameterProviderType: DocumentedType) {
this.createParameterProvider.next(parameterProviderType);
}
}

View File

@ -0,0 +1,91 @@
<!--
~ 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 Parameter Provider</h2>
<form class="parameter-provider-edit-form" [formGroup]="editParameterProviderForm">
<mat-dialog-content>
<mat-tab-group>
<mat-tab label="Settings">
<div class="tab-content py-4 flex gap-x-4">
<div class="w-full">
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
</mat-form-field>
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="value">{{ request.parameterProvider.id }}</div>
</div>
<div class="flex flex-col mb-5">
<div>Type</div>
<div class="value">{{ formatType(request.parameterProvider) }}</div>
</div>
<div class="flex flex-col mb-5">
<div>Bundle</div>
<div class="value">{{ formatBundle(request.parameterProvider) }}</div>
</div>
</div>
<div class="flex flex-col w-full">
<div>Referencing Components</div>
<div>
<parameter-provider-references
[parameterProviderReferences]="
request.parameterProvider.component.referencingParameterContexts
"
(goToParameterContext)="
navigateToParameterContext($event)
"></parameter-provider-references>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="Properties">
<div class="tab-content py-4">
<property-table
formControlName="properties"
[createNewProperty]="createNewProperty"
[createNewService]="createNewService"
[goToService]="goToService"></property-table>
</div>
</mat-tab>
<mat-tab label="Comments">
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editParameterProviderForm.dirty || editParameterProviderForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-raised-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
</mat-dialog-actions>
</form>

View File

@ -0,0 +1,36 @@
/*!
* 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;
.parameter-provider-edit-form {
@include mat.button-density(-1);
.mdc-dialog__content {
padding: 0 16px;
font-size: 14px;
.tab-content {
height: 475px;
overflow-y: auto;
}
}
.mat-mdc-form-field {
width: 100%;
}
}

View File

@ -0,0 +1,170 @@
/*
* 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 { EditParameterProvider } from './edit-parameter-provider.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { EditParameterProviderRequest } from '../../../state/parameter-providers';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('EditParameterProvider', () => {
let component: EditParameterProvider;
let fixture: ComponentFixture<EditParameterProvider>;
const data: EditParameterProviderRequest = {
id: 'id',
parameterProvider: {
revision: {
clientId: '36ba1cc1-018d-1000-bc2c-787bc552d63d',
version: 6
},
id: '369487d7-018d-1000-817a-1d8d9a8f4a91',
uri: 'https://localhost:8443/nifi-api/parameter-providers/369487d7-018d-1000-817a-1d8d9a8f4a91',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [],
component: {
id: '369487d7-018d-1000-817a-1d8d9a8f4a91',
name: 'Group 1 - FileParameterProvider',
type: 'org.apache.nifi.parameter.FileParameterProvider',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-nar',
version: '2.0.0-SNAPSHOT'
},
comments: '',
persistsState: false,
restricted: true,
deprecated: false,
multipleVersionsAvailable: false,
properties: {
'parameter-group-directories': '/Users/rfellows/tmp/parameterProviders/group1',
'parameter-value-byte-limit': '256 B',
'parameter-value-encoding': 'plaintext'
},
descriptors: {
'parameter-group-directories': {
name: 'parameter-group-directories',
displayName: 'Parameter Group Directories',
description:
'A comma-separated list of directory absolute paths that will map to named parameter groups. Each directory that contains files will map to a parameter group, named after the innermost directory in the path. Files inside the directory will map to parameter names, whose values are the content of each respective file.',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'parameter-value-byte-limit': {
name: 'parameter-value-byte-limit',
displayName: 'Parameter Value Byte Limit',
description:
'The maximum byte size of a parameter value. Since parameter values are pulled from the contents of files, this is a safeguard that can prevent memory issues if large files are included.',
defaultValue: '256 B',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'parameter-value-encoding': {
name: 'parameter-value-encoding',
displayName: 'Parameter Value Encoding',
description: 'Indicates how parameter values are encoded inside Parameter files.',
defaultValue: 'base64',
allowableValues: [
{
allowableValue: {
displayName: 'Base64',
value: 'base64',
description:
'File content is Base64-encoded, and will be decoded before providing the value as a Parameter.'
},
canRead: true
},
{
allowableValue: {
displayName: 'Plain text',
value: 'plaintext',
description:
'File content is not encoded, and will be provided directly as a Parameter value.'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
}
},
parameterGroupConfigurations: [
{
groupName: 'group1',
parameterContextName: 'group1',
parameterSensitivities: {
bytes: 'NON_SENSITIVE',
password: 'SENSITIVE',
username: 'NON_SENSITIVE'
},
synchronized: true
}
],
referencingParameterContexts: [
{
id: '3716e18d-018d-1000-f203-4f6d571d572e',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [],
component: {
id: '3716e18d-018d-1000-f203-4f6d571d572e',
name: 'group1'
}
}
],
validationStatus: 'VALID',
extensionMissing: false
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditParameterProvider, NoopAnimationsModule],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: data
}
]
});
fixture = TestBed.createComponent(EditParameterProvider);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,141 @@
/*
* 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, EventEmitter, Inject, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { MatButtonModule } from '@angular/material/button';
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
import { Observable } from 'rxjs';
import {
ControllerServiceReferencingComponent,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
ParameterContextReferenceEntity,
Property
} from '../../../../../state/shared';
import {
EditParameterProviderRequest,
ParameterProviderEntity,
UpdateParameterProviderRequest
} from '../../../state/parameter-providers';
import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Client } from '../../../../../service/client.service';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ControllerServiceReferences } from '../../../../../ui/common/controller-service/controller-service-references/controller-service-references.component';
import { ParameterProviderReferences } from '../parameter-context-references/parameter-provider-references.component';
import { PropertyTable } from '../../../../../ui/common/property-table/property-table.component';
@Component({
selector: 'edit-parameter-provider',
standalone: true,
imports: [
CommonModule,
MatDialogModule,
MatTabsModule,
MatButtonModule,
NifiSpinnerDirective,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
ControllerServiceReferences,
ParameterProviderReferences,
PropertyTable
],
templateUrl: './edit-parameter-provider.component.html',
styleUrls: ['./edit-parameter-provider.component.scss']
})
export class EditParameterProvider {
@Input() createNewProperty!: (existingProperties: string[], allowsSensitive: boolean) => Observable<Property>;
@Input() createNewService!: (request: InlineServiceCreationRequest) => Observable<InlineServiceCreationResponse>;
@Input() goToService!: (serviceId: string) => void;
@Input() goToReferencingParameterContext!: (parameterContextId: string) => void;
@Input() saving$!: Observable<boolean>;
@Output() editParameterProvider: EventEmitter<UpdateParameterProviderRequest> =
new EventEmitter<UpdateParameterProviderRequest>();
editParameterProviderForm: FormGroup;
constructor(
@Inject(MAT_DIALOG_DATA) public request: EditParameterProviderRequest,
private formBuilder: FormBuilder,
private client: Client,
private nifiCommon: NiFiCommon
) {
const providerProperties = request.parameterProvider.component.properties;
const properties: Property[] = Object.entries(providerProperties).map((entry: any) => {
const [property, value] = entry;
return {
property,
value,
descriptor: request.parameterProvider.component.descriptors[property]
};
});
// build the form
this.editParameterProviderForm = this.formBuilder.group({
name: new FormControl(request.parameterProvider.component.name, Validators.required),
properties: new FormControl(properties),
comments: new FormControl(request.parameterProvider.component.comments)
});
}
formatType(entity: ParameterProviderEntity): string {
return this.nifiCommon.formatType(entity.component);
}
formatBundle(entity: ParameterProviderEntity): string {
return this.nifiCommon.formatBundle(entity.component.bundle);
}
submitForm(postUpdateNavigation?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.parameterProvider),
component: {
id: this.request.parameterProvider.id,
name: this.editParameterProviderForm.get('name')?.value,
comments: this.editParameterProviderForm.get('comments')?.value
}
};
const propertyControl: AbstractControl | null = this.editParameterProviderForm.get('properties');
if (propertyControl && propertyControl.dirty) {
const properties: Property[] = propertyControl.value;
const values: { [key: string]: string | null } = {};
properties.forEach((property) => (values[property.property] = property.value));
payload.component.properties = values;
payload.component.sensitiveDynamicPropertyNames = properties
.filter((property) => property.descriptor.dynamic && property.descriptor.sensitive)
.map((property) => property.descriptor.name);
}
this.editParameterProvider.next({
payload,
postUpdateNavigation
});
}
navigateToParameterContext(parameterContextReference: ParameterContextReferenceEntity) {
if (parameterContextReference.component?.id) {
this.goToReferencingParameterContext(parameterContextReference.component?.id);
}
}
}

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.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ParameterProviderReferences } from './parameter-provider-references.component';
describe('ParameterProviderReferences', () => {
let component: ParameterProviderReferences;
let fixture: ComponentFixture<ParameterProviderReferences>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ParameterProviderReferences]
});
fixture = TestBed.createComponent(ParameterProviderReferences);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
/*
* 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, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ParameterContextReferenceEntity } from '../../../../../state/shared';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { RouterLink } from '@angular/router';
@Component({
selector: 'parameter-provider-references',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './parameter-providers-references.component.html',
styleUrls: ['./parameter-providers-references.component.scss']
})
export class ParameterProviderReferences {
@Input() parameterProviderReferences!: ParameterContextReferenceEntity[];
@Output() goToParameterContext: EventEmitter<ParameterContextReferenceEntity> =
new EventEmitter<ParameterContextReferenceEntity>();
constructor(private nifiCommon: NiFiCommon) {}
getUnauthorized(references: ParameterContextReferenceEntity[]) {
return references.filter((p) => !p.permissions.canRead);
}
getAuthorized(references: ParameterContextReferenceEntity[]) {
return references.filter((p) => p.permissions.canRead);
}
goToParameterContextClicked(event: MouseEvent, parameterContextReference: ParameterContextReferenceEntity) {
event.stopPropagation();
this.goToParameterContext.next(parameterContextReference);
}
}

View File

@ -0,0 +1,76 @@
<!--
~ 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="parameter-provider-references">
<div
*ngIf="!parameterProviderReferences || parameterProviderReferences?.length === 0; else hasReferences"
class="value">
No referencing components
</div>
<ng-template #hasReferences>
<ul>
<ng-container
*ngTemplateOutlet="
parameterContexts;
context: {
$implicit: getAuthorized(parameterProviderReferences),
label: 'Parameter Contexts'
}
"></ng-container>
<ng-container
*ngTemplateOutlet="
unauthorized;
context: {
$implicit: getUnauthorized(parameterProviderReferences),
label: 'Unauthorized'
}
"></ng-container>
</ul>
</ng-template>
<ng-template #parameterContexts let-references let-label="label">
<ng-container *ngIf="references.length > 0">
<li>
<h4>
<span class="value">{{ label }}</span> ({{ references.length }})
</h4>
<div class="references">
<div *ngFor="let reference of references" class="flex items-center gap-x-2">
<a (click)="goToParameterContextClicked($event, reference)">{{ reference.component?.name }}</a>
</div>
</div>
</li>
</ng-container>
</ng-template>
<ng-template #unauthorized let-references let-label="label">
<ng-container *ngIf="references.length > 0">
<li>
<h4>
<span class="value">{{ label }}</span> ({{ references.length }})
</h4>
<div class="references">
<div *ngFor="let reference of references" class="flex items-center gap-x-2">
<div class="unset">{{ reference.id }}</div>
</div>
</div>
</li>
</ng-container>
</ng-template>
</div>

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.
*/
.parameter-provider-references {
ul.nested {
padding-inline-start: 20px;
}
.references {
margin-left: 20px;
}
}

View File

@ -0,0 +1,121 @@
<!--
~ 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.
-->
<ng-container>
<div class="parameter-providers-table h-full flex flex-col">
<div class="flex-1 relative">
<div class="listing-table overflow-y-auto border absolute inset-0">
<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">
<!-- TODO: open details -->
<div class="pointer fa fa-info-circle" title="View details"></div>
<!-- TODO: open documentation -->
<div class="pointer fa fa-book" title="View Documentation"></div>
<!-- Validation Errors -->
<div *ngIf="hasErrors(item)">
<div
class="pointer fa fa-warning has-errors"
nifiTooltip
[delayClose]="false"
[tooltipComponentType]="ValidationErrorsTip"
[tooltipInputData]="getValidationErrorsTipData(item)"></div>
</div>
</div>
</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
</th>
<td mat-cell *matCellDef="let item" [title]="formatName(item)">
<div [ngClass]="{ unset: !canRead(item) }">{{ formatName(item) }}</div>
</td>
</ng-container>
<!-- Type column -->
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
</th>
<td mat-cell *matCellDef="let item" [title]="formatType(item)">
{{ formatType(item) }}
</td>
</ng-container>
<!-- Bundle column -->
<ng-container matColumnDef="bundle">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Bundle</div>
</th>
<td mat-cell *matCellDef="let item" [title]="formatBundle(item)">
{{ formatBundle(item) }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3">
<div
class="pointer fa fa-pencil"
(click)="configureClicked(item, $event)"
title="Edit"></div>
<div
class="pointer fa fa-arrow-circle-down"
(click)="fetchClicked(item, $event)"
title="Fetch Parameters"></div>
<div
class="pointer fa fa-trash"
(click)="deleteClicked(item, $event)"
title="Remove"></div>
<div
*ngIf="canManageAccessPolicies()"
class="pointer fa fa-key"
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)"
title="Access Policies"></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"
[class.even]="even"
(click)="selectParameterProvider.next(row)"
[class.selected]="isSelected(row)"></tr>
</table>
</div>
</div>
</div>
</ng-container>

View File

@ -0,0 +1,22 @@
/*!
* 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.
*/
.parameter-providers-table {
.listing-table {
// overrides to the listing-table styles go here
}
}

View File

@ -0,0 +1,39 @@
/*
* 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 { ParameterProvidersTable } from './parameter-providers-table.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ParameterProvidersTable', () => {
let component: ParameterProvidersTable;
let fixture: ComponentFixture<ParameterProvidersTable>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ParameterProvidersTable, NoopAnimationsModule]
});
fixture = TestBed.createComponent(ParameterProvidersTable);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,176 @@
/*
* 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, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatSortModule, Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ParameterProviderEntity } from '../../../state/parameter-providers';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { CurrentUser } from '../../../../../state/current-user';
import { FlowConfiguration } from '../../../../../state/flow-configuration';
import { MatPaginatorModule } from '@angular/material/paginator';
import { SummaryTableFilterModule } from '../../../../summary/ui/common/summary-table-filter/summary-table-filter.module';
import { PortStatusSnapshotEntity } from '../../../../summary/state/summary-listing';
import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { ControllerServiceEntity, ValidationErrorsTipInput } from '../../../../../state/shared';
import { RouterLink } from '@angular/router';
export type SupportedColumns = 'name' | 'type' | 'bundle';
@Component({
selector: 'parameter-providers-table',
standalone: true,
imports: [
CommonModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
SummaryTableFilterModule,
NifiTooltipDirective,
RouterLink
],
templateUrl: './parameter-providers-table.component.html',
styleUrls: ['./parameter-providers-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
})
export class ParameterProvidersTable {
@Input() initialSortColumn: SupportedColumns = 'name';
@Input() initialSortDirection: SortDirection = 'asc';
displayedColumns: string[] = ['moreDetails', 'name', 'type', 'bundle', 'actions'];
dataSource: MatTableDataSource<ParameterProviderEntity> = new MatTableDataSource<ParameterProviderEntity>();
activeSort: Sort = {
active: this.initialSortColumn,
direction: this.initialSortDirection
};
constructor(private nifiCommon: NiFiCommon) {}
@Input() selectedParameterProviderId!: string;
@Input() currentUser!: CurrentUser;
@Input() flowConfiguration!: FlowConfiguration;
@Input() set parameterProviders(parameterProviders: ParameterProviderEntity[]) {
if (parameterProviders) {
this.dataSource.data = this.sortEntities(parameterProviders, this.activeSort);
}
}
@Output() selectParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() configureParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() deleteParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() fetchParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() manageAccessPolicies: EventEmitter<ParameterProviderEntity> = new EventEmitter<ParameterProviderEntity>();
protected readonly ValidationErrorsTip = ValidationErrorsTip;
canRead(entity: ParameterProviderEntity): boolean {
return entity.permissions.canRead;
}
canWrite(entity: ParameterProviderEntity): boolean {
return entity.permissions.canWrite;
}
canManageAccessPolicies(): boolean {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
isSelected(parameterProvider: ParameterProviderEntity): boolean {
if (this.selectedParameterProviderId) {
return parameterProvider.id === this.selectedParameterProviderId;
}
return false;
}
formatName(entity: ParameterProviderEntity): string {
return this.canRead(entity) ? entity.component.name : entity.id;
}
formatType(entity: ParameterProviderEntity): string {
return this.canRead(entity) ? this.nifiCommon.formatType(entity.component) : '';
}
formatBundle(entity: ParameterProviderEntity): string {
return this.canRead(entity) ? this.nifiCommon.formatBundle(entity.component.bundle) : '';
}
hasErrors(entity: ParameterProviderEntity): boolean {
return this.canRead(entity) && !this.nifiCommon.isEmpty(entity.component.validationErrors);
}
getValidationErrorsTipData(entity: ParameterProviderEntity): ValidationErrorsTipInput | null {
return {
isValidating: entity.component.validationStatus === 'VALIDATING',
validationErrors: entity.component?.validationErrors || []
};
}
sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}
private sortEntities(data: ParameterProviderEntity[], sort: Sort): ParameterProviderEntity[] {
if (!data) {
return [];
}
return data.slice().sort((a, b) => {
const isAsc: boolean = 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 'bundle':
retVal = this.nifiCommon.compareString(this.formatBundle(a), this.formatBundle(b));
break;
default:
return 0;
}
return retVal * (isAsc ? 1 : -1);
});
}
configureClicked(entity: ParameterProviderEntity, event: MouseEvent) {
event.stopPropagation();
this.configureParameterProvider.next(entity);
}
fetchClicked(entity: ParameterProviderEntity, event: MouseEvent) {
event.stopPropagation();
this.fetchParameterProvider.next(entity);
}
deleteClicked(entity: ParameterProviderEntity, event: MouseEvent) {
event.stopPropagation();
this.deleteParameterProvider.next(entity);
}
getPolicyLink(entity: ParameterProviderEntity): string[] {
return ['/access-policies', 'read', 'component', 'parameter-providers', entity.id];
}
}

View File

@ -15,4 +15,39 @@
~ limitations under the License.
-->
<p>parameter-providers works!</p>
<ng-container *ngIf="parameterProvidersState$ | async; let parameterProvidersState">
<div *ngIf="isInitialLoading(parameterProvidersState); else loaded">
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
</div>
<ng-template #loaded>
<div class="flex flex-col h-full gap-y-2" *ngIf="currentUser$ | async; let currentUser">
<div class="flex justify-end" *ngIf="currentUser.controllerPermissions.canWrite">
<button
class="nifi-button"
(click)="openNewParameterProviderDialog()"
title="Add a new parameter provider">
<i class="fa fa-plus"></i>
</button>
</div>
<div class="flex-1">
<parameter-providers-table
[currentUser]="currentUser"
[flowConfiguration]="(flowConfiguration$ | async)!"
[parameterProviders]="parameterProvidersState.parameterProviders"
[selectedParameterProviderId]="selectedParameterProviderId$ | async"
(deleteParameterProvider)="deleteParameterProvider($event)"
(configureParameterProvider)="openConfigureParameterProviderDialog($event)"
(selectParameterProvider)="selectParameterProvider($event)"></parameter-providers-table>
</div>
<div class="flex justify-between">
<div class="refresh-container flex items-center gap-x-2">
<button class="nifi-button" (click)="refreshParameterProvidersListing()">
<i class="fa fa-refresh" [class.fa-spin]="parameterProvidersState.status === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="refresh-timestamp">{{ parameterProvidersState.loadedTimestamp }}</div>
</div>
</div>
</div>
</ng-template>
</ng-container>

View File

@ -18,6 +18,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ParameterProviders } from './parameter-providers.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialParameterProvidersState } from '../../state/parameter-providers/parameter-providers.reducer';
describe('ParameterProviders', () => {
let component: ParameterProviders;
@ -25,7 +27,12 @@ describe('ParameterProviders', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParameterProviders]
declarations: [ParameterProviders],
providers: [
provideMockStore({
initialState: initialParameterProvidersState
})
]
});
fixture = TestBed.createComponent(ParameterProviders);
component = fixture.componentInstance;

View File

@ -15,11 +15,105 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { ParameterProviderEntity, ParameterProvidersState } from '../../state/parameter-providers';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
import {
selectParameterProvider,
selectParameterProviderIdFromRoute,
selectParameterProvidersState,
selectSingleEditedParameterProvider
} from '../../state/parameter-providers/parameter-providers.selectors';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import * as ParameterProviderActions from '../../state/parameter-providers/parameter-providers.actions';
import { initialParameterProvidersState } from '../../state/parameter-providers/parameter-providers.reducer';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isDefinedAndNotNull } from '../../../../state/shared';
@Component({
selector: 'parameter-providers',
templateUrl: './parameter-providers.component.html',
styleUrls: ['./parameter-providers.component.scss']
})
export class ParameterProviders {}
export class ParameterProviders implements OnInit, OnDestroy {
currentUser$ = this.store.select(selectCurrentUser);
parameterProvidersState$ = this.store.select(selectParameterProvidersState);
selectedParameterProviderId$ = this.store.select(selectParameterProviderIdFromRoute);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
constructor(private store: Store<NiFiState>) {
this.store
.select(selectSingleEditedParameterProvider)
.pipe(
isDefinedAndNotNull(),
switchMap((id: string) =>
this.store.select(selectParameterProvider(id)).pipe(isDefinedAndNotNull(), take(1))
),
takeUntilDestroyed()
)
.subscribe((entity) => {
this.store.dispatch(
ParameterProviderActions.openConfigureParameterProviderDialog({
request: {
id: entity.id,
parameterProvider: entity
}
})
);
});
}
ngOnInit(): void {
this.store.dispatch(loadFlowConfiguration());
this.store.dispatch(ParameterProviderActions.loadParameterProviders());
}
ngOnDestroy(): void {
this.store.dispatch(ParameterProviderActions.resetParameterProvidersState());
}
isInitialLoading(state: ParameterProvidersState): boolean {
// using the current timestamp to detect the initial load event
return state.loadedTimestamp == initialParameterProvidersState.loadedTimestamp;
}
refreshParameterProvidersListing(): void {
this.store.dispatch(ParameterProviderActions.loadParameterProviders());
}
openNewParameterProviderDialog() {
this.store.dispatch(ParameterProviderActions.openNewParameterProviderDialog());
}
openConfigureParameterProviderDialog(parameterProvider: ParameterProviderEntity) {
this.store.dispatch(
ParameterProviderActions.navigateToEditParameterProvider({
id: parameterProvider.component.id
})
);
}
selectParameterProvider(parameterProvider: ParameterProviderEntity) {
this.store.dispatch(
ParameterProviderActions.selectParameterProvider({
request: {
id: parameterProvider.id
}
})
);
}
deleteParameterProvider(parameterProvider: ParameterProviderEntity) {
this.store.dispatch(
ParameterProviderActions.promptParameterProviderDeletion({
request: {
parameterProvider
}
})
);
}
}

View File

@ -18,10 +18,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ParameterProviders } from './parameter-providers.component';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { ParameterContextListingModule } from '../../../parameter-contexts/ui/parameter-context-listing/parameter-context-listing.module';
import { ParameterProvidersTable } from './parameter-providers-table/parameter-providers-table.component';
@NgModule({
declarations: [ParameterProviders],
exports: [ParameterProviders],
imports: [CommonModule]
imports: [CommonModule, NgxSkeletonLoaderModule, ParameterProvidersTable]
})
export class ParameterProvidersModule {}

View File

@ -35,12 +35,13 @@ import { CurrentUser } from '../../../../../state/current-user';
export class ReportingTaskTable {
@Input() initialSortColumn: 'name' | 'type' | 'bundle' | 'state' = 'name';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
activeSort: Sort = {
active: this.initialSortColumn,
direction: this.initialSortDirection
};
@Input() set reportingTasks(reportingTaskEntities: ReportingTaskEntity[]) {
this.dataSource.data = this.sortEntities(reportingTaskEntities, {
active: this.initialSortColumn,
direction: this.initialSortDirection
});
this.dataSource.data = this.sortEntities(reportingTaskEntities, this.activeSort);
}
@Input() selectedReportingTaskId!: string;
@ -258,6 +259,7 @@ export class ReportingTaskTable {
}
sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}

View File

@ -32,19 +32,6 @@ import {
export class UsersService {
private static readonly API: string = '../nifi-api';
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
constructor(
private httpClient: HttpClient,
private client: Client,
@ -83,7 +70,7 @@ export class UsersService {
...request.userPayload
}
};
return this.httpClient.put(this.stripProtocol(request.uri), payload);
return this.httpClient.put(this.nifiCommon.stripProtocol(request.uri), payload);
}
updateUserGroup(request: UpdateUserGroupRequest): Observable<any> {
@ -94,16 +81,16 @@ export class UsersService {
...request.userGroupPayload
}
};
return this.httpClient.put(this.stripProtocol(request.uri), payload);
return this.httpClient.put(this.nifiCommon.stripProtocol(request.uri), payload);
}
deleteUser(user: UserEntity): Observable<any> {
const revision: any = this.client.getRevision(user);
return this.httpClient.delete(this.stripProtocol(user.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(user.uri), { params: revision });
}
deleteUserGroup(userGroup: UserGroupEntity): Observable<any> {
const revision: any = this.client.getRevision(userGroup);
return this.httpClient.delete(this.stripProtocol(userGroup.uri), { params: revision });
return this.httpClient.delete(this.nifiCommon.stripProtocol(userGroup.uri), { params: revision });
}
}

View File

@ -28,24 +28,11 @@ export class ComponentStateService {
private nifiCommon: NiFiCommon
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getComponentState(request: LoadComponentStateRequest): Observable<any> {
return this.httpClient.get(`${this.stripProtocol(request.componentUri)}/state`);
return this.httpClient.get(`${this.nifiCommon.stripProtocol(request.componentUri)}/state`);
}
clearComponentState(request: ClearComponentStateRequest): Observable<any> {
return this.httpClient.post(`${this.stripProtocol(request.componentUri)}/state/clear-requests`, {});
return this.httpClient.post(`${this.nifiCommon.stripProtocol(request.componentUri)}/state/clear-requests`, {});
}
}

View File

@ -36,25 +36,12 @@ export class ControllerServiceStateService {
private client: Client
) {}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
private stripProtocol(url: string): string {
return this.nifiCommon.substringAfterFirst(url, ':');
}
getControllerService(id: string): Observable<any> {
return this.httpClient.get(`${ControllerServiceStateService.API}/controller-services/${id}`);
}
setEnable(controllerService: ControllerServiceEntity, enabled: boolean): Observable<any> {
return this.httpClient.put(`${this.stripProtocol(controllerService.uri)}/run-status`, {
return this.httpClient.put(`${this.nifiCommon.stripProtocol(controllerService.uri)}/run-status`, {
revision: this.client.getRevision(controllerService),
state: enabled ? 'ENABLED' : 'DISABLED',
uiOnly: true
@ -69,7 +56,7 @@ export class ControllerServiceStateService {
true
);
return this.httpClient.put(`${this.stripProtocol(controllerService.uri)}/references`, {
return this.httpClient.put(`${this.nifiCommon.stripProtocol(controllerService.uri)}/references`, {
id: controllerService.id,
state: enabled ? 'ENABLED' : 'DISABLED',
referencingComponentRevisions: referencingComponentRevisions,
@ -89,7 +76,7 @@ export class ControllerServiceStateService {
false
);
return this.httpClient.put(`${this.stripProtocol(controllerService.uri)}/references`, {
return this.httpClient.put(`${this.nifiCommon.stripProtocol(controllerService.uri)}/references`, {
id: controllerService.id,
state: running ? 'RUNNING' : 'STOPPED',
referencingComponentRevisions: referencingComponentRevisions,

View File

@ -511,4 +511,17 @@ export class NiFiCommon {
public getAllPolicyTypeListing(): SelectOption[] {
return this.policyTypeListing;
}
/**
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
*
* https://stackoverflow.com/a/59586462
*
* @param url
* @private
*/
public stripProtocol(url: string): string {
return this.substringAfterFirst(url, ':');
}
}

View File

@ -0,0 +1,177 @@
/*
* 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 { MatDialog } from '@angular/material/dialog';
import { catchError, map, NEVER, Observable, switchMap, take } from 'rxjs';
import {
ControllerServiceCreator,
ControllerServiceEntity,
CreateControllerServiceRequest,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Property,
PropertyDescriptor,
PropertyDescriptorRetriever
} from '../state/shared';
import { NewPropertyDialog } from '../ui/common/new-property-dialog/new-property-dialog.component';
import { CreateControllerService } from '../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { ManagementControllerServiceService } from '../pages/settings/service/management-controller-service.service';
import { ExtensionTypesService } from './extension-types.service';
import { Client } from './client.service';
@Injectable({
providedIn: 'root'
})
export class PropertyTableHelperService {
constructor(
private dialog: MatDialog,
private extensionTypesService: ExtensionTypesService,
private client: Client
) {}
/**
* Returns a function that can be used to pass into a PropertyTable to support creating a new property
* @param id id of the component to create the property for
* @param propertyDescriptorService the service to call to get property descriptors
*/
createNewProperty(
id: string,
propertyDescriptorService: PropertyDescriptorRetriever
): (existingProperties: string[], allowsSensitive: boolean) => Observable<Property> {
return (existingProperties: string[], allowsSensitive: boolean) => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return propertyDescriptorService
.getPropertyDescriptor(id, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
}
/**
* Returns a function that can be used to pass into a PropertyTable to create a controller service, inline.
*
* @param id id of the component where the inline-create controller service was initiated
* @param controllerServiceCreator service to use to create the controller service and lookup property descriptors
* @param propertyDescriptorService the service to call to get property descriptors
* @param afterServiceCreated OPTIONAL - callback function to possibly dispatch an action after the controller service has been created
* @param processGroupId OPTIONAL - process group id to create the
*/
createNewService(
id: string,
controllerServiceCreator: ControllerServiceCreator,
propertyDescriptorService: PropertyDescriptorRetriever,
processGroupId?: string | null,
afterServiceCreated?: (createdResponse: ControllerServiceEntity) => void
): (request: InlineServiceCreationRequest) => Observable<InlineServiceCreationResponse> {
return (request: InlineServiceCreationRequest) => {
const descriptor: PropertyDescriptor = request.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session, and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
const payload: CreateControllerServiceRequest = {
revision: {
clientId: this.client.getClientId(),
version: 0
},
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
};
if (processGroupId) {
payload.processGroupId = processGroupId;
}
return controllerServiceCreator.createControllerService(payload).pipe(
take(1),
switchMap((createResponse) => {
// if provided, call the callback function
if (afterServiceCreated) {
afterServiceCreated(createResponse);
}
// fetch an updated property descriptor
return propertyDescriptorService
.getPropertyDescriptor(id, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createResponse.id,
descriptor: descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
}
}

View File

@ -51,6 +51,11 @@ export const selectFlowAnalysisRuleTypes = createSelector(
(state: ExtensionTypesState) => state.flowAnalysisRuleTypes
);
export const selectParameterProviderTypes = createSelector(
selectExtensionTypesState,
(state: ExtensionTypesState) => state.parameterProviderTypes
);
export const selectTypesToIdentifyComponentRestrictions = createSelector(
selectExtensionTypesState,
(state: ExtensionTypesState) => {

View File

@ -112,7 +112,7 @@ export interface EditTenantResponse {
userGroup?: any;
}
export interface CreateControllerServiceRequest {
export interface CreateControllerServiceDialogRequest {
controllerServiceTypes: DocumentedType[];
}
@ -306,6 +306,7 @@ export interface ParameterContextReferenceEntity {
permissions: Permissions;
id: string;
component?: ParameterContextReference;
bulletins?: BulletinEntity[];
}
export interface ParameterContextReference {
@ -508,6 +509,10 @@ export interface PropertyDescriptor {
identifiesControllerServiceBundle?: Bundle;
}
export interface PropertyDescriptorEntity {
propertyDescriptor: PropertyDescriptor;
}
export interface Property {
property: string;
value: string | null;
@ -522,3 +527,18 @@ export interface InlineServiceCreationResponse {
value: string;
descriptor: PropertyDescriptor;
}
export interface PropertyDescriptorRetriever {
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<PropertyDescriptorEntity>;
}
export interface CreateControllerServiceRequest {
processGroupId?: string;
controllerServiceType: string;
controllerServiceBundle: Bundle;
revision: Revision;
}
export interface ControllerServiceCreator {
createControllerService(createControllerService: CreateControllerServiceRequest): Observable<any>;
}

View File

@ -55,12 +55,13 @@ import { CurrentUser } from '../../../../state/current-user';
export class ControllerServiceTable {
@Input() initialSortColumn: 'name' | 'type' | 'bundle' | 'state' | 'scope' = 'name';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
activeSort: Sort = {
active: this.initialSortColumn,
direction: this.initialSortDirection
};
@Input() set controllerServices(controllerServiceEntities: ControllerServiceEntity[]) {
this.dataSource.data = this.sortEntities(controllerServiceEntities, {
active: this.initialSortColumn,
direction: this.initialSortDirection
});
this.dataSource.data = this.sortEntities(controllerServiceEntities, this.activeSort);
}
@Input() selectedServiceId!: string;
@ -277,6 +278,7 @@ export class ControllerServiceTable {
}
sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateControllerService } from './create-controller-service.component';
import { CreateControllerServiceRequest, DocumentedType } from '../../../../state/shared';
import { CreateControllerServiceDialogRequest, DocumentedType } from '../../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/extension-types/extension-types.reducer';
@ -28,7 +28,7 @@ describe('CreateControllerService', () => {
let component: CreateControllerService;
let fixture: ComponentFixture<CreateControllerService>;
const data: CreateControllerServiceRequest = {
const data: CreateControllerServiceDialogRequest = {
controllerServiceTypes: [
{
type: 'org.apache.nifi.services.azure.storage.ADLSCredentialsControllerService',

View File

@ -17,7 +17,7 @@
import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CreateControllerServiceRequest, DocumentedType } from '../../../../state/shared';
import { CreateControllerServiceDialogRequest, DocumentedType } from '../../../../state/shared';
import { ExtensionCreation } from '../../extension-creation/extension-creation.component';
import { Observable } from 'rxjs';
import { AsyncPipe } from '@angular/common';
@ -35,7 +35,7 @@ export class CreateControllerService {
controllerServiceTypes: DocumentedType[];
constructor(@Inject(MAT_DIALOG_DATA) private dialogRequest: CreateControllerServiceRequest) {
constructor(@Inject(MAT_DIALOG_DATA) private dialogRequest: CreateControllerServiceDialogRequest) {
this.controllerServiceTypes = dialogRequest.controllerServiceTypes;
}

View File

@ -49,16 +49,17 @@ export class ExtensionCreation {
this.selectedType = documentedTypes[0];
}
this.dataSource.data = this.sortEntities(documentedTypes, {
active: this.initialSortColumn,
direction: this.initialSortDirection
});
this.dataSource.data = this.sortEntities(documentedTypes, this.activeSort);
}
@Input() componentType!: string;
@Input() saving!: boolean;
@Input() initialSortColumn: 'type' | 'version' | 'tags' = 'type';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
activeSort: Sort = {
active: this.initialSortColumn,
direction: this.initialSortDirection
};
@Output() extensionTypeSelected: EventEmitter<DocumentedType> = new EventEmitter<DocumentedType>();
@ -151,6 +152,7 @@ export class ExtensionCreation {
}
sortData(sort: Sort) {
this.activeSort = sort;
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}