NIFI-12425: Controller Service Listing (#8091)

* NIFI-12425:
- Controller Service Listing.
- Adding lazy loading to the Canvas with the introduction of the Controller Service listing.
- Reorganizing existing components in the Flow Designer.
- Allowing the current Process Group to be configured.
- Inline Service creation.

* NIFI-12425:
- Removing unused imports.

This closes #8091
This commit is contained in:
Matt Gilman 2023-12-01 13:50:30 -05:00 committed by GitHub
parent d40bb3eda6
commit 387a101931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
234 changed files with 2216 additions and 483 deletions

View File

@ -102,6 +102,7 @@ import org.apache.nifi.web.api.entity.ControllerServiceReferencingComponentsEnti
import org.apache.nifi.web.api.entity.CurrentUserEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity;
import org.apache.nifi.web.api.entity.FlowComparisonEntity;
import org.apache.nifi.web.api.entity.FlowConfigurationEntity;
import org.apache.nifi.web.api.entity.FlowEntity;
@ -1008,6 +1009,14 @@ public interface NiFiServiceFacade {
*/
ProcessGroupFlowEntity getProcessGroupFlow(String groupId, boolean uiOnly);
/**
* Returns the breadcrumbs for the specified group.
*
* @param groupId group id
* @return the breadcrumbs
*/
FlowBreadcrumbEntity getProcessGroupBreadcrumbs(String groupId);
// ----------------------------------------
// ProcessGroup methods
// ----------------------------------------

View File

@ -297,6 +297,7 @@ import org.apache.nifi.web.api.entity.ControllerServiceReferencingComponentsEnti
import org.apache.nifi.web.api.entity.CurrentUserEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisRuleEntity;
import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity;
import org.apache.nifi.web.api.entity.FlowComparisonEntity;
import org.apache.nifi.web.api.entity.FlowConfigurationEntity;
import org.apache.nifi.web.api.entity.FlowEntity;
@ -4783,6 +4784,12 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
return entityFactory.createProcessGroupFlowEntity(dtoFactory.createProcessGroupFlowDto(processGroup, groupStatus, revisionManager, this::getProcessGroupBulletins, uiOnly), permissions);
}
@Override
public FlowBreadcrumbEntity getProcessGroupBreadcrumbs(final String groupId) {
final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);
return dtoFactory.createBreadcrumbEntity(processGroup);
}
@Override
public ProcessGroupEntity getProcessGroup(final String groupId) {
final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId);

View File

@ -88,6 +88,7 @@ import org.apache.nifi.web.api.entity.ControllerStatusEntity;
import org.apache.nifi.web.api.entity.CurrentUserEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisResultEntity;
import org.apache.nifi.web.api.entity.FlowAnalysisRuleTypesEntity;
import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity;
import org.apache.nifi.web.api.entity.FlowConfigurationEntity;
import org.apache.nifi.web.api.entity.FlowRegistryBucketEntity;
import org.apache.nifi.web.api.entity.FlowRegistryBucketsEntity;
@ -401,6 +402,43 @@ public class FlowResource extends ApplicationResource {
return generateOkResponse(entity).build();
}
@GET
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Path("process-groups/{id}/breadcrumbs")
@ApiOperation(
value = "Gets the breadcrumbs for a process group",
response = FlowBreadcrumbEntity.class,
authorizations = {
@Authorization(value = "Read - /flow")
}
)
@ApiResponses(
value = {
@ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@ApiResponse(code = 401, message = "Client could not be authenticated."),
@ApiResponse(code = 403, message = "Client is not authorized to make this request."),
@ApiResponse(code = 404, message = "The specified resource could not be found."),
@ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
}
)
public Response getBreadcrumbs(
@ApiParam(
value = "The process group id."
)
@PathParam("id") final String groupId) {
authorizeFlow();
if (isReplicateRequest()) {
return replicate(HttpMethod.GET);
}
// get the breadcrumbs for this process group
final FlowBreadcrumbEntity breadcrumbEntity = serviceFacade.getProcessGroupBreadcrumbs(groupId);
return generateOkResponse(breadcrumbEntity).build();
}
/**
* Retrieves the metrics of the entire flow.
*

View File

@ -1842,7 +1842,6 @@ public final class DtoFactory {
Collection<ValidationResult> validationErrors = null;
if (component instanceof ProcessorNode) {
final ProcessorNode node = ((ProcessorNode) component);
dto.setGroupId(node.getProcessGroup().getIdentifier());
dto.setState(node.getScheduledState().name());
dto.setActiveThreadCount(node.getActiveThreadCount());
dto.setType(node.getComponentType());
@ -1919,6 +1918,7 @@ public final class DtoFactory {
orderedProperties.putAll(sortedProperties);
// build the descriptor and property dtos
dto.setGroupId(processGroupId);
dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>());
dto.setProperties(new LinkedHashMap<String, String>());
for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) {
@ -2091,7 +2091,7 @@ public final class DtoFactory {
* @param group group
* @return dto
*/
private FlowBreadcrumbEntity createBreadcrumbEntity(final ProcessGroup group) {
public FlowBreadcrumbEntity createBreadcrumbEntity(final ProcessGroup group) {
if (group == null) {
return null;
}

View File

@ -45,7 +45,8 @@ const routes: Routes = [
{
path: '',
canMatch: [authGuard],
loadChildren: () => import('./pages/canvas/feature/flow-designer.module').then((m) => m.FlowDesignerModule)
loadChildren: () =>
import('./pages/flow-designer/feature/flow-designer.module').then((m) => m.FlowDesignerModule)
}
];

View File

@ -20,7 +20,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlowDesignerModule } from './pages/canvas/feature/flow-designer.module';
import { FlowDesignerModule } from './pages/flow-designer/feature/flow-designer.module';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

View File

@ -1,55 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { CommonModule, NgOptimizedImage } from '@angular/common';
import { HeaderComponent } from './header.component';
import { FlowStatus } from './flow-status/flow-status.component';
import { CdkDrag } from '@angular/cdk/drag-drop';
import { NewCanvasItem } from './new-canvas-item/new-canvas-item.component';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { Search } from './search/search.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { RouterLink } from '@angular/router';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
@NgModule({
declarations: [HeaderComponent, NewCanvasItem, FlowStatus, Search],
exports: [HeaderComponent],
imports: [
CommonModule,
NgOptimizedImage,
CdkDrag,
MatButtonModule,
MatMenuModule,
MatDividerModule,
FormsModule,
ReactiveFormsModule,
MatFormFieldModule,
MatAutocompleteModule,
CdkConnectedOverlay,
CdkOverlayOrigin,
RouterLink,
NifiTooltipDirective
]
})
export class HeaderModule {}

View File

@ -25,6 +25,7 @@ import { EffectsModule } from '@ngrx/effects';
import { CounterListingEffects } from '../state/counter-listing/counter-listing.effects';
import { CounterListingModule } from '../ui/counter-listing/counter-listing.module';
import { ParameterContextListingModule } from '../../parameter-contexts/ui/parameter-context-listing/parameter-context-listing.module';
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [Counters],
@ -35,7 +36,8 @@ import { ParameterContextListingModule } from '../../parameter-contexts/ui/param
StoreModule.forFeature(countersFeatureKey, reducers),
EffectsModule.forFeature(CounterListingEffects),
CounterListingModule,
ParameterContextListingModule
ParameterContextListingModule,
MatDialogModule
]
})
export class CountersModule {}

View File

@ -20,22 +20,28 @@ import { RouterModule, Routes } from '@angular/router';
import { FlowDesigner } from './flow-designer.component';
import { RootGroupRedirector } from '../ui/root/redirector/root-group-redirector.component';
import { rootGroupGuard } from '../ui/root/guard/root-group.guard';
import { Canvas } from '../ui/canvas/canvas.component';
import { ControllerServices } from '../ui/controller-service/controller-services.component';
const routes: Routes = [
{
path: 'process-groups/:processGroupId',
component: FlowDesigner,
children: [
{ path: 'bulk/:ids', component: FlowDesigner },
{
path: ':type/:id',
component: FlowDesigner,
children: [{ path: 'edit', component: FlowDesigner }]
path: 'controller-services',
loadChildren: () =>
import('../ui/controller-service/controller-services.module').then(
(m) => m.ControllerServicesModule
)
},
{
path: '',
loadChildren: () => import('../ui/canvas/canvas.module').then((m) => m.CanvasModule)
}
]
},
{ path: '', component: RootGroupRedirector, canActivate: [rootGroupGuard] }
// { path: '**', component: FlowDesignerComponent }
];
@NgModule({

View File

@ -15,8 +15,4 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<fd-header></fd-header>
<fd-canvas class="flex-1"></fd-canvas>
<fd-footer></fd-footer>
</div>
<router-outlet></router-outlet>

View File

@ -20,33 +20,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FlowDesigner } from './flow-designer.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../state/flow/flow.reducer';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
describe('FlowDesigner', () => {
let component: FlowDesigner;
let fixture: ComponentFixture<FlowDesigner>;
@Component({
selector: 'fd-header',
template: ''
})
class MockHeader {}
@Component({
selector: 'fd-canvas',
template: ''
})
class MockCanvas {}
@Component({
selector: 'fd-footer',
template: ''
})
class MockFooter {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FlowDesigner, MockHeader, MockCanvas, MockFooter],
declarations: [FlowDesigner],
imports: [RouterTestingModule],
providers: [
provideMockStore({
initialState

View File

@ -19,9 +19,6 @@ import { NgModule } from '@angular/core';
import { CommonModule, NgOptimizedImage } from '@angular/common';
import { FlowDesigner } from './flow-designer.component';
import { FlowDesignerRoutingModule } from './flow-designer-routing.module';
import { HeaderModule } from '../ui/header/header.module';
import { FooterModule } from '../ui/footer/footer.module';
import { CanvasModule } from '../ui/canvas/canvas.module';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { FlowEffects } from '../state/flow/flow.effects';
@ -29,18 +26,16 @@ import { TransformEffects } from '../state/transform/transform.effects';
import { VersionControlTip } from '../ui/common/tooltips/version-control-tip/version-control-tip.component';
import { canvasFeatureKey, reducers } from '../state';
import { MatDialogModule } from '@angular/material/dialog';
import { ControllerServicesEffects } from '../state/controller-services/controller-services.effects';
@NgModule({
declarations: [FlowDesigner, VersionControlTip],
exports: [FlowDesigner],
imports: [
CommonModule,
HeaderModule,
CanvasModule,
FooterModule,
FlowDesignerRoutingModule,
StoreModule.forFeature(canvasFeatureKey, reducers),
EffectsModule.forFeature(FlowEffects, TransformEffects),
EffectsModule.forFeature(FlowEffects, TransformEffects, ControllerServicesEffects),
NgOptimizedImage,
MatDialogModule
]

View File

@ -25,6 +25,8 @@ import { flowFeatureKey } from '../../state/flow';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { CanvasState } from '../../state';
import { transformFeatureKey } from '../../state/transform';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('ConnectableBehavior', () => {
let service: ConnectableBehavior;
@ -32,7 +34,8 @@ describe('ConnectableBehavior', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -22,11 +22,12 @@ import * as fromFlow from '../../state/flow/flow.reducer';
import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectTransform } from '../../state/transform/transform.selectors';
import { initialState } from '../../state/transform/transform.reducer';
import { CanvasState } from '../../state';
import { flowFeatureKey } from '../../state/flow';
import { transformFeatureKey } from '../../state/transform';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('DraggableBehavior', () => {
let service: DraggableBehavior;
@ -34,7 +35,8 @@ describe('DraggableBehavior', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,13 +26,16 @@ import * as fromFlow from '../../state/flow/flow.reducer';
import { transformFeatureKey } from '../../state/transform';
import * as fromTransform from '../../state/transform/transform.reducer';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('EditableBehaviorService', () => {
let service: EditableBehavior;
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
beforeEach(() => {

View File

@ -19,13 +19,14 @@ import { TestBed } from '@angular/core/testing';
import { QuickSelectBehavior } from './quick-select-behavior.service';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../state/flow/flow.reducer';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { CanvasState } from '../../state';
import { flowFeatureKey } from '../../state/flow';
import * as fromFlow from '../../state/flow/flow.reducer';
import { transformFeatureKey } from '../../state/transform';
import * as fromTransform from '../../state/transform/transform.reducer';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('QuickSelectBehavior', () => {
let service: QuickSelectBehavior;
@ -33,7 +34,8 @@ describe('QuickSelectBehavior', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -24,6 +24,8 @@ import { transformFeatureKey } from '../../state/transform';
import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('SelectableBehavior', () => {
let service: SelectableBehavior;
@ -31,7 +33,8 @@ describe('SelectableBehavior', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../state/flow/flow.selectors';
import { selectTransform } from '../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../state/controller-services';
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
describe('BirdseyeView', () => {
let service: BirdseyeView;
@ -33,7 +35,8 @@ describe('BirdseyeView', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -25,7 +25,8 @@ import { transformFeatureKey } from '../state/transform';
import * as fromTransform from '../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../state/flow/flow.selectors';
import { selectTransform } from '../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../state/controller-services';
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
describe('CanvasUtils', () => {
let service: CanvasUtils;
@ -33,7 +34,8 @@ describe('CanvasUtils', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../state/flow/flow.selectors';
import { selectTransform } from '../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../state/controller-services';
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
describe('CanvasView', () => {
let service: CanvasView;
@ -33,7 +35,8 @@ describe('CanvasView', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } 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,
CreateControllerServiceRequest,
DeleteControllerServiceRequest
} from '../state/controller-services';
@Injectable({ providedIn: 'root' })
export class ControllerServiceService {
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,
private nifiCommon: NiFiCommon
) {}
getControllerServices(processGroupId: string): Observable<any> {
const uiOnly: any = { uiOnly: true };
return this.httpClient.get(
`${ControllerServiceService.API}/flow/process-groups/${processGroupId}/controller-services`,
{
params: uiOnly
}
);
}
getBreadcrumbs(processGroupId: string): Observable<any> {
return this.httpClient.get(`${ControllerServiceService.API}/flow/process-groups/${processGroupId}/breadcrumbs`);
}
getControllerService(id: string): Observable<any> {
return this.httpClient.get(`${ControllerServiceService.API}/controller-services/${id}`);
}
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
}
}
);
}
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
const params: any = {
propertyName,
sensitive
};
return this.httpClient.get(`${ControllerServiceService.API}/controller-services/${id}/descriptors`, {
params
});
}
updateControllerService(configureControllerService: ConfigureControllerServiceRequest): Observable<any> {
return this.httpClient.put(
this.stripProtocol(configureControllerService.uri),
configureControllerService.payload
);
}
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 });
}
}

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('ConnectionManager', () => {
let service: ConnectionManager;
@ -33,7 +35,8 @@ describe('ConnectionManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('FunnelManager', () => {
let service: FunnelManager;
@ -33,7 +35,8 @@ describe('FunnelManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('LabelManager', () => {
let service: LabelManager;
@ -33,7 +35,8 @@ describe('LabelManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('PortManager', () => {
let service: PortManager;
@ -33,7 +35,8 @@ describe('PortManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('ProcessGroupManager', () => {
let service: ProcessGroupManager;
@ -33,7 +35,8 @@ describe('ProcessGroupManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('ProcessorManager', () => {
let service: ProcessorManager;
@ -33,7 +35,8 @@ describe('ProcessorManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -26,6 +26,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
describe('RemoteProcessGroupManager', () => {
let service: RemoteProcessGroupManager;
@ -33,7 +35,8 @@ describe('RemoteProcessGroupManager', () => {
beforeEach(() => {
const initialState: CanvasState = {
[flowFeatureKey]: fromFlow.initialState,
[transformFeatureKey]: fromTransform.initialState
[transformFeatureKey]: fromTransform.initialState,
[controllerServicesFeatureKey]: fromControllerServices.initialState
};
TestBed.configureTestingModule({

View File

@ -0,0 +1,102 @@
/*
* 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 {
ConfigureControllerServiceRequest,
ConfigureControllerServiceSuccess,
CreateControllerServiceRequest,
CreateControllerServiceSuccess,
DeleteControllerServiceRequest,
DeleteControllerServiceSuccess,
LoadControllerServicesRequest,
LoadControllerServicesResponse,
SelectControllerServiceRequest
} from './index';
import { EditControllerServiceDialogRequest } from '../../../../state/shared';
export const loadControllerServices = createAction(
'[Controller Services] Load Controller Services',
props<{ request: LoadControllerServicesRequest }>()
);
export const loadControllerServicesSuccess = createAction(
'[Controller Services] Load Controller Services Success',
props<{ response: LoadControllerServicesResponse }>()
);
export const controllerServicesApiError = createAction(
'[Controller Services] Load Controller Service Error',
props<{ error: string }>()
);
export const openNewControllerServiceDialog = createAction('[Controller Services] Open New Controller Service Dialog');
export const createControllerService = createAction(
'[Controller Services] Create Controller Service',
props<{ request: CreateControllerServiceRequest }>()
);
export const createControllerServiceSuccess = createAction(
'[Controller Services] Create Controller Service Success',
props<{ response: CreateControllerServiceSuccess }>()
);
export const inlineCreateControllerServiceSuccess = createAction(
'[Controller Services] Inline Create Controller Service Success',
props<{ response: CreateControllerServiceSuccess }>()
);
export const navigateToEditService = createAction(
'[Controller Services] Navigate To Edit Service',
props<{ id: string }>()
);
export const openConfigureControllerServiceDialog = createAction(
'[Controller Services] Open Configure Controller Service Dialog',
props<{ request: EditControllerServiceDialogRequest }>()
);
export const configureControllerService = createAction(
'[Controller Services] Configure Controller Service',
props<{ request: ConfigureControllerServiceRequest }>()
);
export const configureControllerServiceSuccess = createAction(
'[Controller Services] Configure Controller Service Success',
props<{ response: ConfigureControllerServiceSuccess }>()
);
export const promptControllerServiceDeletion = createAction(
'[Controller Services] Prompt Controller Service Deletion',
props<{ request: DeleteControllerServiceRequest }>()
);
export const deleteControllerService = createAction(
'[Controller Services] Delete Controller Service',
props<{ request: DeleteControllerServiceRequest }>()
);
export const deleteControllerServiceSuccess = createAction(
'[Controller Services] Delete Controller Service Success',
props<{ response: DeleteControllerServiceSuccess }>()
);
export const selectControllerService = createAction(
'[Controller Services] Select Controller Service',
props<{ request: SelectControllerServiceRequest }>()
);

View File

@ -0,0 +1,466 @@
/*
* 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, createEffect, ofType } from '@ngrx/effects';
import * as ControllerServicesActions from './controller-services.actions';
import {
catchError,
combineLatest,
from,
map,
NEVER,
Observable,
of,
switchMap,
take,
tap,
withLatestFrom
} from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { selectControllerServiceTypes } from '../../../../state/extension-types/extension-types.selectors';
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { Client } from '../../../../service/client.service';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditControllerService } from '../../../../ui/common/controller-service/edit-controller-service/edit-controller-service.component';
import {
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Property,
PropertyDescriptor
} 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';
import { ControllerServiceService } from '../../service/controller-service.service';
@Injectable()
export class ControllerServicesEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private client: Client,
private controllerServiceService: ControllerServiceService,
private extensionTypesService: ExtensionTypesService,
private dialog: MatDialog,
private router: Router
) {}
loadControllerServices$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServicesActions.loadControllerServices),
map((action) => action.request),
switchMap((request) =>
combineLatest([
this.controllerServiceService.getControllerServices(request.processGroupId),
this.controllerServiceService.getBreadcrumbs(request.processGroupId)
]).pipe(
map(([controllerServicesResponse, breadcrumbsResponse]) =>
ControllerServicesActions.loadControllerServicesSuccess({
response: {
processGroupId: breadcrumbsResponse.id,
controllerServices: controllerServicesResponse.controllerServices,
loadedTimestamp: controllerServicesResponse.currentTime,
breadcrumb: breadcrumbsResponse
}
})
),
catchError((error) =>
of(
ControllerServicesActions.controllerServicesApiError({
error: error.error
})
)
)
)
)
)
);
openNewControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.openNewControllerServiceDialog),
withLatestFrom(
this.store.select(selectControllerServiceTypes),
this.store.select(selectCurrentProcessGroupId)
),
tap(([action, controllerServiceTypes, processGroupId]) => {
const dialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes
},
panelClass: 'medium-dialog'
});
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
dialogReference.componentInstance.createControllerService
.pipe(take(1))
.subscribe((controllerServiceType) => {
this.store.dispatch(
ControllerServicesActions.createControllerService({
request: {
revision: {
clientId: this.client.getClientId(),
version: 0
},
processGroupId,
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
}
})
);
});
})
),
{ dispatch: false }
);
createControllerService$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServicesActions.createControllerService),
map((action) => action.request),
switchMap((request) =>
from(this.controllerServiceService.createControllerService(request)).pipe(
map((response) =>
ControllerServicesActions.createControllerServiceSuccess({
response: {
controllerService: response
}
})
),
catchError((error) =>
of(
ControllerServicesActions.controllerServicesApiError({
error: error.error
})
)
)
)
)
)
);
createControllerServiceSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.createControllerServiceSuccess),
tap(() => {
this.dialog.closeAll();
})
),
{ dispatch: false }
);
navigateToEditService$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.navigateToEditService),
map((action) => action.id),
withLatestFrom(this.store.select(selectCurrentProcessGroupId)),
tap(([id, processGroupId]) => {
this.router.navigate(['/process-groups', processGroupId, 'controller-services', id, 'edit']);
})
),
{ dispatch: false }
);
openConfigureControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.openConfigureControllerServiceDialog),
map((action) => action.request),
withLatestFrom(this.store.select(selectCurrentProcessGroupId)),
tap(([request, processGroupId]) => {
const serviceId: string = request.id;
const editDialogReference = this.dialog.open(EditControllerService, {
data: {
controllerService: request.controllerService
},
panelClass: 'large-dialog'
});
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.getServiceLink = (serviceId: string) => {
return this.controllerServiceService.getControllerService(serviceId).pipe(
take(1),
map((serviceEntity) => {
return [
'/process-groups',
serviceEntity.component.parentGroupId,
'controller-services',
serviceEntity.id
];
})
);
};
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) => {
// dispatch an inline create service success action so the new service is in the state
this.store.dispatch(
ControllerServicesActions.inlineCreateControllerServiceSuccess(
{
response: {
controllerService: createReponse
}
}
)
);
// fetch an updated property descriptor
return this.controllerServiceService
.getPropertyDescriptor(serviceId, 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.editControllerService
.pipe(take(1))
.subscribe((payload: any) => {
this.store.dispatch(
ControllerServicesActions.configureControllerService({
request: {
id: request.controllerService.id,
uri: request.controllerService.uri,
payload
}
})
);
});
editDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(
ControllerServicesActions.selectControllerService({
request: {
processGroupId,
id: serviceId
}
})
);
}
});
})
),
{ dispatch: false }
);
configureControllerService$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServicesActions.configureControllerService),
map((action) => action.request),
switchMap((request) =>
from(this.controllerServiceService.updateControllerService(request)).pipe(
map((response) =>
ControllerServicesActions.configureControllerServiceSuccess({
response: {
id: request.id,
controllerService: response
}
})
),
catchError((error) =>
of(
ControllerServicesActions.controllerServicesApiError({
error: error.error
})
)
)
)
)
)
);
configureControllerServiceSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.configureControllerServiceSuccess),
tap(() => {
this.dialog.closeAll();
})
),
{ dispatch: false }
);
promptControllerServiceDeletion$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.promptControllerServiceDeletion),
map((action) => action.request),
tap((request) => {
const dialogReference = this.dialog.open(YesNoDialog, {
data: {
title: 'Delete Controller Service',
message: `Delete controller service ${request.controllerService.component.name}?`
},
panelClass: 'small-dialog'
});
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
this.store.dispatch(
ControllerServicesActions.deleteControllerService({
request
})
);
});
})
),
{ dispatch: false }
);
deleteControllerService$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServicesActions.deleteControllerService),
map((action) => action.request),
switchMap((request) =>
from(this.controllerServiceService.deleteControllerService(request)).pipe(
map((response) =>
ControllerServicesActions.deleteControllerServiceSuccess({
response: {
controllerService: response
}
})
),
catchError((error) =>
of(
ControllerServicesActions.controllerServicesApiError({
error: error.error
})
)
)
)
)
)
);
selectControllerService$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.selectControllerService),
map((action) => action.request),
tap((request) => {
this.router.navigate([
'/process-groups',
request.processGroupId,
'controller-services',
request.id
]);
})
),
{ dispatch: false }
);
}

View File

@ -0,0 +1,113 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createReducer, on } from '@ngrx/store';
import {
configureControllerService,
configureControllerServiceSuccess,
controllerServicesApiError,
createControllerService,
createControllerServiceSuccess,
deleteControllerServiceSuccess,
inlineCreateControllerServiceSuccess,
loadControllerServices,
loadControllerServicesSuccess
} from './controller-services.actions';
import { produce } from 'immer';
import { ControllerServicesState } from './index';
export const initialState: ControllerServicesState = {
processGroupId: 'root',
controllerServices: [],
breadcrumb: {
id: '',
permissions: {
canRead: false,
canWrite: false
},
versionedFlowState: '',
breadcrumb: {
id: '',
name: ''
}
},
saving: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
export const controllerServicesReducer = createReducer(
initialState,
on(loadControllerServices, (state) => ({
...state,
status: 'loading' as const
})),
on(loadControllerServicesSuccess, (state, { response }) => ({
...state,
processGroupId: response.processGroupId,
controllerServices: response.controllerServices,
breadcrumb: response.breadcrumb,
loadedTimestamp: response.loadedTimestamp,
error: null,
status: 'success' as const
})),
on(controllerServicesApiError, (state, { error }) => ({
...state,
saving: false,
error,
status: 'error' as const
})),
on(createControllerService, (state, { request }) => ({
...state,
saving: true
})),
on(createControllerServiceSuccess, (state, { response }) => {
return produce(state, (draftState) => {
draftState.controllerServices.push(response.controllerService);
draftState.saving = false;
});
}),
on(inlineCreateControllerServiceSuccess, (state, { response }) => {
return produce(state, (draftState) => {
draftState.controllerServices.push(response.controllerService);
});
}),
on(configureControllerService, (state, { request }) => ({
...state,
saving: true
})),
on(configureControllerServiceSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const componentIndex: number = draftState.controllerServices.findIndex((f: any) => response.id === f.id);
if (componentIndex > -1) {
draftState.controllerServices[componentIndex] = response.controllerService;
}
draftState.saving = false;
});
}),
on(deleteControllerServiceSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const componentIndex: number = draftState.controllerServices.findIndex(
(f: any) => response.controllerService.id === f.id
);
if (componentIndex > -1) {
draftState.controllerServices.splice(componentIndex, 1);
}
});
})
);

View File

@ -0,0 +1,70 @@
/*
* 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 { selectCurrentRoute } from '../../../../state/router/router.selectors';
import { ControllerServiceEntity } from '../../../../state/shared';
import { CanvasState, selectCanvasState } from '../index';
import { controllerServicesFeatureKey, ControllerServicesState } from './index';
export const selectControllerServicesState = createSelector(
selectCanvasState,
(state: CanvasState) => state[controllerServicesFeatureKey]
);
export const selectSaving = createSelector(
selectControllerServicesState,
(state: ControllerServicesState) => state.saving
);
export const selectCurrentProcessGroupId = createSelector(
selectControllerServicesState,
(state: ControllerServicesState) => state.processGroupId
);
export const selectProcessGroupIdFromRoute = createSelector(selectCurrentRoute, (route) => {
if (route) {
// always select the process group from the route
return route.params.processGroupId;
}
return null;
});
export const selectControllerServiceIdFromRoute = createSelector(selectCurrentRoute, (route) => {
if (route) {
// always select the controller service from the route
return route.params.id;
}
return null;
});
export const selectSingleEditedService = createSelector(selectCurrentRoute, (route) => {
if (route?.routeConfig?.path == 'edit') {
return route.params.id;
}
return null;
});
export const selectServices = createSelector(
selectControllerServicesState,
(state: ControllerServicesState) => state.controllerServices
);
export const selectService = (id: string) =>
createSelector(selectServices, (services: ControllerServiceEntity[]) =>
services.find((service) => id == service.id)
);

View File

@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Bundle, ControllerServiceEntity, Revision } from '../../../../state/shared';
import { BreadcrumbEntity } from '../shared';
export const controllerServicesFeatureKey = 'controllerServiceListing';
export interface LoadControllerServicesRequest {
processGroupId: string;
}
export interface LoadControllerServicesResponse {
processGroupId: string;
breadcrumb: BreadcrumbEntity;
controllerServices: ControllerServiceEntity[];
loadedTimestamp: string;
}
export interface CreateControllerServiceRequest {
processGroupId: string;
controllerServiceType: string;
controllerServiceBundle: Bundle;
revision: Revision;
}
export interface CreateControllerServiceSuccess {
controllerService: ControllerServiceEntity;
}
export interface ConfigureControllerServiceRequest {
id: string;
uri: string;
payload: any;
}
export interface ConfigureControllerServiceSuccess {
id: string;
controllerService: ControllerServiceEntity;
}
export interface DeleteControllerServiceRequest {
controllerService: ControllerServiceEntity;
}
export interface DeleteControllerServiceSuccess {
controllerService: ControllerServiceEntity;
}
export interface SelectControllerServiceRequest {
processGroupId: string;
id: string;
}
export interface ControllerServicesState {
processGroupId: string;
breadcrumb: BreadcrumbEntity;
controllerServices: ControllerServiceEntity[];
saving: boolean;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -51,7 +51,9 @@ import {
UpdateConnectionRequest,
UpdateConnectionSuccess,
UpdatePositionsRequest,
UploadProcessGroupRequest
UploadProcessGroupRequest,
EditCurrentProcessGroupRequest,
NavigateToControllerServicesRequest
} from './index';
/*
@ -243,6 +245,20 @@ export const navigateToEditComponent = createAction(
export const editComponent = createAction('[Canvas] Edit Component', props<{ request: EditComponentDialogRequest }>());
export const navigateToEditCurrentProcessGroup = createAction('[Canvas] Navigate To Edit Current Process Group');
export const navigateToControllerServicesForProcessGroup = createAction(
'[Canvas] Navigate To Controller Services For Process Group',
props<{ request: NavigateToControllerServicesRequest }>()
);
export const editCurrentProcessGroup = createAction(
'[Canvas] Edit Current Process Group',
props<{
request: EditCurrentProcessGroupRequest;
}>()
);
export const openEditPortDialog = createAction(
'[Canvas] Open Edit Port Dialog',
props<{ request: EditComponentDialogRequest }>()

View File

@ -28,6 +28,7 @@ import {
interval,
map,
mergeMap,
NEVER,
Observable,
of,
switchMap,
@ -60,15 +61,18 @@ import {
} from './flow.selectors';
import { ConnectionManager } from '../../service/manager/connection-manager.service';
import { MatDialog } from '@angular/material/dialog';
import { CreatePort } from '../../ui/port/create-port/create-port.component';
import { EditPort } from '../../ui/port/edit-port/edit-port.component';
import { CreatePort } from '../../ui/canvas/items/port/create-port/create-port.component';
import { EditPort } from '../../ui/canvas/items/port/edit-port/edit-port.component';
import {
ComponentType,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Parameter,
ParameterEntity,
Property
Property,
PropertyDescriptor
} from '../../../../state/shared';
import { Router } from '@angular/router';
import { Client } from '../../../../service/client.service';
@ -76,16 +80,20 @@ import { CanvasUtils } from '../../service/canvas-utils.service';
import { CanvasView } from '../../service/canvas-view.service';
import { selectProcessorTypes } from '../../../../state/extension-types/extension-types.selectors';
import { NiFiState } from '../../../../state';
import { CreateProcessor } from '../../ui/processor/create-processor/create-processor.component';
import { EditProcessor } from '../../ui/processor/edit-processor/edit-processor.component';
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/process-group/create-process-group/create-process-group.component';
import { CreateConnection } from '../../ui/connection/create-connection/create-connection.component';
import { EditConnectionComponent } from '../../ui/connection/edit-connection/edit-connection.component';
import { CreateProcessGroup } from '../../ui/canvas/items/process-group/create-process-group/create-process-group.component';
import { CreateConnection } from '../../ui/canvas/items/connection/create-connection/create-connection.component';
import { EditConnectionComponent } from '../../ui/canvas/items/connection/edit-connection/edit-connection.component';
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
import { GroupComponents } from '../../ui/process-group/group-components/group-components.component';
import { EditProcessGroup } from '../../ui/process-group/edit-process-group/edit-process-group.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 * as ControllerServicesActions from '../controller-services/controller-services.actions';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { ControllerServiceService } from '../../service/controller-service.service';
@Injectable()
export class FlowEffects {
@ -93,6 +101,8 @@ export class FlowEffects {
private actions$: Actions,
private store: Store<NiFiState>,
private flowService: FlowService,
private extensionTypesService: ExtensionTypesService,
private controllerServiceService: ControllerServiceService,
private client: Client,
private canvasUtils: CanvasUtils,
private canvasView: CanvasView,
@ -642,7 +652,31 @@ export class FlowEffects {
{ dispatch: false }
);
editComponentRequest$ = createEffect(() =>
navigateToEditCurrentProcessGroup$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.navigateToEditCurrentProcessGroup),
withLatestFrom(this.store.select(selectCurrentProcessGroupId)),
tap(([action, processGroupId]) => {
this.router.navigate(['/process-groups', processGroupId, 'edit']);
})
),
{ dispatch: false }
);
navigateToControllerServicesForProcessGroup$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.navigateToControllerServicesForProcessGroup),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/process-groups', request.id, 'controller-services']);
})
),
{ dispatch: false }
);
editComponent$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.editComponent),
map((action) => action.request),
@ -664,6 +698,33 @@ export class FlowEffects {
)
);
editCurrentProcessGroup$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.editCurrentProcessGroup),
map((action) => action.request),
switchMap((request) =>
from(this.flowService.getProcessGroup(request.id)).pipe(
map((response) =>
FlowActions.openEditProcessGroupDialog({
request: {
type: ComponentType.ProcessGroup,
uri: response.uri,
entity: response
}
})
),
catchError((error) =>
of(
FlowActions.flowApiError({
error: error.error
})
)
)
)
)
)
);
openEditPortDialog$ = createEffect(
() =>
this.actions$.pipe(
@ -701,8 +762,13 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.openEditProcessorDialog),
map((action) => action.request),
withLatestFrom(this.store.select(selectCurrentParameterContext)),
tap(([request, parameterContext]) => {
withLatestFrom(
this.store.select(selectCurrentParameterContext),
this.store.select(selectCurrentProcessGroupId)
),
tap(([request, parameterContext, processGroupId]) => {
const processorId: string = request.entity.id;
const editDialogReference = this.dialog.open(EditProcessor, {
data: request,
panelClass: 'large-dialog'
@ -724,11 +790,7 @@ export class FlowEffects {
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.flowService
.getPropertyDescriptor(
request.entity.id,
dialogResponse.name,
dialogResponse.sensitive
)
.getPropertyDescriptor(processorId, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
@ -763,7 +825,6 @@ export class FlowEffects {
return this.flowService.getControllerService(serviceId).pipe(
take(1),
map((serviceEntity) => {
// TODO - finalize once route is defined
return [
'/process-groups',
serviceEntity.component.parentGroupId,
@ -774,7 +835,85 @@ export class FlowEffects {
);
};
// TODO - inline service creation...
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) => {
// dispatch an inline create service success action so the new service is in the state
this.store.dispatch(
ControllerServicesActions.inlineCreateControllerServiceSuccess(
{
response: {
controllerService: 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.editProcessor
.pipe(takeUntil(editDialogReference.afterClosed()))
@ -782,7 +921,7 @@ export class FlowEffects {
this.store.dispatch(
FlowActions.updateProcessor({
request: {
id: request.entity.id,
id: processorId,
uri: request.uri,
type: request.type,
payload
@ -791,20 +930,23 @@ export class FlowEffects {
);
});
editDialogReference.afterClosed().subscribe(() => {
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(FlowActions.clearFlowApiError());
this.store.dispatch(
FlowActions.selectComponents({
request: {
components: [
{
id: request.entity.id,
componentType: request.type
}
]
}
})
);
if (response != 'ROUTED') {
this.store.dispatch(
FlowActions.selectComponents({
request: {
components: [
{
id: processorId,
componentType: request.type
}
]
}
})
);
}
});
})
),

View File

@ -111,6 +111,16 @@ export const selectSingleEditedComponent = createSelector(selectCurrentRoute, (r
return selectedComponent;
});
export const selectEditedCurrentProcessGroup = createSelector(selectCurrentRoute, (route) => {
if (route?.routeConfig?.path == 'edit') {
if (route.params.ids == null && route.params.type == null) {
return route.params.processGroupId;
}
}
return null;
});
export const selectTransitionRequired = createSelector(selectFlowState, (state: FlowState) => state.transitionRequired);
export const selectDragging = createSelector(selectFlowState, (state: FlowState) => state.dragging);

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Position } from '../shared';
import { BreadcrumbEntity, Position } from '../shared';
import {
BulletinEntity,
Bundle,
@ -220,6 +220,14 @@ export interface EditComponentDialogRequest {
entity: any;
}
export interface NavigateToControllerServicesRequest {
id: string;
}
export interface EditCurrentProcessGroupRequest {
id: string;
}
export interface EditConnectionDialogRequest extends EditComponentDialogRequest {
newDestination?: {
type: ComponentType | null;
@ -362,20 +370,6 @@ export interface ComponentEntity {
component: any;
}
export interface Breadcrumb {
id: string;
name: string;
versionControlInformation?: VersionControlInformation;
}
export interface BreadcrumbEntity {
id: string;
permissions: Permissions;
versionedFlowState: string;
breadcrumb: Breadcrumb;
parentBreadcrumb?: BreadcrumbEntity;
}
export interface Relationship {
autoTerminate: boolean;
description: string;

View File

@ -24,18 +24,22 @@ import { flowFeatureKey, FlowState } from './flow';
import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
import { transformReducer } from './transform/transform.reducer';
import { flowReducer } from './flow/flow.reducer';
import { controllerServicesFeatureKey, ControllerServicesState } from './controller-services';
import { controllerServicesReducer } from './controller-services/controller-services.reducer';
export const canvasFeatureKey = 'canvas';
export interface CanvasState {
[flowFeatureKey]: FlowState;
[transformFeatureKey]: CanvasTransform;
[controllerServicesFeatureKey]: ControllerServicesState;
}
export function reducers(state: CanvasState | undefined, action: Action) {
return combineReducers({
[flowFeatureKey]: flowReducer,
[transformFeatureKey]: transformReducer
[transformFeatureKey]: transformReducer,
[controllerServicesFeatureKey]: controllerServicesReducer
})(state, action);
}

View File

@ -0,0 +1,43 @@
/*
* 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 { Permissions } from '../../../../state/shared';
import { VersionControlInformation } from '../flow';
export interface Dimension {
width: number;
height: number;
}
export interface Position {
x: number;
y: number;
}
export interface Breadcrumb {
id: string;
name: string;
versionControlInformation?: VersionControlInformation;
}
export interface BreadcrumbEntity {
id: string;
permissions: Permissions;
versionedFlowState: string;
breadcrumb: Breadcrumb;
parentBreadcrumb?: BreadcrumbEntity;
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Canvas } from './canvas.component';
const routes: Routes = [
{
path: '',
component: Canvas,
children: [
{ path: 'bulk/:ids', component: Canvas },
{ path: 'edit', component: Canvas },
{
path: ':type/:id',
component: Canvas,
children: [{ path: 'edit', component: Canvas }]
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CanvasRoutingModule {}

View File

@ -15,6 +15,12 @@
~ limitations under the License.
-->
<div id="canvas-container" class="canvas-background h-full" [cdkContextMenuTriggerFor]="contextMenu.menu"></div>
<fd-context-menu #contextMenu menuId="root"></fd-context-menu>
<graph-controls></graph-controls>
<div class="flex flex-col h-screen justify-between">
<fd-header></fd-header>
<div class="flex-1">
<div id="canvas-container" class="canvas-background h-full" [cdkContextMenuTriggerFor]="contextMenu.menu"></div>
<fd-context-menu #contextMenu menuId="root"></fd-context-menu>
<graph-controls></graph-controls>
</div>
<fd-footer></fd-footer>
</div>

View File

@ -21,23 +21,35 @@ import { Canvas } from './canvas.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../state/flow/flow.reducer';
import { ContextMenu } from './context-menu/context-menu.component';
import { GraphControls } from './graph-controls/graph-controls.component';
import { OperationControl } from './operation-control/operation-control.component';
import { NavigationControl } from './navigation-control/navigation-control.component';
import { Component } from '@angular/core';
import { CdkContextMenuTrigger } from '@angular/cdk/menu';
import { selectBreadcrumbs } from '../../state/flow/flow.selectors';
import { BreadcrumbEntity } from '../../state/flow';
import { BreadcrumbEntity } from '../../state/shared';
describe('Canvas', () => {
let component: Canvas;
let fixture: ComponentFixture<Canvas>;
@Component({
selector: 'birdseye',
selector: 'fd-header',
standalone: true,
template: ''
})
class MockBirdseye {}
class MockHeader {}
@Component({
selector: 'fd-footer',
standalone: true,
template: ''
})
class MockFooter {}
@Component({
selector: 'graph-controls',
standalone: true,
template: ''
})
class MockGraphControls {}
beforeEach(() => {
const breadcrumbEntity: BreadcrumbEntity = {
@ -54,8 +66,8 @@ describe('Canvas', () => {
};
TestBed.configureTestingModule({
declarations: [Canvas, ContextMenu, GraphControls, OperationControl, NavigationControl, MockBirdseye],
imports: [CdkContextMenuTrigger],
declarations: [Canvas],
imports: [CdkContextMenuTrigger, ContextMenu, MockGraphControls, MockHeader, MockFooter],
providers: [
provideMockStore({
initialState,

View File

@ -23,6 +23,7 @@ import {
centerSelectedComponent,
deselectAllComponents,
editComponent,
editCurrentProcessGroup,
loadProcessGroup,
selectComponents,
setSkipTransform,
@ -39,6 +40,7 @@ import {
selectBulkSelectedComponentIds,
selectConnection,
selectCurrentProcessGroupId,
selectEditedCurrentProcessGroup,
selectFunnel,
selectInputPort,
selectLabel,
@ -216,6 +218,23 @@ export class Canvas implements OnInit, OnDestroy {
})
);
});
// edit the current process group from the route
this.store
.select(selectEditedCurrentProcessGroup)
.pipe(
filter((processGroupId) => processGroupId != null),
takeUntilDestroyed()
)
.subscribe((processGroupId) => {
this.store.dispatch(
editCurrentProcessGroup({
request: {
id: processGroupId
}
})
);
});
}
ngOnInit(): void {

View File

@ -21,13 +21,24 @@ import { Canvas } from './canvas.component';
import { ContextMenu } from './context-menu/context-menu.component';
import { CdkContextMenuTrigger, CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
import { GraphControls } from './graph-controls/graph-controls.component';
import { NavigationControl } from './navigation-control/navigation-control.component';
import { OperationControl } from './operation-control/operation-control.component';
import { Birdseye } from './birdseye/birdseye.component';
import { CanvasRoutingModule } from './canvas-routing.module';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
@NgModule({
declarations: [Canvas, ContextMenu, GraphControls, NavigationControl, Birdseye, OperationControl],
declarations: [Canvas],
exports: [Canvas],
imports: [CommonModule, CdkMenu, CdkMenuItem, CdkMenuTrigger, CdkContextMenuTrigger]
imports: [
CommonModule,
CdkMenu,
CdkMenuItem,
CdkMenuTrigger,
CdkContextMenuTrigger,
CanvasRoutingModule,
GraphControls,
ContextMenu,
HeaderComponent,
FooterComponent
]
})
export class CanvasModule {}

View File

@ -27,7 +27,7 @@ describe('ContextMenu', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ContextMenu],
imports: [ContextMenu],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(ContextMenu);

View File

@ -27,12 +27,16 @@ import {
leaveProcessGroup,
moveComponents,
navigateToComponent,
navigateToControllerServicesForProcessGroup,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
reloadFlow
} from '../../../state/flow/flow.actions';
import { CanvasUtils } from '../../../service/canvas-utils.service';
import { DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
import { ComponentType } from '../../../../../state/shared';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
export interface ContextMenuItemDefinition {
isSeparator?: boolean;
@ -50,7 +54,9 @@ export interface ContextMenuDefinition {
@Component({
selector: 'fd-context-menu',
standalone: true,
templateUrl: './context-menu.component.html',
imports: [NgForOf, AsyncPipe, CdkMenu, CdkMenuItem, NgIf, CdkMenuTrigger],
styleUrls: ['./context-menu.component.scss']
})
export class ContextMenu implements OnInit {
@ -286,8 +292,9 @@ export class ContextMenu implements OnInit {
clazz: 'fa fa-gear',
text: 'Configure',
action: function (store: Store<CanvasState>, selection: any) {
// TODO - when selection is empty support configuring the current Process Group
if (!selection.empty()) {
if (selection.empty()) {
store.dispatch(navigateToEditCurrentProcessGroup());
} else {
const selectionData = selection.datum();
store.dispatch(
navigateToEditComponent({
@ -300,6 +307,33 @@ export class ContextMenu implements OnInit {
}
}
},
{
condition: function (canvasUtils: CanvasUtils, selection: any) {
return canvasUtils.isProcessGroup(selection) || selection.empty();
},
clazz: 'fa fa-list',
text: 'Controller Services',
action: function (store: Store<CanvasState>, selection: any, canvasUtils: CanvasUtils) {
if (selection.empty()) {
store.dispatch(
navigateToControllerServicesForProcessGroup({
request: {
id: canvasUtils.getProcessGroupId()
}
})
);
} else {
const selectionData = selection.datum();
store.dispatch(
navigateToControllerServicesForProcessGroup({
request: {
id: selectionData.id
}
})
);
}
}
},
{
condition: function (canvasUtils: CanvasUtils, selection: any) {
// TODO - hasDetails

View File

@ -16,7 +16,7 @@
-->
<footer>
<div class="bg-nifi-accent">
<div class="bg-nifi-accent breadcrumb-container">
<breadcrumbs
[entity]="(breadcrumbs$ | async)!"
[currentProcessGroupId]="(currentProcessGroupId$ | async)!"></breadcrumbs>

View File

@ -14,3 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.breadcrumb-container {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
background-color: rgba(249, 250, 251, 0.9);
border-top: 1px solid #aabbc3;
color: #598599;
z-index: 3;
}

View File

@ -19,12 +19,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../state/flow/flow.reducer';
import { Breadcrumbs } from './breadcrumbs/breadcrumbs.component';
import { BreadcrumbEntity } from '../../state/flow';
import { selectBreadcrumbs } from '../../state/flow/flow.selectors';
import { initialState } from '../../../state/flow/flow.reducer';
import { selectBreadcrumbs } from '../../../state/flow/flow.selectors';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { BreadcrumbEntity } from '../../../state/shared';
describe('FooterComponent', () => {
let component: FooterComponent;
@ -45,7 +44,6 @@ describe('FooterComponent', () => {
};
TestBed.configureTestingModule({
declarations: [FooterComponent, Breadcrumbs],
imports: [RouterModule, RouterTestingModule],
providers: [
provideMockStore({

View File

@ -16,13 +16,17 @@
*/
import { Component } from '@angular/core';
import { selectBreadcrumbs, selectCurrentProcessGroupId } from '../../state/flow/flow.selectors';
import { selectBreadcrumbs, selectCurrentProcessGroupId } from '../../../state/flow/flow.selectors';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../state';
import { CanvasState } from '../../../state';
import { Breadcrumbs } from '../../common/breadcrumbs/breadcrumbs.component';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'fd-footer',
standalone: true,
templateUrl: './footer.component.html',
imports: [Breadcrumbs, AsyncPipe],
styleUrls: ['./footer.component.scss']
})
export class FooterComponent {

View File

@ -20,11 +20,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GraphControls } from './graph-controls.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/flow/flow.reducer';
import { NavigationControl } from '../navigation-control/navigation-control.component';
import { OperationControl } from '../operation-control/operation-control.component';
import { NavigationControl } from './navigation-control/navigation-control.component';
import { OperationControl } from './operation-control/operation-control.component';
import { Component } from '@angular/core';
import { BreadcrumbEntity } from '../../../state/flow';
import { selectBreadcrumbs } from '../../../state/flow/flow.selectors';
import { Birdseye } from './navigation-control/birdseye/birdseye.component';
import { BreadcrumbEntity } from '../../../state/shared';
describe('GraphControls', () => {
let component: GraphControls;
@ -32,6 +33,7 @@ describe('GraphControls', () => {
@Component({
selector: 'birdseye',
standalone: true,
template: ''
})
class MockBirdseye {}
@ -51,7 +53,7 @@ describe('GraphControls', () => {
};
TestBed.configureTestingModule({
declarations: [GraphControls, NavigationControl, OperationControl, MockBirdseye],
imports: [GraphControls, NavigationControl, OperationControl, MockBirdseye],
providers: [
provideMockStore({
initialState,
@ -63,6 +65,13 @@ describe('GraphControls', () => {
]
})
]
}).overrideComponent(NavigationControl, {
remove: {
imports: [Birdseye]
},
add: {
imports: [MockBirdseye]
}
});
fixture = TestBed.createComponent(GraphControls);

View File

@ -23,10 +23,15 @@ import {
selectNavigationCollapsed,
selectOperationCollapsed
} from '../../../state/flow/flow.selectors';
import { NavigationControl } from './navigation-control/navigation-control.component';
import { OperationControl } from './operation-control/operation-control.component';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'graph-controls',
standalone: true,
templateUrl: './graph-controls.component.html',
imports: [NavigationControl, OperationControl, AsyncPipe],
styleUrls: ['./graph-controls.component.scss']
})
export class GraphControls {

View File

@ -18,7 +18,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Birdseye } from './birdseye.component';
import { BirdseyeView } from '../../../service/birdseye-view.service';
import { BirdseyeView } from '../../../../../service/birdseye-view.service';
import SpyObj = jasmine.SpyObj;
import createSpyObj = jasmine.createSpyObj;
@ -31,7 +31,7 @@ describe('Birdseye', () => {
birdseyeViewSpy = createSpyObj('BirdseyeView', ['init', 'refresh']);
TestBed.configureTestingModule({
declarations: [Birdseye],
imports: [Birdseye],
providers: [{ provide: BirdseyeView, useValue: birdseyeViewSpy }]
});
fixture = TestBed.createComponent(Birdseye);

View File

@ -16,10 +16,11 @@
*/
import { Component, OnInit } from '@angular/core';
import { BirdseyeView } from '../../../service/birdseye-view.service';
import { BirdseyeView } from '../../../../../service/birdseye-view.service';
@Component({
selector: 'birdseye',
standalone: true,
templateUrl: './birdseye.component.html',
styleUrls: ['./birdseye.component.scss']
})

View File

@ -19,8 +19,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NavigationControl } from './navigation-control.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/flow/flow.reducer';
import { initialState } from '../../../../state/flow/flow.reducer';
import { Component } from '@angular/core';
import { Birdseye } from './birdseye/birdseye.component';
describe('NavigationControl', () => {
let component: NavigationControl;
@ -28,19 +29,28 @@ describe('NavigationControl', () => {
@Component({
selector: 'birdseye',
standalone: true,
template: ''
})
class MockBirdseye {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [NavigationControl, MockBirdseye],
imports: [NavigationControl, MockBirdseye],
providers: [
provideMockStore({
initialState
})
]
}).overrideComponent(NavigationControl, {
remove: {
imports: [Birdseye]
},
add: {
imports: [MockBirdseye]
}
});
fixture = TestBed.createComponent(NavigationControl);
component = fixture.componentInstance;
fixture.detectChanges();

View File

@ -17,16 +17,20 @@
import { Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../../state';
import { zoomActual, zoomFit, zoomIn, zoomOut } from '../../../state/transform/transform.actions';
import { leaveProcessGroup, setNavigationCollapsed } from '../../../state/flow/flow.actions';
import { CanvasUtils } from '../../../service/canvas-utils.service';
import { initialState } from '../../../state/flow/flow.reducer';
import { Storage } from '../../../../../service/storage.service';
import { CanvasState } from '../../../../state';
import { zoomActual, zoomFit, zoomIn, zoomOut } from '../../../../state/transform/transform.actions';
import { leaveProcessGroup, setNavigationCollapsed } from '../../../../state/flow/flow.actions';
import { CanvasUtils } from '../../../../service/canvas-utils.service';
import { initialState } from '../../../../state/flow/flow.reducer';
import { Storage } from '../../../../../../service/storage.service';
import { NgIf } from '@angular/common';
import { Birdseye } from './birdseye/birdseye.component';
@Component({
selector: 'navigation-control',
standalone: true,
templateUrl: './navigation-control.component.html',
imports: [NgIf, Birdseye],
styleUrls: ['./navigation-control.component.scss']
})
export class NavigationControl {

View File

@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OperationControl } from './operation-control.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/flow/flow.reducer';
import { initialState } from '../../../../state/flow/flow.reducer';
describe('OperationControl', () => {
let component: OperationControl;
@ -27,7 +27,7 @@ describe('OperationControl', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [OperationControl],
imports: [OperationControl],
providers: [
provideMockStore({
initialState

View File

@ -21,17 +21,21 @@ import {
getParameterContextsAndOpenGroupComponentsDialog,
navigateToEditComponent,
setOperationCollapsed
} from '../../../state/flow/flow.actions';
} from '../../../../state/flow/flow.actions';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../../state';
import { CanvasUtils } from '../../../service/canvas-utils.service';
import { initialState } from '../../../state/flow/flow.reducer';
import { Storage } from '../../../../../service/storage.service';
import { BreadcrumbEntity, DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
import { CanvasState } from '../../../../state';
import { CanvasUtils } from '../../../../service/canvas-utils.service';
import { initialState } from '../../../../state/flow/flow.reducer';
import { Storage } from '../../../../../../service/storage.service';
import { DeleteComponentRequest, MoveComponentRequest } from '../../../../state/flow';
import { NgIf } from '@angular/common';
import { BreadcrumbEntity } from '../../../../state/shared';
@Component({
selector: 'operation-control',
standalone: true,
templateUrl: './operation-control.component.html',
imports: [NgIf],
styleUrls: ['./operation-control.component.scss']
})
export class OperationControl {

Some files were not shown because too many files have changed in this diff Show More