NIFI-12529: Enable/Disable Controller Service (#8185)

* NIFI-12529:
- Enable/Disable Controller Services.
- Resetting state when destroying Controller Service listings, General, Registry Clients, and Reporting Tasks.

* NIFI-12529:
- Addressing review feedback.

This closes #8185
This commit is contained in:
Matt Gilman 2023-12-28 08:36:21 -05:00 committed by GitHub
parent 211d1001f2
commit a73d812c23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2719 additions and 36 deletions

View File

@ -38,6 +38,7 @@ import { MatNativeDateModule } from '@angular/material/core';
import { AboutEffects } from './state/about/about.effects';
import { StatusHistoryEffects } from './state/status-history/status-history.effects';
import { MatDialogModule } from '@angular/material/dialog';
import { ControllerServiceStateEffects } from './state/contoller-service-state/controller-service-state.effects';
// @ts-ignore
@NgModule({
@ -56,7 +57,13 @@ import { MatDialogModule } from '@angular/material/dialog';
routerState: RouterState.Minimal,
navigationActionTiming: NavigationActionTiming.PostActivation
}),
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects, StatusHistoryEffects),
EffectsModule.forRoot(
UserEffects,
ExtensionTypesEffects,
AboutEffects,
StatusHistoryEffects,
ControllerServiceStateEffects
),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,

View File

@ -27,7 +27,13 @@ import {
LoadControllerServicesResponse,
SelectControllerServiceRequest
} from './index';
import { EditControllerServiceDialogRequest } from '../../../../state/shared';
import {
DisableControllerServiceDialogRequest,
EditControllerServiceDialogRequest,
SetEnableControllerServiceDialogRequest
} from '../../../../state/shared';
export const resetControllerServicesState = createAction('[Controller Services] Reset Controller Services State');
export const loadControllerServices = createAction(
'[Controller Services] Load Controller Services',
@ -81,6 +87,16 @@ export const configureControllerServiceSuccess = createAction(
props<{ response: ConfigureControllerServiceSuccess }>()
);
export const openEnableControllerServiceDialog = createAction(
'[Controller Services] Open Enable Controller Service Dialog',
props<{ request: SetEnableControllerServiceDialogRequest }>()
);
export const openDisableControllerServiceDialog = createAction(
'[Controller Services] Open Disable Controller Service Dialog',
props<{ request: DisableControllerServiceDialogRequest }>()
);
export const promptControllerServiceDeletion = createAction(
'[Controller Services] Prompt Controller Service Deletion',
props<{ request: DeleteControllerServiceRequest }>()

View File

@ -67,6 +67,8 @@ import { EditParameterDialog } from '../../../../ui/common/edit-parameter-dialog
import { selectParameterSaving } from '../parameter/parameter.selectors';
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';
@Injectable()
export class ControllerServicesEffects {
@ -548,6 +550,86 @@ export class ControllerServicesEffects {
{ dispatch: false }
);
openEnableControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.openEnableControllerServiceDialog),
map((action) => action.request),
withLatestFrom(this.store.select(selectCurrentProcessGroupId)),
tap(([request, currentProcessGroupId]) => {
const serviceId: string = request.id;
const enableDialogReference = this.dialog.open(EnableControllerService, {
data: request,
id: serviceId,
panelClass: 'large-dialog'
});
enableDialogReference.componentInstance.goToReferencingComponent = (
component: ControllerServiceReferencingComponent
) => {
enableDialogReference.close('ROUTED');
const route: string[] = this.getRouteForReference(component);
this.router.navigate(route);
};
enableDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(
ControllerServicesActions.loadControllerServices({
request: {
processGroupId: currentProcessGroupId
}
})
);
}
});
})
),
{ dispatch: false }
);
openDisableControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.openDisableControllerServiceDialog),
map((action) => action.request),
withLatestFrom(this.store.select(selectCurrentProcessGroupId)),
tap(([request, currentProcessGroupId]) => {
const serviceId: string = request.id;
const enableDialogReference = this.dialog.open(DisableControllerService, {
data: request,
id: serviceId,
panelClass: 'large-dialog'
});
enableDialogReference.componentInstance.goToReferencingComponent = (
component: ControllerServiceReferencingComponent
) => {
enableDialogReference.close('ROUTED');
const route: string[] = this.getRouteForReference(component);
this.router.navigate(route);
};
enableDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(
ControllerServicesActions.loadControllerServices({
request: {
processGroupId: currentProcessGroupId
}
})
);
}
});
})
),
{ dispatch: false }
);
promptControllerServiceDeletion$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -26,7 +26,8 @@ import {
deleteControllerServiceSuccess,
inlineCreateControllerServiceSuccess,
loadControllerServices,
loadControllerServicesSuccess
loadControllerServicesSuccess,
resetControllerServicesState
} from './controller-services.actions';
import { produce } from 'immer';
import { ControllerServicesState } from './index';
@ -54,6 +55,9 @@ export const initialState: ControllerServicesState = {
export const controllerServicesReducer = createReducer(
initialState,
on(resetControllerServicesState, (state) => ({
...initialState
})),
on(loadControllerServices, (state) => ({
...state,
status: 'loading' as const

View File

@ -61,7 +61,7 @@ import {
Loading Flow
*/
export const resetState = createAction('[Canvas] Reset State');
export const resetFlowState = createAction('[Canvas] Reset Flow State');
export const reloadFlow = createAction('[Canvas] Reload Flow');

View File

@ -37,7 +37,7 @@ import {
loadProcessorSuccess,
loadRemoteProcessGroupSuccess,
navigateWithoutTransform,
resetState,
resetFlowState,
setDragging,
setNavigationCollapsed,
setOperationCollapsed,
@ -139,7 +139,7 @@ export const initialState: FlowState = {
export const flowReducer = createReducer(
initialState,
on(resetState, (state) => ({
on(resetFlowState, (state) => ({
...initialState
})),
on(loadProcessGroup, (state, { request }) => ({

View File

@ -25,7 +25,7 @@ import {
editComponent,
editCurrentProcessGroup,
loadProcessGroup,
resetState,
resetFlowState,
selectComponents,
setSkipTransform,
startProcessGroupPolling,
@ -562,7 +562,7 @@ export class Canvas implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.store.dispatch(resetState());
this.store.dispatch(resetFlowState());
this.store.dispatch(stopProcessGroupPolling());
}
}

View File

@ -45,6 +45,8 @@
[definedByCurrentGroup]="definedByCurrentGroup(serviceState.breadcrumb)"
(selectControllerService)="selectControllerService($event)"
(configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
</div>
<div class="flex justify-between">

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { filter, switchMap, take, tap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -31,8 +31,11 @@ import {
loadControllerServices,
navigateToEditService,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
openEnableControllerServiceDialog,
openNewControllerServiceDialog,
promptControllerServiceDeletion,
resetControllerServicesState,
selectControllerService
} from '../../state/controller-services/controller-services.actions';
import { initialState } from '../../state/controller-services/controller-services.reducer';
@ -44,7 +47,7 @@ import { BreadcrumbEntity } from '../../state/shared';
templateUrl: './controller-services.component.html',
styleUrls: ['./controller-services.component.scss']
})
export class ControllerServices {
export class ControllerServices implements OnDestroy {
serviceState$ = this.store.select(selectControllerServicesState);
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
@ -154,6 +157,28 @@ export class ControllerServices {
);
}
enableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openEnableControllerServiceDialog({
request: {
id: entity.id,
controllerService: entity
}
})
);
}
disableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openDisableControllerServiceDialog({
request: {
id: entity.id,
controllerService: entity
}
})
);
}
deleteControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
promptControllerServiceDeletion({
@ -177,4 +202,8 @@ export class ControllerServices {
})
);
}
ngOnDestroy(): void {
this.store.dispatch(resetControllerServicesState());
}
}

View File

@ -18,6 +18,8 @@
import { createAction, props } from '@ngrx/store';
import { ControllerConfigResponse, UpdateControllerConfigRequest } from './index';
export const resetGeneralState = createAction('[General] Reset General State');
export const loadControllerConfig = createAction('[General] Load Controller Config');
export const loadControllerConfigSuccess = createAction(

View File

@ -21,6 +21,7 @@ import {
controllerConfigApiError,
loadControllerConfig,
loadControllerConfigSuccess,
resetGeneralState,
updateControllerConfigSuccess
} from './general.actions';
import { Revision } from '../../../../state/shared';
@ -45,6 +46,9 @@ export const initialState: GeneralState = {
export const generalReducer = createReducer(
initialState,
on(resetGeneralState, (state) => ({
...initialState
})),
on(loadControllerConfig, (state) => ({
...state,
status: 'loading' as const

View File

@ -26,7 +26,15 @@ import {
LoadManagementControllerServicesResponse,
SelectControllerServiceRequest
} from './index';
import { EditControllerServiceDialogRequest } from '../../../../state/shared';
import {
DisableControllerServiceDialogRequest,
EditControllerServiceDialogRequest,
SetEnableControllerServiceDialogRequest
} from '../../../../state/shared';
export const resetManagementControllerServicesState = createAction(
'[Management Controller Services] Reset Management Controller Services State'
);
export const loadManagementControllerServices = createAction(
'[Management Controller Services] Load Management Controller Services'
@ -81,6 +89,16 @@ export const configureControllerServiceSuccess = createAction(
props<{ response: ConfigureControllerServiceSuccess }>()
);
export const openEnableControllerServiceDialog = createAction(
'[Management Controller Services] Open Enable Controller Service Dialog',
props<{ request: SetEnableControllerServiceDialogRequest }>()
);
export const openDisableControllerServiceDialog = createAction(
'[Management Controller Services] Open Disable Controller Service Dialog',
props<{ request: DisableControllerServiceDialogRequest }>()
);
export const promptControllerServiceDeletion = createAction(
'[Management Controller Services] Prompt Controller Service Deletion',
props<{ request: DeleteControllerServiceRequest }>()

View File

@ -43,6 +43,8 @@ import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new
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';
@Injectable()
export class ManagementControllerServicesEffects {
@ -56,7 +58,7 @@ export class ManagementControllerServicesEffects {
private router: Router
) {}
loadControllerConfig$ = createEffect(() =>
loadManagementControllerServices$ = createEffect(() =>
this.actions$.pipe(
ofType(ManagementControllerServicesActions.loadManagementControllerServices),
switchMap(() =>
@ -412,6 +414,68 @@ export class ManagementControllerServicesEffects {
{ dispatch: false }
);
openEnableControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ManagementControllerServicesActions.openEnableControllerServiceDialog),
map((action) => action.request),
tap((request) => {
const serviceId: string = request.id;
const enableDialogReference = this.dialog.open(EnableControllerService, {
data: request,
id: serviceId,
panelClass: 'large-dialog'
});
enableDialogReference.componentInstance.goToReferencingComponent = (
component: ControllerServiceReferencingComponent
) => {
const route: string[] = this.getRouteForReference(component);
this.router.navigate(route);
};
enableDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(ManagementControllerServicesActions.loadManagementControllerServices());
}
});
})
),
{ dispatch: false }
);
openDisableControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ManagementControllerServicesActions.openDisableControllerServiceDialog),
map((action) => action.request),
tap((request) => {
const serviceId: string = request.id;
const enableDialogReference = this.dialog.open(DisableControllerService, {
data: request,
id: serviceId,
panelClass: 'large-dialog'
});
enableDialogReference.componentInstance.goToReferencingComponent = (
component: ControllerServiceReferencingComponent
) => {
const route: string[] = this.getRouteForReference(component);
this.router.navigate(route);
};
enableDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(ManagementControllerServicesActions.loadManagementControllerServices());
}
});
})
),
{ dispatch: false }
);
promptControllerServiceDeletion$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -27,7 +27,8 @@ import {
inlineCreateControllerServiceSuccess,
loadManagementControllerServices,
loadManagementControllerServicesSuccess,
managementControllerServicesApiError
managementControllerServicesApiError,
resetManagementControllerServicesState
} from './management-controller-services.actions';
import { produce } from 'immer';
@ -41,6 +42,9 @@ export const initialState: ManagementControllerServicesState = {
export const managementControllerServicesReducer = createReducer(
initialState,
on(resetManagementControllerServicesState, (state) => ({
...initialState
})),
on(loadManagementControllerServices, (state) => ({
...state,
status: 'loading' as const

View File

@ -28,6 +28,8 @@ import {
SelectRegistryClientRequest
} from './index';
export const resetRegistryClientsState = createAction('[Registry Clients] Reset Registry Clients State');
export const loadRegistryClients = createAction('[Registry Clients] Load Registry Clients');
export const loadRegistryClientsSuccess = createAction(

View File

@ -27,7 +27,8 @@ import {
deleteRegistryClientSuccess,
loadRegistryClients,
loadRegistryClientsSuccess,
registryClientsApiError
registryClientsApiError,
resetRegistryClientsState
} from './registry-clients.actions';
export const initialState: RegistryClientsState = {
@ -40,6 +41,9 @@ export const initialState: RegistryClientsState = {
export const registryClientsReducer = createReducer(
initialState,
on(resetRegistryClientsState, (state) => ({
...initialState
})),
on(loadRegistryClients, (state) => ({
...state,
status: 'loading' as const

View File

@ -29,6 +29,8 @@ import {
StopReportingTaskSuccess
} from './index';
export const resetReportingTasksState = createAction('[Reporting Tasks] Reset Reporting Tasks State');
export const loadReportingTasks = createAction('[Reporting Tasks] Load Reporting Tasks');
export const loadReportingTasksSuccess = createAction(

View File

@ -25,6 +25,7 @@ import {
loadReportingTasks,
loadReportingTasksSuccess,
reportingTasksApiError,
resetReportingTasksState,
startReportingTaskSuccess,
stopReportingTaskSuccess
} from './reporting-tasks.actions';
@ -40,6 +41,9 @@ export const initialState: ReportingTasksState = {
export const reportingTasksReducer = createReducer(
initialState,
on(resetReportingTasksState, (state) => ({
...initialState
})),
on(loadReportingTasks, (state) => ({
...state,
status: 'loading' as const

View File

@ -15,10 +15,10 @@
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { GeneralState } from '../../state/general';
import { Store } from '@ngrx/store';
import { loadControllerConfig } from '../../state/general/general.actions';
import { loadControllerConfig, resetGeneralState } from '../../state/general/general.actions';
import { selectGeneral } from '../../state/general/general.selectors';
@Component({
@ -26,7 +26,7 @@ import { selectGeneral } from '../../state/general/general.selectors';
templateUrl: './general.component.html',
styleUrls: ['./general.component.scss']
})
export class General implements OnInit {
export class General implements OnInit, OnDestroy {
general$ = this.store.select(selectGeneral);
constructor(private store: Store<GeneralState>) {}
@ -34,4 +34,8 @@ export class General implements OnInit {
ngOnInit(): void {
this.store.dispatch(loadControllerConfig());
}
ngOnDestroy(): void {
this.store.dispatch(resetGeneralState());
}
}

View File

@ -34,6 +34,8 @@
[definedByCurrentGroup]="definedByCurrentGroup"
(selectControllerService)="selectControllerService($event)"
(configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
</div>
<div class="flex justify-between">

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { ManagementControllerServicesState } from '../../state/management-controller-services';
import {
@ -28,8 +28,11 @@ import {
loadManagementControllerServices,
navigateToEditService,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
openEnableControllerServiceDialog,
openNewControllerServiceDialog,
promptControllerServiceDeletion,
resetManagementControllerServicesState,
selectControllerService
} from '../../state/management-controller-services/management-controller-services.actions';
import { ControllerServiceEntity } from '../../../../state/shared';
@ -38,13 +41,15 @@ import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { selectUser } from '../../../../state/user/user.selectors';
import { NiFiState } from '../../../../state';
import { state } from '@angular/animations';
import { resetEnableControllerServiceState } from '../../../../state/contoller-service-state/controller-service-state.actions';
@Component({
selector: 'management-controller-services',
templateUrl: './management-controller-services.component.html',
styleUrls: ['./management-controller-services.component.scss']
})
export class ManagementControllerServices implements OnInit {
export class ManagementControllerServices implements OnInit, OnDestroy {
serviceState$ = this.store.select(selectManagementControllerServicesState);
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
currentUser$ = this.store.select(selectUser);
@ -109,6 +114,28 @@ export class ManagementControllerServices implements OnInit {
);
}
enableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openEnableControllerServiceDialog({
request: {
id: entity.id,
controllerService: entity
}
})
);
}
disableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openDisableControllerServiceDialog({
request: {
id: entity.id,
controllerService: entity
}
})
);
}
deleteControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
promptControllerServiceDeletion({
@ -128,4 +155,8 @@ export class ManagementControllerServices implements OnInit {
})
);
}
ngOnDestroy(): void {
this.store.dispatch(resetManagementControllerServicesState());
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import {
selectRegistryClient,
@ -29,6 +29,7 @@ import {
openConfigureRegistryClientDialog,
openNewRegistryClientDialog,
promptRegistryClientDeletion,
resetRegistryClientsState,
selectClient
} from '../../state/registry-clients/registry-clients.actions';
import { RegistryClientEntity, RegistryClientsState } from '../../state/registry-clients';
@ -37,13 +38,14 @@ import { selectUser } from '../../../../state/user/user.selectors';
import { NiFiState } from '../../../../state';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { state } from '@angular/animations';
@Component({
selector: 'registry-clients',
templateUrl: './registry-clients.component.html',
styleUrls: ['./registry-clients.component.scss']
})
export class RegistryClients implements OnInit {
export class RegistryClients implements OnInit, OnDestroy {
registryClientsState$ = this.store.select(selectRegistryClientsState);
selectedRegistryClientId$ = this.store.select(selectRegistryClientIdFromRoute);
currentUser$ = this.store.select(selectUser);
@ -118,4 +120,8 @@ export class RegistryClients implements OnInit {
})
);
}
ngOnDestroy(): void {
this.store.dispatch(resetRegistryClientsState());
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { ReportingTaskEntity, ReportingTasksState } from '../../state/reporting-tasks';
import {
@ -26,6 +26,7 @@ import {
loadReportingTasks,
openNewReportingTaskDialog,
promptReportingTaskDeletion,
resetReportingTasksState,
selectReportingTask,
startReportingTask,
stopReportingTask
@ -33,13 +34,14 @@ import {
import { initialState } from '../../state/reporting-tasks/reporting-tasks.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import { NiFiState } from '../../../../state';
import { state } from '@angular/animations';
@Component({
selector: 'reporting-tasks',
templateUrl: './reporting-tasks.component.html',
styleUrls: ['./reporting-tasks.component.scss']
})
export class ReportingTasks implements OnInit {
export class ReportingTasks implements OnInit, OnDestroy {
reportingTaskState$ = this.store.select(selectReportingTasksState);
selectedReportingTaskId$ = this.store.select(selectReportingTaskIdFromRoute);
currentUser$ = this.store.select(selectUser);
@ -102,4 +104,8 @@ export class ReportingTasks implements OnInit {
})
);
}
ngOnDestroy(): void {
this.store.dispatch(resetReportingTasksState());
}
}

View File

@ -0,0 +1,144 @@
/*
* 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 {
ControllerServiceEntity,
ControllerServiceReferencingComponent,
ControllerServiceReferencingComponentEntity
} from '../state/shared';
import { Client } from './client.service';
import { NiFiCommon } from './nifi-common.service';
@Injectable({ providedIn: 'root' })
export class ControllerServiceStateService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private nifiCommon: NiFiCommon,
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`, {
revision: this.client.getRevision(controllerService),
state: enabled ? 'ENABLED' : 'DISABLED',
uiOnly: true
});
}
updateReferencingServices(controllerService: ControllerServiceEntity, enabled: boolean): Observable<any> {
const referencingComponentRevisions: { [key: string]: any } = {};
this.getReferencingComponentRevisions(
controllerService.component.referencingComponents,
referencingComponentRevisions,
true
);
return this.httpClient.put(`${this.stripProtocol(controllerService.uri)}/references`, {
id: controllerService.id,
state: enabled ? 'ENABLED' : 'DISABLED',
referencingComponentRevisions: referencingComponentRevisions,
// 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
uiOnly: true
});
}
updateReferencingSchedulableComponents(
controllerService: ControllerServiceEntity,
running: boolean
): Observable<any> {
const referencingComponentRevisions: { [key: string]: any } = {};
this.getReferencingComponentRevisions(
controllerService.component.referencingComponents,
referencingComponentRevisions,
false
);
return this.httpClient.put(`${this.stripProtocol(controllerService.uri)}/references`, {
id: controllerService.id,
state: running ? 'RUNNING' : 'STOPPED',
referencingComponentRevisions: referencingComponentRevisions,
// 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(),
uiOnly: true
});
}
/**
* Gathers all referencing component revisions.
*
* @param referencingComponents
* @param referencingComponentRevisions
* @param serviceOnly - true includes only services, false includes only schedulable components
*/
private getReferencingComponentRevisions(
referencingComponents: ControllerServiceReferencingComponentEntity[],
referencingComponentRevisions: { [key: string]: any },
serviceOnly: boolean
): void {
if (this.nifiCommon.isEmpty(referencingComponents)) {
return;
}
// include the revision of each referencing component
referencingComponents.forEach((referencingComponentEntity) => {
const referencingComponent: ControllerServiceReferencingComponent = referencingComponentEntity.component;
if (serviceOnly) {
if (referencingComponent.referenceType === 'ControllerService') {
referencingComponentRevisions[referencingComponent.id] =
this.client.getRevision(referencingComponentEntity);
}
} else {
if (
referencingComponent.referenceType === 'Processor' ||
referencingComponent.referenceType === 'ReportingTask'
) {
referencingComponentRevisions[referencingComponent.id] =
this.client.getRevision(referencingComponentEntity);
}
}
// recurse
this.getReferencingComponentRevisions(
referencingComponent.referencingComponents,
referencingComponentRevisions,
serviceOnly
);
});
}
}

View File

@ -0,0 +1,115 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createAction, props } from '@ngrx/store';
import {
SetEnableControllerServiceRequest,
ReferencingComponentsStepResponse,
ControllerServiceStepResponse,
SetEnableStepFailure,
SetControllerServiceRequest,
SetEnableStep
} from './index';
export const resetEnableControllerServiceState = createAction(
'[Enable Controller Service] Reset Enable Controller Service State'
);
export const setControllerService = createAction(
'[Enable Controller Service] Set Controller Service',
props<{
request: SetControllerServiceRequest;
}>()
);
export const submitEnableRequest = createAction(
'[Enable Controller Service] Submit Enable Request',
props<{
request: SetEnableControllerServiceRequest;
}>()
);
export const submitDisableRequest = createAction('[Enable Controller Service] Submit Disable Request');
export const setEnableControllerService = createAction('[Enable Controller Service] Set Enable Controller Service');
export const setEnableControllerServiceSuccess = createAction(
'[Enable Controller Service] Set Enable Controller Service Success',
props<{
response: ControllerServiceStepResponse;
}>()
);
export const updateReferencingServices = createAction('[Enable Controller Service] Update Referencing Services');
export const updateReferencingServicesSuccess = createAction(
'[Enable Controller Service] Update Referencing Services Success',
props<{
response: ReferencingComponentsStepResponse;
}>()
);
export const updateReferencingComponents = createAction('[Enable Controller Service] Update Referencing Components');
export const updateReferencingComponentsSuccess = createAction(
'[Enable Controller Service] Update Referencing Components Success',
props<{
response: ReferencingComponentsStepResponse;
}>()
);
export const startPollingControllerService = createAction(
'[Enable Controller Service] Start Polling Controller Service'
);
export const stopPollingControllerService = createAction('[Enable Controller Service] Stop Polling Controller Service');
export const pollControllerService = createAction('[Enable Controller Service] Poll Controller Service');
export const pollControllerServiceSuccess = createAction(
'[Enable Controller Service] Poll Controller Service Success',
props<{
response: ControllerServiceStepResponse;
previousStep: SetEnableStep;
}>()
);
export const setEnableStepFailure = createAction(
'[Enable Controller Service] Set Enable Step Failure',
props<{
response: SetEnableStepFailure;
}>()
);
export const controllerServiceApiError = createAction(
'[Enable Controller Service] Controller Service Api Error',
props<{
error: string;
}>()
);
export const clearControllerServiceApiError = createAction(
'[Enable Controller Service] Clear Controller Service Api Error'
);
export const showOkDialog = createAction(
'[Enable Controller Service] Show Ok Dialog',
props<{
title: string;
message: string;
}>()
);

View File

@ -0,0 +1,500 @@
/*
* 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 ControllerServiceActions from './controller-service-state.actions';
import {
asyncScheduler,
catchError,
filter,
from,
interval,
map,
of,
switchMap,
takeUntil,
tap,
withLatestFrom
} from 'rxjs';
import { Store } from '@ngrx/store';
import { NiFiState } from '../index';
import { selectControllerService, selectControllerServiceSetEnableRequest } from './controller-service-state.selectors';
import { OkDialog } from '../../ui/common/ok-dialog/ok-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { ControllerServiceStateService } from '../../service/controller-service-state.service';
import { ControllerServiceEntity, ControllerServiceReferencingComponentEntity } from '../shared';
import { SetEnableRequest, SetEnableStep } from './index';
@Injectable()
export class ControllerServiceStateEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private dialog: MatDialog,
private controllerServiceStateService: ControllerServiceStateService
) {}
submitEnableRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.submitEnableRequest),
map((action) => action.request),
withLatestFrom(this.store.select(selectControllerService)),
filter(([request, controllerService]) => !!controllerService),
switchMap(([request, controllerService]) => {
if (
request.scope === 'SERVICE_AND_REFERENCING_COMPONENTS' &&
// @ts-ignore
this.hasUnauthorizedReferences(controllerService.component.referencingComponents)
) {
return of(
ControllerServiceActions.setEnableStepFailure({
response: {
step: SetEnableStep.EnableService,
error: 'Unable to enable due to unauthorized referencing components.'
}
})
);
} else {
return of(ControllerServiceActions.setEnableControllerService());
}
})
)
);
submitDisableRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.submitDisableRequest),
withLatestFrom(this.store.select(selectControllerService)),
filter(([request, controllerService]) => !!controllerService),
switchMap(([request, controllerService]) => {
// @ts-ignore
if (this.hasUnauthorizedReferences(controllerService.component.referencingComponents)) {
return of(
ControllerServiceActions.setEnableStepFailure({
response: {
step: SetEnableStep.StopReferencingComponents,
error: 'Unable to disable due to unauthorized referencing components.'
}
})
);
} else {
return of(ControllerServiceActions.updateReferencingComponents());
}
})
)
);
setEnableControllerService$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.setEnableControllerService),
withLatestFrom(
this.store.select(selectControllerService),
this.store.select(selectControllerServiceSetEnableRequest)
),
switchMap(([request, controllerService, setEnableRequest]) => {
if (controllerService) {
return from(
this.controllerServiceStateService.setEnable(controllerService, setEnableRequest.enable)
).pipe(
map((response) =>
ControllerServiceActions.setEnableControllerServiceSuccess({
response: {
controllerService: response,
currentStep: setEnableRequest.currentStep
}
})
),
catchError((error) =>
of(
ControllerServiceActions.setEnableStepFailure({
response: {
step: setEnableRequest.currentStep,
error: error.error
}
})
)
)
);
} else {
return of(
ControllerServiceActions.showOkDialog({
title: 'Enable Service',
message: 'Controller Service not initialized'
})
);
}
})
)
);
setEnableControllerServiceSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.setEnableControllerServiceSuccess),
map((action) => action.response),
// if the current step is DisableService, it's the end of a disable request and there is no need to start polling
filter((response) => response.currentStep !== SetEnableStep.DisableService),
switchMap((response) => of(ControllerServiceActions.startPollingControllerService()))
)
);
startPollingControllerService$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.startPollingControllerService),
switchMap(() =>
interval(2000, asyncScheduler).pipe(
takeUntil(this.actions$.pipe(ofType(ControllerServiceActions.stopPollingControllerService)))
)
),
switchMap(() => of(ControllerServiceActions.pollControllerService()))
)
);
pollControllerService$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.pollControllerService),
withLatestFrom(
this.store.select(selectControllerService),
this.store.select(selectControllerServiceSetEnableRequest)
),
filter(([action, controllerService, setEnableRequest]) => !!controllerService),
switchMap(([action, controllerService, setEnableRequest]) => {
// @ts-ignore
const cs: ControllerServiceEntity = controllerService;
return from(this.controllerServiceStateService.getControllerService(cs.id)).pipe(
map((response) =>
ControllerServiceActions.pollControllerServiceSuccess({
response: {
controllerService: response,
currentStep: this.getNextStep(setEnableRequest, cs)
},
previousStep: setEnableRequest.currentStep
})
),
catchError((error) =>
of(
ControllerServiceActions.setEnableStepFailure({
response: {
step: setEnableRequest.currentStep,
error: error.error
}
})
)
)
);
})
)
);
setEnableStepComplete$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.pollControllerServiceSuccess),
filter((action) => {
const response = action.response;
const previousStep = action.previousStep;
// if the state hasn't transitioned we don't want trigger the next action
return response.currentStep !== previousStep;
}),
switchMap((action) => {
const response = action.response;
// the request in the store will drive whether the action will enable or disable
switch (response.currentStep) {
case SetEnableStep.DisableReferencingServices:
case SetEnableStep.EnableReferencingServices:
return of(ControllerServiceActions.updateReferencingServices());
case SetEnableStep.DisableService:
case SetEnableStep.EnableService:
return of(ControllerServiceActions.setEnableControllerService());
case SetEnableStep.StopReferencingComponents:
case SetEnableStep.StartReferencingComponents:
return of(ControllerServiceActions.updateReferencingComponents());
case SetEnableStep.Completed:
default:
// if the sequence is complete or if it's an unexpected step stop polling
return of(ControllerServiceActions.stopPollingControllerService());
}
})
)
);
updateReferencingServices$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.updateReferencingServices),
withLatestFrom(
this.store.select(selectControllerService),
this.store.select(selectControllerServiceSetEnableRequest)
),
switchMap(([action, controllerService, setEnableRequest]) => {
if (controllerService) {
return from(
this.controllerServiceStateService.updateReferencingServices(
controllerService,
setEnableRequest.enable
)
).pipe(
map((response) =>
ControllerServiceActions.updateReferencingServicesSuccess({
response: {
referencingComponents: response.controllerServiceReferencingComponents,
currentStep: setEnableRequest.currentStep
}
})
),
catchError((error) =>
of(
ControllerServiceActions.setEnableStepFailure({
response: {
step: setEnableRequest.currentStep,
error: error.error
}
})
)
)
);
} else {
return of(
ControllerServiceActions.showOkDialog({
title: 'Enable Service',
message: 'Controller Service not initialized'
})
);
}
})
)
);
updateReferencingComponents$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.updateReferencingComponents),
withLatestFrom(
this.store.select(selectControllerService),
this.store.select(selectControllerServiceSetEnableRequest)
),
switchMap(([action, controllerService, setEnableRequest]) => {
if (controllerService) {
return from(
this.controllerServiceStateService.updateReferencingSchedulableComponents(
controllerService,
setEnableRequest.enable
)
).pipe(
map((response) =>
ControllerServiceActions.updateReferencingComponentsSuccess({
response: {
referencingComponents: response.controllerServiceReferencingComponents,
currentStep: setEnableRequest.currentStep
}
})
),
catchError((error) =>
of(
ControllerServiceActions.setEnableStepFailure({
response: {
step: setEnableRequest.currentStep,
error: error.error
}
})
)
)
);
} else {
return of(
ControllerServiceActions.showOkDialog({
title: 'Enable Service',
message: 'Controller Service not initialized'
})
);
}
})
)
);
updateReferencingComponentsSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.updateReferencingComponentsSuccess),
map((action) => action.response),
// if the current step is StartReferencingComponents, it's the end of an enable request and there is no need to start polling
filter((response) => response.currentStep !== SetEnableStep.StartReferencingComponents),
switchMap((response) => of(ControllerServiceActions.startPollingControllerService()))
)
);
setEnableStepFailure$ = createEffect(() =>
this.actions$.pipe(
ofType(ControllerServiceActions.setEnableStepFailure),
switchMap(() => of(ControllerServiceActions.stopPollingControllerService()))
)
);
showOkDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServiceActions.showOkDialog),
tap((request) => {
this.dialog.open(OkDialog, {
data: {
title: request.title,
message: request.message
},
panelClass: 'medium-dialog'
});
})
),
{ dispatch: false }
);
hasUnauthorizedReferences(referencingComponents: ControllerServiceReferencingComponentEntity[]): boolean {
// if there are no referencing components there is nothing unauthorized
if (referencingComponents.length === 0) {
return false;
}
// identify any unauthorized referencing components
const unauthorized: boolean = referencingComponents.some((referencingComponentEntity) => {
return !referencingComponentEntity.permissions.canRead || !referencingComponentEntity.permissions.canWrite;
});
// if any are unauthorized there is no need to check further
if (unauthorized) {
return true;
}
// consider the components that are referencing the referencingServices
const referencingServices = referencingComponents.filter((referencingComponent) => {
return referencingComponent.component.referenceType === 'ControllerService';
});
if (referencingServices.length === 0) {
// if there are no more nested services all references are authorized
return false;
} else {
// if there are nested services, check if they have any referencing components are unauthorized
return referencingServices.some((referencingService) => {
return this.hasUnauthorizedReferences(referencingService.component.referencingComponents);
});
}
}
getNextStep(request: SetEnableRequest, controllerServiceEntity: ControllerServiceEntity): SetEnableStep {
switch (request.currentStep) {
case SetEnableStep.EnableService:
return this.isServiceActionComplete(controllerServiceEntity, ['ENABLED', 'ENABLING'])
? request.scope === 'SERVICE_ONLY'
? SetEnableStep.Completed
: SetEnableStep.EnableReferencingServices
: request.currentStep;
case SetEnableStep.EnableReferencingServices:
return this.isReferencingServicesActionComplete(
controllerServiceEntity.component.referencingComponents,
['ENABLED', 'ENABLING']
)
? SetEnableStep.StartReferencingComponents
: request.currentStep;
case SetEnableStep.StartReferencingComponents:
// since we are starting components, there is no condition to wait for
return SetEnableStep.Completed;
case SetEnableStep.StopReferencingComponents:
return this.areReferencingComponentsStopped(controllerServiceEntity.component.referencingComponents)
? SetEnableStep.DisableReferencingServices
: request.currentStep;
case SetEnableStep.DisableReferencingServices:
return this.isReferencingServicesActionComplete(
controllerServiceEntity.component.referencingComponents,
['DISABLED']
)
? SetEnableStep.DisableService
: request.currentStep;
case SetEnableStep.DisableService:
return this.isServiceActionComplete(controllerServiceEntity, ['DISABLED'])
? SetEnableStep.Completed
: request.currentStep;
default:
return request.currentStep;
}
}
isServiceActionComplete(controllerServiceEntity: ControllerServiceEntity, acceptedRunStatus: string[]): boolean {
return acceptedRunStatus.includes(controllerServiceEntity.status.runStatus);
}
isReferencingServicesActionComplete(
referencingComponents: ControllerServiceReferencingComponentEntity[],
acceptedRunStatus: string[]
): boolean {
const referencingServices = referencingComponents.filter((referencingComponent) => {
return referencingComponent.component.referenceType === 'ControllerService';
});
if (referencingServices.length === 0) {
return true;
}
return referencingServices.some((referencingService) => {
const isEnabled: boolean = acceptedRunStatus.includes(referencingService.component.state);
if (isEnabled) {
// if this service isn't enabled, there is no need to check further...
return this.isReferencingServicesActionComplete(
referencingService.component.referencingComponents,
acceptedRunStatus
);
}
return isEnabled;
});
}
areReferencingComponentsStopped(referencingComponents: ControllerServiceReferencingComponentEntity[]): boolean {
// consider the schedulable components in the referencingComponents
const referencingScheduleableComponents = referencingComponents.filter((referencingComponent) => {
return (
referencingComponent.component.referenceType === 'Processor' ||
referencingComponent.component.referenceType === 'ReportingTask'
);
});
const stillRunning: boolean = referencingScheduleableComponents.some((referencingComponentEntity) => {
const referencingComponent = referencingComponentEntity.component;
return (
referencingComponent.state === 'RUNNING' ||
(referencingComponent.activeThreadCount && referencingComponent.activeThreadCount > 0)
);
});
// if any are still running, there is no need to check further...
if (stillRunning) {
return false;
}
// consider the scheduleable components that are referencing the referencingServices
const referencingServices = referencingComponents.filter((referencingComponent) => {
return referencingComponent.component.referenceType === 'ControllerService';
});
if (referencingServices.length === 0) {
// if there are no more nested services all schedulable components have stopped
return true;
} else {
// if there are nested services, check if they have any referencing components that are still running
return referencingServices.some((referencingService) => {
return this.areReferencingComponentsStopped(referencingService.component.referencingComponents);
});
}
}
}

View File

@ -0,0 +1,105 @@
/*
* 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 { ControllerServiceState, SetEnableStep } from './index';
import {
clearControllerServiceApiError,
controllerServiceApiError,
setEnableStepFailure,
pollControllerServiceSuccess,
resetEnableControllerServiceState,
setControllerService,
setEnableControllerServiceSuccess,
submitDisableRequest,
submitEnableRequest,
updateReferencingComponentsSuccess,
updateReferencingServicesSuccess
} from './controller-service-state.actions';
import { produce } from 'immer';
export const initialState: ControllerServiceState = {
setEnableRequest: {
enable: true,
currentStep: SetEnableStep.Pending,
scope: 'SERVICE_ONLY'
},
controllerService: null,
error: null,
status: 'pending'
};
export const controllerServiceStateReducer = createReducer(
initialState,
on(resetEnableControllerServiceState, (state) => ({
...initialState
})),
on(setControllerService, (state, { request }) => ({
...state,
controllerService: request.controllerService
})),
on(submitEnableRequest, (state, { request }) => ({
...state,
setEnableRequest: {
enable: true,
currentStep: SetEnableStep.EnableService,
scope: request.scope
}
})),
on(submitDisableRequest, (state) => ({
...state,
setEnableRequest: {
enable: false,
currentStep: SetEnableStep.StopReferencingComponents,
scope: 'SERVICE_AND_REFERENCING_COMPONENTS' as const
}
})),
on(setEnableControllerServiceSuccess, pollControllerServiceSuccess, (state, { response }) => ({
...state,
controllerService: response.controllerService,
setEnableRequest: {
...state.setEnableRequest,
currentStep: response.currentStep
}
})),
on(updateReferencingServicesSuccess, updateReferencingComponentsSuccess, (state, { response }) => {
return produce(state, (draftState) => {
if (draftState.controllerService) {
draftState.controllerService.component.referencingComponents = response.referencingComponents;
}
draftState.setEnableRequest.currentStep = response.currentStep;
});
}),
on(setEnableStepFailure, (state, { response }) => ({
...state,
setEnableRequest: {
...state.setEnableRequest,
error: response
},
status: 'error' as const
})),
on(controllerServiceApiError, (state, { error }) => ({
...state,
error: error,
status: 'error' as const
})),
on(clearControllerServiceApiError, (state) => ({
...state,
error: null,
status: 'pending' as const
}))
);

View File

@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { controllerServiceStateFeatureKey, ControllerServiceState } from './index';
import { ControllerServiceEntity } from '../shared';
export const selectEnableControllerServiceState = createFeatureSelector<ControllerServiceState>(
controllerServiceStateFeatureKey
);
export const selectControllerService = createSelector(
selectEnableControllerServiceState,
(state: ControllerServiceState) => state.controllerService
);
export const selectControllerServiceSetEnableRequest = createSelector(
selectEnableControllerServiceState,
(state: ControllerServiceState) => state.setEnableRequest
);

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 { ControllerServiceEntity, ControllerServiceReferencingComponentEntity, SelectOption } from '../shared';
export const controllerServiceStateFeatureKey = 'enableControllerService';
export const controllerServiceActionScopes: SelectOption[] = [
{
text: 'Service only',
value: 'SERVICE_ONLY',
description: 'Enable only this controller service.'
},
{
text: 'Service and referencing components',
value: 'SERVICE_AND_REFERENCING_COMPONENTS',
description: 'Enable this controller service and enable/start all referencing components.'
}
];
export interface SetControllerServiceRequest {
controllerService: ControllerServiceEntity;
}
export interface SetEnableControllerServiceRequest {
scope: 'SERVICE_ONLY' | 'SERVICE_AND_REFERENCING_COMPONENTS';
}
export interface ControllerServiceStepResponse {
controllerService: ControllerServiceEntity;
currentStep: SetEnableStep;
}
export interface ReferencingComponentsStepResponse {
referencingComponents: ControllerServiceReferencingComponentEntity[];
currentStep: SetEnableStep;
}
export interface SetEnableStepFailure {
step: SetEnableStep;
error: string;
}
export enum SetEnableStep {
Pending,
EnableService,
EnableReferencingServices,
StartReferencingComponents,
StopReferencingComponents,
DisableReferencingServices,
DisableService,
Completed
}
export interface SetEnableRequest {
enable: boolean;
currentStep: SetEnableStep;
scope: 'SERVICE_ONLY' | 'SERVICE_AND_REFERENCING_COMPONENTS';
error?: SetEnableStepFailure;
}
export interface ControllerServiceState {
setEnableRequest: SetEnableRequest;
controllerService: ControllerServiceEntity | null;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -25,6 +25,8 @@ import { aboutFeatureKey, AboutState } from './about';
import { aboutReducer } from './about/about.reducer';
import { statusHistoryFeatureKey, StatusHistoryState } from './status-history';
import { statusHistoryReducer } from './status-history/status-history.reducer';
import { controllerServiceStateFeatureKey, ControllerServiceState } from './contoller-service-state';
import { controllerServiceStateReducer } from './contoller-service-state/controller-service-state.reducer';
export interface NiFiState {
router: RouterReducerState;
@ -32,6 +34,7 @@ export interface NiFiState {
[extensionTypesFeatureKey]: ExtensionTypesState;
[aboutFeatureKey]: AboutState;
[statusHistoryFeatureKey]: StatusHistoryState;
[controllerServiceStateFeatureKey]: ControllerServiceState;
}
export const rootReducers: ActionReducerMap<NiFiState> = {
@ -39,5 +42,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[userFeatureKey]: userReducer,
[extensionTypesFeatureKey]: extensionTypesReducer,
[aboutFeatureKey]: aboutReducer,
[statusHistoryFeatureKey]: statusHistoryReducer
[statusHistoryFeatureKey]: statusHistoryReducer,
[controllerServiceStateFeatureKey]: controllerServiceStateReducer
};

View File

@ -63,6 +63,16 @@ export interface UpdateControllerServiceRequest {
postUpdateNavigation?: string[];
}
export interface SetEnableControllerServiceDialogRequest {
id: string;
controllerService: ControllerServiceEntity;
}
export interface DisableControllerServiceDialogRequest {
id: string;
controllerService: ControllerServiceEntity;
}
export interface ProvenanceEventSummary {
id: string;
eventId: number;

View File

@ -98,7 +98,7 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header>State</th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-2">
<div [ngClass]="getStateIcon(item)"></div>
<div class="flex justify-center" [ngClass]="getStateIcon(item)"></div>
<div>{{ formatState(item) }}</div>
</div>
</td>
@ -127,8 +127,13 @@
<div
class="pointer fa icon icon-enable-false"
*ngIf="canDisable(item)"
(click)="disableClicked(item, $event)"
title="Disable"></div>
<div class="pointer fa fa-flash" *ngIf="canEnable(item)" title="Enable"></div>
<div
class="pointer fa fa-flash"
*ngIf="canEnable(item)"
(click)="enabledClicked(item, $event)"
title="Enable"></div>
<div
class="pointer fa fa-exchange"
*ngIf="canChangeVersion(item)"

View File

@ -79,6 +79,10 @@ export class ControllerServiceTable implements AfterViewInit {
new EventEmitter<ControllerServiceEntity>();
@Output() configureControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() enableControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() disableControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
protected readonly TextTip = TextTip;
protected readonly BulletinsTip = BulletinsTip;
@ -213,11 +217,21 @@ export class ControllerServiceTable implements AfterViewInit {
}
canEnable(entity: ControllerServiceEntity): boolean {
return this.canOperate(entity) && this.isDisabled(entity) && entity.status.validationStatus === 'VALID';
const userAuthorized: boolean = this.canRead(entity) && this.canOperate(entity);
return userAuthorized && this.isDisabled(entity) && entity.status.validationStatus === 'VALID';
}
enabledClicked(entity: ControllerServiceEntity, event: MouseEvent): void {
this.enableControllerService.next(entity);
}
canDisable(entity: ControllerServiceEntity): boolean {
return this.canOperate(entity) && this.isEnabledOrEnabling(entity);
const userAuthorized: boolean = this.canRead(entity) && this.canOperate(entity);
return userAuthorized && this.isEnabledOrEnabling(entity);
}
disableClicked(entity: ControllerServiceEntity, event: MouseEvent): void {
this.disableControllerService.next(entity);
}
canChangeVersion(entity: ControllerServiceEntity): boolean {

View File

@ -0,0 +1,142 @@
<!--
~ 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>Disable Controller Service</h2>
<ng-container *ngIf="(controllerService$ | async)! as controllerService">
<div class="controller-service-disable-form" *ngIf="(disableRequest$ | async)! as disableRequest">
<ng-container *ngIf="disableRequest.currentStep === SetEnableStep.Pending; else disableInProgress">
<mat-dialog-content>
<div class="tab-content py-4 flex gap-x-4">
<div class="w-96 max-w-full flex flex-col gap-y-4">
<div class="flex flex-col">
<div>Service</div>
<div class="value">{{ controllerService.component.name }}</div>
</div>
<div class="flex flex-col">
<div>Scope</div>
<div class="value">Service and referencing components</div>
</div>
</div>
<div class="w-96 max-w-full flex flex-col">
<div>Referencing Components</div>
<div>
<controller-service-references
[serviceReferences]="controllerService.component.referencingComponents"
[goToReferencingComponent]="goToReferencingComponent"></controller-service-references>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
<button type="button" color="primary" (click)="submitForm()" mat-raised-button>Disable</button>
</mat-dialog-actions>
</ng-container>
<ng-template #disableInProgress>
<mat-dialog-content>
<div class="tab-content py-4 flex gap-x-4">
<div class="w-96 max-w-full flex flex-col gap-y-4">
<div class="flex flex-col">
<div>Service</div>
<div class="value">{{ controllerService.component.name }}</div>
</div>
<div class="flex flex-col">
<div>Steps To Disable {{ controllerService.component.name }}</div>
<div class="flex flex-col gap-y-1.5">
<div class="flex justify-between items-center">
<div class="value">Stopping referencing processors and reporting tasks</div>
<ng-container
*ngTemplateOutlet="
getTemplateForStep(SetEnableStep.StopReferencingComponents, disableRequest)
"></ng-container>
</div>
<div
*ngIf="
disableRequest.error &&
disableRequest.error.step == SetEnableStep.StopReferencingComponents
"
class="text-xs ml-2">
{{ disableRequest.error.error }}
</div>
<div class="flex justify-between items-center">
<div class="value">Disabling referencing controller services</div>
<ng-container
*ngTemplateOutlet="
getTemplateForStep(SetEnableStep.DisableReferencingServices, disableRequest)
"></ng-container>
</div>
<div
*ngIf="
disableRequest.error &&
disableRequest.error.step == SetEnableStep.DisableReferencingServices
"
class="text-xs ml-2">
{{ disableRequest.error.error }}
</div>
<div class="flex justify-between items-center">
<div class="value">Disabling this controller service</div>
<ng-container
*ngTemplateOutlet="
getTemplateForStep(SetEnableStep.DisableService, disableRequest)
"></ng-container>
</div>
<div
*ngIf="
disableRequest.error &&
disableRequest.error.step == SetEnableStep.DisableService
"
class="text-xs ml-2">
{{ disableRequest.error.error }}
</div>
</div>
</div>
</div>
<div class="w-96 max-w-full flex flex-col">
<div>Referencing Components</div>
<div>
<controller-service-references
[serviceReferences]="controllerService.component.referencingComponents"
[goToReferencingComponent]="goToReferencingComponent"></controller-service-references>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<ng-container
*ngIf="
disableRequest.currentStep === SetEnableStep.Completed || disableRequest.error;
else updateInProgressActions
">
<button color="primary" mat-raised-button mat-dialog-close>Close</button>
</ng-container>
<ng-template #updateInProgressActions>
<button color="accent" (click)="cancelClicked()" mat-raised-button mat-dialog-close>Cancel</button>
</ng-template>
</mat-dialog-actions>
</ng-template>
</div>
</ng-container>
<ng-template #stepInProgress>
<div class="fa fa-spin fa-circle-o-notch"></div>
</ng-template>
<ng-template #stepComplete>
<div class="fa fa-check text-green-500"></div>
</ng-template>
<ng-template #stepError>
<div class="fa fa-times text-red-400"></div>
</ng-template>
<ng-template #stepNotStarted><div class="w-3.5"></div></ng-template>

View File

@ -0,0 +1,32 @@
/*
* 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;
.controller-service-disable-form {
@include mat.button-density(-1);
.mdc-dialog__content {
padding: 0 16px;
font-size: 14px;
.tab-content {
height: 475px;
overflow-y: auto;
}
}
}

View File

@ -0,0 +1,354 @@
/*
* 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 { DisableControllerService } from './disable-controller-service.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/contoller-service-state/controller-service-state.reducer';
import { SetEnableControllerServiceDialogRequest } from '../../../../state/shared';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
describe('EnableControllerService', () => {
let component: DisableControllerService;
let fixture: ComponentFixture<DisableControllerService>;
const data: SetEnableControllerServiceDialogRequest = {
id: '92db6ee4-018c-1000-1061-e3476c3f4e9f',
controllerService: {
revision: {
clientId: 'b37eba61-156a-47a4-875f-945cd153b876',
version: 7
},
id: '92db6ee4-018c-1000-1061-e3476c3f4e9f',
uri: 'https://localhost:4200/nifi-api/controller-services/92db6ee4-018c-1000-1061-e3476c3f4e9f',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [
{
id: 0,
groupId: 'asdf',
sourceId: 'asdf',
timestamp: '14:08:44 EST',
canRead: true,
bulletin: {
id: 0,
category: 'asdf',
groupId: 'asdf',
sourceId: 'asdf',
sourceName: 'asdf',
level: 'ERROR',
message: 'asdf',
timestamp: '14:08:44 EST'
}
}
],
component: {
id: '92db6ee4-018c-1000-1061-e3476c3f4e9f',
name: 'AvroReader',
type: 'org.apache.nifi.avro.AvroReader',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-record-serialization-services-nar',
version: '2.0.0-SNAPSHOT'
},
controllerServiceApis: [
{
type: 'org.apache.nifi.serialization.RecordReaderFactory',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
}
}
],
state: 'DISABLED',
persistsState: false,
restricted: false,
deprecated: false,
multipleVersionsAvailable: false,
supportsSensitiveDynamicProperties: false,
properties: {
'schema-access-strategy': 'embedded-avro-schema',
'schema-registry': null,
'schema-name': '${schema.name}',
'schema-version': null,
'schema-branch': null,
'schema-text': '${avro.schema}',
'schema-reference-reader': null,
'cache-size': '1000'
},
descriptors: {
'schema-access-strategy': {
name: 'schema-access-strategy',
displayName: 'Schema Access Strategy',
description: 'Specifies how to obtain the schema that is to be used for interpreting the data.',
defaultValue: 'embedded-avro-schema',
allowableValues: [
{
allowableValue: {
displayName: "Use 'Schema Name' Property",
value: 'schema-name',
description:
"The name of the Schema to use is specified by the 'Schema Name' Property. The value of this property is used to lookup the Schema in the configured Schema Registry service."
},
canRead: true
},
{
allowableValue: {
displayName: "Use 'Schema Text' Property",
value: 'schema-text-property',
description:
"The text of the Schema itself is specified by the 'Schema Text' Property. The value of this property must be a valid Avro Schema. If Expression Language is used, the value of the 'Schema Text' property must be valid after substituting the expressions."
},
canRead: true
},
{
allowableValue: {
displayName: 'Schema Reference Reader',
value: 'schema-reference-reader',
description:
'The schema reference information will be provided through a configured Schema Reference Reader service implementation.'
},
canRead: true
},
{
allowableValue: {
displayName: 'Use Embedded Avro Schema',
value: 'embedded-avro-schema',
description:
'The FlowFile has the Avro Schema embedded within the content, and this schema will be used.'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'schema-registry': {
name: 'schema-registry',
displayName: 'Schema Registry',
description: 'Specifies the Controller Service to use for the Schema Registry',
allowableValues: [],
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.schemaregistry.services.SchemaRegistry',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-reference-reader', 'schema-name']
}
]
},
'schema-name': {
name: 'schema-name',
displayName: 'Schema Name',
description: 'Specifies the name of the schema to lookup in the Schema Registry property',
defaultValue: '${schema.name}',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-name']
}
]
},
'schema-version': {
name: 'schema-version',
displayName: 'Schema Version',
description:
'Specifies the version of the schema to lookup in the Schema Registry. If not specified then the latest version of the schema will be retrieved.',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-name']
}
]
},
'schema-branch': {
name: 'schema-branch',
displayName: 'Schema Branch',
description:
'Specifies the name of the branch to use when looking up the schema in the Schema Registry property. If the chosen Schema Registry does not support branching, this value will be ignored.',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-name']
}
]
},
'schema-text': {
name: 'schema-text',
displayName: 'Schema Text',
description: 'The text of an Avro-formatted Schema',
defaultValue: '${avro.schema}',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-text-property']
}
]
},
'schema-reference-reader': {
name: 'schema-reference-reader',
displayName: 'Schema Reference Reader',
description:
'Service implementation responsible for reading FlowFile attributes or content to determine the Schema Reference Identifier',
allowableValues: [],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.schemaregistry.services.SchemaReferenceReader',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-reference-reader']
}
]
},
'cache-size': {
name: 'cache-size',
displayName: 'Cache Size',
description: 'Specifies how many Schemas should be cached',
defaultValue: '1000',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
}
},
referencingComponents: [
{
revision: {
clientId: 'b37eba61-156a-47a4-875f-945cd153b876',
version: 5,
lastModifier: 'admin'
},
id: '92db918c-018c-1000-ccd5-0796caa6d463',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [
{
id: 0,
groupId: 'asdf',
sourceId: 'asdf',
timestamp: '14:08:44 EST',
canRead: true,
bulletin: {
id: 0,
category: 'asdf',
groupId: 'asdf',
sourceId: 'asdf',
sourceName: 'asdf',
level: 'ERROR',
message: 'asdf',
timestamp: '14:08:44 EST'
}
}
],
component: {
id: '92db918c-018c-1000-ccd5-0796caa6d463',
name: 'ReaderLookup',
type: 'ReaderLookup',
state: 'DISABLED',
validationErrors: [
"'avro' validated against '92db6ee4-018c-1000-1061-e3476c3f4e9f' is invalid because Controller Service with ID 92db6ee4-018c-1000-1061-e3476c3f4e9f is disabled"
],
referenceType: 'ControllerService',
referenceCycle: false,
referencingComponents: []
},
operatePermissions: {
canRead: false,
canWrite: false
}
}
],
validationStatus: 'VALID',
bulletinLevel: 'WARN',
extensionMissing: false
},
operatePermissions: {
canRead: false,
canWrite: false
},
status: {
runStatus: 'DISABLED',
validationStatus: 'VALID'
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DisableControllerService, BrowserAnimationsModule],
providers: [provideMockStore({ initialState }), { provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(DisableControllerService);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,128 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Inject, Input, OnDestroy, TemplateRef, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import {
ControllerServiceReferencingComponent,
SetEnableControllerServiceDialogRequest
} from '../../../../state/shared';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { PropertyTable } from '../../property-table/property-table.component';
import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component';
import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component';
import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive';
import { TextTip } from '../../tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../../tooltips/nifi-tooltip.directive';
import { ControllerServiceState, SetEnableRequest, SetEnableStep } from '../../../../state/contoller-service-state';
import { Store } from '@ngrx/store';
import {
resetEnableControllerServiceState,
setControllerService,
stopPollingControllerService,
submitDisableRequest
} from '../../../../state/contoller-service-state/controller-service-state.actions';
import {
selectControllerService,
selectControllerServiceSetEnableRequest
} from '../../../../state/contoller-service-state/controller-service-state.selectors';
@Component({
selector: 'disable-controller-service',
standalone: true,
templateUrl: './disable-controller-service.component.html',
imports: [
MatDialogModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
NgIf,
MatTabsModule,
MatOptionModule,
MatSelectModule,
NgForOf,
PropertyTable,
ControllerServiceApi,
ControllerServiceReferences,
AsyncPipe,
NifiSpinnerDirective,
NifiTooltipDirective,
NgTemplateOutlet
],
styleUrls: ['./disable-controller-service.component.scss']
})
export class DisableControllerService implements OnDestroy {
@Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void;
protected readonly TextTip = TextTip;
protected readonly SetEnableStep = SetEnableStep;
disableRequest$ = this.store.select(selectControllerServiceSetEnableRequest);
controllerService$ = this.store.select(selectControllerService);
@ViewChild('stepComplete') stepComplete!: TemplateRef<any>;
@ViewChild('stepError') stepError!: TemplateRef<any>;
@ViewChild('stepInProgress') stepInProgress!: TemplateRef<any>;
@ViewChild('stepNotStarted') stepNotStarted!: TemplateRef<any>;
constructor(
@Inject(MAT_DIALOG_DATA) public request: SetEnableControllerServiceDialogRequest,
private store: Store<ControllerServiceState>
) {
this.store.dispatch(
setControllerService({
request: {
controllerService: request.controllerService
}
})
);
}
submitForm() {
this.store.dispatch(submitDisableRequest());
}
getTemplateForStep(step: SetEnableStep, disableRequest: SetEnableRequest): TemplateRef<any> {
if (disableRequest.currentStep > step) {
return this.stepComplete;
} else {
if (disableRequest.error?.step === step) {
return this.stepError;
}
if (disableRequest.currentStep === step) {
return this.stepInProgress;
}
return this.stepNotStarted;
}
}
cancelClicked(): void {
this.store.dispatch(stopPollingControllerService());
}
ngOnDestroy(): void {
this.store.dispatch(resetEnableControllerServiceState());
}
}

View File

@ -18,8 +18,6 @@
@use '@angular/material' as mat;
.controller-service-edit-form {
//@include mat.dialog-density(-2);
//@include mat.tabs-density(-2);
@include mat.button-density(-1);
.mdc-dialog__content {

View File

@ -0,0 +1,171 @@
<!--
~ 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>Enable Controller Service</h2>
<ng-container *ngIf="(controllerService$ | async)! as controllerService">
<form
class="controller-service-enable-form"
[formGroup]="enableControllerServiceForm"
*ngIf="(enableRequest$ | async)! as enableRequest">
<ng-container *ngIf="enableRequest.currentStep === SetEnableStep.Pending; else enableInProgress">
<mat-dialog-content>
<div class="tab-content py-4 flex gap-x-4">
<div class="w-96 flex flex-col gap-y-4">
<div class="flex flex-col">
<div>Service</div>
<div class="value">{{ controllerService.component.name }}</div>
</div>
<div>
<mat-form-field>
<mat-label>Scope</mat-label>
<mat-select formControlName="scope">
<mat-option
*ngFor="let option of controllerServiceActionScopes"
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
[delayClose]="false"
>{{ option.text }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="w-96 flex flex-col">
<div>Referencing Components</div>
<div>
<controller-service-references
[serviceReferences]="controllerService.component.referencingComponents"
[goToReferencingComponent]="goToReferencingComponent"></controller-service-references>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
<button
[disabled]="enableControllerServiceForm.invalid"
type="button"
color="primary"
(click)="submitForm()"
mat-raised-button>
Enable
</button>
</mat-dialog-actions>
</ng-container>
<ng-template #enableInProgress>
<mat-dialog-content>
<div class="tab-content py-4 flex gap-x-4">
<div class="w-96 flex flex-col gap-y-4">
<div class="flex flex-col">
<div>Service</div>
<div class="value">{{ controllerService.component.name }}</div>
</div>
<div class="flex flex-col">
<div>Steps To Disable {{ controllerService.component.name }}</div>
<div class="flex flex-col gap-y-1.5">
<div class="flex justify-between items-center">
<div class="value">Enabling this controller service</div>
<ng-container
*ngTemplateOutlet="
getTemplateForStep(SetEnableStep.EnableService, enableRequest)
"></ng-container>
</div>
<div
*ngIf="
enableRequest.error && enableRequest.error.step == SetEnableStep.EnableService
"
class="text-xs ml-2">
{{ enableRequest.error.error }}
</div>
<ng-container *ngIf="enableRequest.scope === 'SERVICE_AND_REFERENCING_COMPONENTS'">
<div class="flex justify-between items-center">
<div class="value">Enable referencing controller services</div>
<ng-container
*ngTemplateOutlet="
getTemplateForStep(
SetEnableStep.EnableReferencingServices,
enableRequest
)
"></ng-container>
</div>
<div
*ngIf="
enableRequest.error &&
enableRequest.error.step == SetEnableStep.EnableReferencingServices
"
class="text-xs ml-2">
{{ enableRequest.error.error }}
</div>
<div class="flex justify-between items-center">
<div class="value">Starting referencing processors and reporting tasks</div>
<ng-container
*ngTemplateOutlet="
getTemplateForStep(
SetEnableStep.StartReferencingComponents,
enableRequest
)
"></ng-container>
</div>
<div
*ngIf="
enableRequest.error &&
enableRequest.error.step == SetEnableStep.StartReferencingComponents
"
class="text-xs ml-2">
{{ enableRequest.error.error }}
</div>
</ng-container>
</div>
</div>
</div>
<div class="w-96 flex flex-col">
<div>Referencing Components</div>
<div>
<controller-service-references
[serviceReferences]="controllerService.component.referencingComponents"
[goToReferencingComponent]="goToReferencingComponent"></controller-service-references>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<ng-container
*ngIf="
enableRequest.currentStep === SetEnableStep.Completed || enableRequest.error;
else updateInProgressActions
">
<button color="primary" mat-raised-button mat-dialog-close>Close</button>
</ng-container>
<ng-template #updateInProgressActions>
<button color="accent" (click)="cancelClicked()" mat-raised-button mat-dialog-close>Cancel</button>
</ng-template>
</mat-dialog-actions>
</ng-template>
</form>
</ng-container>
<ng-template #stepInProgress>
<div class="fa fa-spin fa-circle-o-notch"></div>
</ng-template>
<ng-template #stepComplete>
<div class="fa fa-check text-green-500"></div>
</ng-template>
<ng-template #stepError>
<div class="fa fa-times text-red-400"></div>
</ng-template>
<ng-template #stepNotStarted><div class="w-3.5"></div></ng-template>

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;
.controller-service-enable-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,354 @@
/*
* 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 { EnableControllerService } from './enable-controller-service.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../state/contoller-service-state/controller-service-state.reducer';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { SetEnableControllerServiceDialogRequest } from '../../../../state/shared';
describe('EnableControllerService', () => {
let component: EnableControllerService;
let fixture: ComponentFixture<EnableControllerService>;
const data: SetEnableControllerServiceDialogRequest = {
id: '92db6ee4-018c-1000-1061-e3476c3f4e9f',
controllerService: {
revision: {
clientId: 'b37eba61-156a-47a4-875f-945cd153b876',
version: 7
},
id: '92db6ee4-018c-1000-1061-e3476c3f4e9f',
uri: 'https://localhost:4200/nifi-api/controller-services/92db6ee4-018c-1000-1061-e3476c3f4e9f',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [
{
id: 0,
groupId: 'asdf',
sourceId: 'asdf',
timestamp: '14:08:44 EST',
canRead: true,
bulletin: {
id: 0,
category: 'asdf',
groupId: 'asdf',
sourceId: 'asdf',
sourceName: 'asdf',
level: 'ERROR',
message: 'asdf',
timestamp: '14:08:44 EST'
}
}
],
component: {
id: '92db6ee4-018c-1000-1061-e3476c3f4e9f',
name: 'AvroReader',
type: 'org.apache.nifi.avro.AvroReader',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-record-serialization-services-nar',
version: '2.0.0-SNAPSHOT'
},
controllerServiceApis: [
{
type: 'org.apache.nifi.serialization.RecordReaderFactory',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
}
}
],
state: 'DISABLED',
persistsState: false,
restricted: false,
deprecated: false,
multipleVersionsAvailable: false,
supportsSensitiveDynamicProperties: false,
properties: {
'schema-access-strategy': 'embedded-avro-schema',
'schema-registry': null,
'schema-name': '${schema.name}',
'schema-version': null,
'schema-branch': null,
'schema-text': '${avro.schema}',
'schema-reference-reader': null,
'cache-size': '1000'
},
descriptors: {
'schema-access-strategy': {
name: 'schema-access-strategy',
displayName: 'Schema Access Strategy',
description: 'Specifies how to obtain the schema that is to be used for interpreting the data.',
defaultValue: 'embedded-avro-schema',
allowableValues: [
{
allowableValue: {
displayName: "Use 'Schema Name' Property",
value: 'schema-name',
description:
"The name of the Schema to use is specified by the 'Schema Name' Property. The value of this property is used to lookup the Schema in the configured Schema Registry service."
},
canRead: true
},
{
allowableValue: {
displayName: "Use 'Schema Text' Property",
value: 'schema-text-property',
description:
"The text of the Schema itself is specified by the 'Schema Text' Property. The value of this property must be a valid Avro Schema. If Expression Language is used, the value of the 'Schema Text' property must be valid after substituting the expressions."
},
canRead: true
},
{
allowableValue: {
displayName: 'Schema Reference Reader',
value: 'schema-reference-reader',
description:
'The schema reference information will be provided through a configured Schema Reference Reader service implementation.'
},
canRead: true
},
{
allowableValue: {
displayName: 'Use Embedded Avro Schema',
value: 'embedded-avro-schema',
description:
'The FlowFile has the Avro Schema embedded within the content, and this schema will be used.'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'schema-registry': {
name: 'schema-registry',
displayName: 'Schema Registry',
description: 'Specifies the Controller Service to use for the Schema Registry',
allowableValues: [],
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.schemaregistry.services.SchemaRegistry',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-reference-reader', 'schema-name']
}
]
},
'schema-name': {
name: 'schema-name',
displayName: 'Schema Name',
description: 'Specifies the name of the schema to lookup in the Schema Registry property',
defaultValue: '${schema.name}',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-name']
}
]
},
'schema-version': {
name: 'schema-version',
displayName: 'Schema Version',
description:
'Specifies the version of the schema to lookup in the Schema Registry. If not specified then the latest version of the schema will be retrieved.',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-name']
}
]
},
'schema-branch': {
name: 'schema-branch',
displayName: 'Schema Branch',
description:
'Specifies the name of the branch to use when looking up the schema in the Schema Registry property. If the chosen Schema Registry does not support branching, this value will be ignored.',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-name']
}
]
},
'schema-text': {
name: 'schema-text',
displayName: 'Schema Text',
description: 'The text of an Avro-formatted Schema',
defaultValue: '${avro.schema}',
required: false,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables and FlowFile Attributes',
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-text-property']
}
]
},
'schema-reference-reader': {
name: 'schema-reference-reader',
displayName: 'Schema Reference Reader',
description:
'Service implementation responsible for reading FlowFile attributes or content to determine the Schema Reference Identifier',
allowableValues: [],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.schemaregistry.services.SchemaReferenceReader',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: [
{
propertyName: 'schema-access-strategy',
dependentValues: ['schema-reference-reader']
}
]
},
'cache-size': {
name: 'cache-size',
displayName: 'Cache Size',
description: 'Specifies how many Schemas should be cached',
defaultValue: '1000',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
}
},
referencingComponents: [
{
revision: {
clientId: 'b37eba61-156a-47a4-875f-945cd153b876',
version: 5,
lastModifier: 'admin'
},
id: '92db918c-018c-1000-ccd5-0796caa6d463',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [
{
id: 0,
groupId: 'asdf',
sourceId: 'asdf',
timestamp: '14:08:44 EST',
canRead: true,
bulletin: {
id: 0,
category: 'asdf',
groupId: 'asdf',
sourceId: 'asdf',
sourceName: 'asdf',
level: 'ERROR',
message: 'asdf',
timestamp: '14:08:44 EST'
}
}
],
component: {
id: '92db918c-018c-1000-ccd5-0796caa6d463',
name: 'ReaderLookup',
type: 'ReaderLookup',
state: 'DISABLED',
validationErrors: [
"'avro' validated against '92db6ee4-018c-1000-1061-e3476c3f4e9f' is invalid because Controller Service with ID 92db6ee4-018c-1000-1061-e3476c3f4e9f is disabled"
],
referenceType: 'ControllerService',
referenceCycle: false,
referencingComponents: []
},
operatePermissions: {
canRead: false,
canWrite: false
}
}
],
validationStatus: 'VALID',
bulletinLevel: 'WARN',
extensionMissing: false
},
operatePermissions: {
canRead: false,
canWrite: false
},
status: {
runStatus: 'DISABLED',
validationStatus: 'VALID'
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EnableControllerService, BrowserAnimationsModule, MatDialogModule],
providers: [provideMockStore({ initialState }), { provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(EnableControllerService);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,159 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Inject, Input, OnDestroy, TemplateRef, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import {
ControllerServiceReferencingComponent,
SetEnableControllerServiceDialogRequest,
SelectOption,
TextTipInput
} from '../../../../state/shared';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { PropertyTable } from '../../property-table/property-table.component';
import { ControllerServiceApi } from '../controller-service-api/controller-service-api.component';
import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component';
import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive';
import { TextTip } from '../../tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../../tooltips/nifi-tooltip.directive';
import {
controllerServiceActionScopes,
ControllerServiceState,
SetEnableRequest,
SetEnableStep
} from '../../../../state/contoller-service-state';
import { Store } from '@ngrx/store';
import {
resetEnableControllerServiceState,
setControllerService,
stopPollingControllerService,
submitEnableRequest
} from '../../../../state/contoller-service-state/controller-service-state.actions';
import {
selectControllerService,
selectControllerServiceSetEnableRequest
} from '../../../../state/contoller-service-state/controller-service-state.selectors';
@Component({
selector: 'enable-controller-service',
standalone: true,
templateUrl: './enable-controller-service.component.html',
imports: [
ReactiveFormsModule,
MatDialogModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
NgIf,
MatTabsModule,
MatOptionModule,
MatSelectModule,
NgForOf,
PropertyTable,
ControllerServiceApi,
ControllerServiceReferences,
AsyncPipe,
NifiSpinnerDirective,
NifiTooltipDirective,
NgTemplateOutlet
],
styleUrls: ['./enable-controller-service.component.scss']
})
export class EnableControllerService implements OnDestroy {
@Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void;
protected readonly TextTip = TextTip;
protected readonly SetEnableStep = SetEnableStep;
protected readonly controllerServiceActionScopes: SelectOption[] = controllerServiceActionScopes;
enableRequest$ = this.store.select(selectControllerServiceSetEnableRequest);
controllerService$ = this.store.select(selectControllerService);
enableControllerServiceForm: FormGroup;
@ViewChild('stepComplete') stepComplete!: TemplateRef<any>;
@ViewChild('stepError') stepError!: TemplateRef<any>;
@ViewChild('stepInProgress') stepInProgress!: TemplateRef<any>;
@ViewChild('stepNotStarted') stepNotStarted!: TemplateRef<any>;
constructor(
@Inject(MAT_DIALOG_DATA) public request: SetEnableControllerServiceDialogRequest,
private store: Store<ControllerServiceState>,
private formBuilder: FormBuilder
) {
// build the form
this.enableControllerServiceForm = this.formBuilder.group({
scope: new FormControl(controllerServiceActionScopes[0].value, Validators.required)
});
this.store.dispatch(
setControllerService({
request: {
controllerService: request.controllerService
}
})
);
}
getSelectOptionTipData(option: SelectOption): TextTipInput {
return {
// @ts-ignore
text: option.description
};
}
submitForm() {
this.store.dispatch(
submitEnableRequest({
request: {
scope: this.enableControllerServiceForm.get('scope')?.value
}
})
);
}
getTemplateForStep(step: SetEnableStep, enableRequest: SetEnableRequest): TemplateRef<any> {
if (enableRequest.currentStep > step) {
return this.stepComplete;
} else {
if (enableRequest.error?.step === step) {
return this.stepError;
}
if (enableRequest.currentStep === step) {
return this.stepInProgress;
}
return this.stepNotStarted;
}
}
cancelClicked(): void {
this.store.dispatch(stopPollingControllerService());
}
ngOnDestroy(): void {
this.store.dispatch(resetEnableControllerServiceState());
}
}

View File

@ -72,16 +72,20 @@
[tooltipComponentType]="TextTip"
[tooltipInputData]="getAllowableValueOptionTipData(parameterAllowableValue)"
[delayClose]="false">
<span class="option-text" [class.unset]="parameterAllowableValue.value == null">{{
parameterAllowableValue.displayName
}}</span>
<span
class="option-text"
[class.unset]="parameterAllowableValue.value == null"
>{{ parameterAllowableValue.displayName }}</span
>
</mat-option>
</ng-container>
<ng-template #noDescription>
<mat-option [value]="parameterAllowableValue.id" (mousedown)="preventDrag($event)">
<span class="option-text" [class.unset]="parameterAllowableValue.value == null">{{
parameterAllowableValue.displayName
}}</span>
<span
class="option-text"
[class.unset]="parameterAllowableValue.value == null"
>{{ parameterAllowableValue.displayName }}</span
>
</mat-option>
</ng-template>
</ng-container>