[NIFI-13034] Change Component Version (#8653)

* [NIFI-13034] - Change Processor version

* Change controller service version

* Change version of reporting task and flow analysis rule

* add missing license header

* fix for updating parameter context when in clustered mode.

* review feedback - collapse 2 actions for opening change component version into 1

* update DocumentedType comparison logic

This closes #8653
This commit is contained in:
Rob Fellows 2024-04-17 13:36:47 -04:00 committed by GitHub
parent 5eccc4c7e7
commit 20ec8064c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 799 additions and 33 deletions

View File

@ -22,14 +22,16 @@ import { CanvasState } from '../state';
import {
centerSelectedComponents,
deleteComponents,
downloadFlow,
enterProcessGroup,
getParameterContextsAndOpenGroupComponentsDialog,
goToRemoteProcessGroup,
leaveProcessGroup,
moveComponents,
moveToFront,
navigateToAdvancedProcessorUi,
navigateToComponent,
navigateToControllerServicesForProcessGroup,
navigateToAdvancedProcessorUi,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
navigateToManageComponentPolicies,
@ -37,6 +39,7 @@ import {
navigateToProvenanceForComponent,
navigateToQueueListing,
navigateToViewStatusHistoryForComponent,
openChangeProcessorVersionDialog,
openChangeVersionDialogRequest,
openCommitLocalChangesDialogRequest,
openForceCommitLocalChangesDialogRequest,
@ -51,9 +54,7 @@ import {
startCurrentProcessGroup,
stopComponents,
stopCurrentProcessGroup,
stopVersionControlRequest,
downloadFlow,
moveToFront
stopVersionControlRequest
} from '../state/flow/flow.actions';
import { ComponentType } from '../../../state/shared';
import {
@ -970,14 +971,24 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}
},
{
condition: (selection: any) => {
// TODO - canChangeProcessorVersion
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.canChangeProcessorVersion(selection);
},
clazz: 'fa fa-exchange',
text: 'Change version',
action: () => {
// TODO - changeVersion
action: (selection: d3.Selection<any, any, any, any>) => {
const data = selection.datum();
this.store.dispatch(
openChangeProcessorVersionDialog({
request: {
id: data.component.id,
uri: data.uri,
revision: data.revision,
type: data.component.type,
bundle: data.component.bundle
}
})
);
}
},
{

View File

@ -1712,6 +1712,33 @@ export class CanvasUtils {
return false;
}
/**
* Determines whether the current selection is a processor with more than one version.
*
* @argument {d3.Selection} selection The selection
* @returns {boolean}
*/
public canChangeProcessorVersion(selection: d3.Selection<any, any, any, any>): boolean {
if (selection.size() !== 1) {
return false;
}
if (!this.canRead(selection) || !this.canModify(selection)) {
return false;
}
if (this.isProcessor(selection)) {
const data = selection.datum();
const supportsModification = !(
data.status.aggregateSnapshot.runStatus === 'Running' ||
data.status.aggregateSnapshot.activeThreadCount > 0
);
return supportsModification && data.component.multipleVersionsAvailable;
}
return false;
}
public canMoveToFront(selection: d3.Selection<any, any, any, any>): boolean {
// ensure the correct number of components are selected
if (selection.size() !== 1) {

View File

@ -30,6 +30,7 @@ import {
CreateControllerServiceRequest,
DisableControllerServiceDialogRequest,
EditControllerServiceDialogRequest,
FetchComponentVersionsRequest,
SetEnableControllerServiceDialogRequest
} from '../../../../state/shared';
@ -121,3 +122,8 @@ export const selectControllerService = createAction(
'[Controller Services] Select Controller Service',
props<{ request: SelectControllerServiceRequest }>()
);
export const openChangeControllerServiceVersionDialog = createAction(
`[Controller Services] Open Change Controller Service Version Dialog`,
props<{ request: FetchComponentVersionsRequest }>()
);

View File

@ -30,6 +30,7 @@ import { EditControllerService } from '../../../../ui/common/controller-service/
import {
ComponentType,
ControllerServiceReferencingComponent,
OpenChangeComponentVersionDialogRequest,
EditControllerServiceDialogRequest,
UpdateControllerServiceRequest
} from '../../../../state/shared';
@ -49,6 +50,8 @@ import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { ParameterHelperService } from '../../service/parameter-helper.service';
import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
@Injectable()
export class ControllerServicesEffects {
@ -61,7 +64,8 @@ export class ControllerServicesEffects {
private dialog: MatDialog,
private router: Router,
private propertyTableHelperService: PropertyTableHelperService,
private parameterHelperService: ParameterHelperService
private parameterHelperService: ParameterHelperService,
private extensionTypesService: ExtensionTypesService
) {}
loadControllerServices$ = createEffect(() =>
@ -551,6 +555,58 @@ export class ControllerServicesEffects {
{ dispatch: false }
);
openChangeControllerServiceVersionDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.openChangeControllerServiceVersionDialog),
map((action) => action.request),
switchMap((request) =>
from(
this.extensionTypesService.getControllerServiceVersionsForType(request.type, request.bundle)
).pipe(
map(
(response) =>
({
fetchRequest: request,
componentVersions: response.controllerServiceTypes
}) as OpenChangeComponentVersionDialogRequest
),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
})
)
),
tap((request) => {
const dialogRequest = this.dialog.open(ChangeComponentVersionDialog, {
...LARGE_DIALOG,
data: request
});
dialogRequest.componentInstance.changeVersion.pipe(take(1)).subscribe((newVersion) => {
this.store.dispatch(
ControllerServicesActions.configureControllerService({
request: {
id: request.fetchRequest.id,
uri: request.fetchRequest.uri,
payload: {
component: {
bundle: newVersion.bundle,
id: request.fetchRequest.id
},
revision: request.fetchRequest.revision
}
}
})
);
dialogRequest.close();
});
})
),
{ dispatch: false }
);
private getRouteForReference(reference: ControllerServiceReferencingComponent): string[] {
if (reference.referenceType == 'ControllerService') {
if (reference.groupId == null) {

View File

@ -94,6 +94,7 @@ import {
VersionControlInformationEntity
} from './index';
import { StatusHistoryRequest } from '../../../../state/status-history';
import { FetchComponentVersionsRequest } from '../../../../state/shared';
const CANVAS_PREFIX = '[Canvas]';
@ -769,3 +770,8 @@ export const downloadFlow = createAction(
);
export const moveToFront = createAction(`${CANVAS_PREFIX} Move To Front`, props<{ request: MoveToFrontRequest }>());
export const openChangeProcessorVersionDialog = createAction(
`${CANVAS_PREFIX} Open Change Processor Version Dialog`,
props<{ request: FetchComponentVersionsRequest }>()
);

View File

@ -80,6 +80,7 @@ import {
BucketEntity,
ComponentType,
isDefinedAndNotNull,
OpenChangeComponentVersionDialogRequest,
RegistryClientEntity,
VersionedFlowEntity,
VersionedFlowSnapshotMetadataEntity
@ -116,6 +117,8 @@ import { ChangeVersionDialog } from '../../ui/canvas/items/flow/change-version-d
import { ChangeVersionProgressDialog } from '../../ui/canvas/items/flow/change-version-progress-dialog/change-version-progress-dialog';
import { LocalChangesDialog } from '../../ui/canvas/items/flow/local-changes-dialog/local-changes-dialog';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
@Injectable()
export class FlowEffects {
@ -134,7 +137,8 @@ export class FlowEffects {
private router: Router,
private dialog: MatDialog,
private propertyTableHelperService: PropertyTableHelperService,
private parameterHelperService: ParameterHelperService
private parameterHelperService: ParameterHelperService,
private extensionTypesService: ExtensionTypesService
) {}
reloadFlow$ = createEffect(() =>
@ -3199,4 +3203,55 @@ export class FlowEffects {
})
)
);
openChangeProcessorVersionDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.openChangeProcessorVersionDialog),
map((action) => action.request),
switchMap((request) =>
from(this.extensionTypesService.getProcessorVersionsForType(request.type, request.bundle)).pipe(
map(
(response) =>
({
fetchRequest: request,
componentVersions: response.processorTypes
}) as OpenChangeComponentVersionDialogRequest
),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(FlowActions.flowSnackbarError({ error: errorResponse.error }));
}
})
)
),
tap((request) => {
const dialogRequest = this.dialog.open(ChangeComponentVersionDialog, {
...LARGE_DIALOG,
data: request
});
dialogRequest.componentInstance.changeVersion.pipe(take(1)).subscribe((newVersion) => {
this.store.dispatch(
FlowActions.updateProcessor({
request: {
id: request.fetchRequest.id,
uri: request.fetchRequest.uri,
type: ComponentType.Processor,
payload: {
component: {
bundle: newVersion.bundle,
id: request.fetchRequest.id
},
revision: request.fetchRequest.revision
}
}
})
);
dialogRequest.close();
});
})
),
{ dispatch: false }
);
}

View File

@ -53,6 +53,7 @@
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"
(viewStateControllerService)="viewStateControllerService($event)"
(changeControllerServiceVersion)="changeControllerServiceVersion($event)"
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
</div>
}

View File

@ -31,6 +31,7 @@ import {
loadControllerServices,
navigateToAdvancedServiceUi,
navigateToEditService,
openChangeControllerServiceVersionDialog,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
openEnableControllerServiceDialog,
@ -228,6 +229,20 @@ export class ControllerServices implements OnInit, OnDestroy {
);
}
changeControllerServiceVersion(entity: ControllerServiceEntity): void {
this.store.dispatch(
openChangeControllerServiceVersionDialog({
request: {
id: entity.id,
bundle: entity.component.bundle,
uri: entity.uri,
type: entity.component.type,
revision: entity.revision
}
})
);
}
deleteControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
promptControllerServiceDeletion({

View File

@ -156,6 +156,7 @@ export class EditParameterContext {
const payload: any = {
revision: this.client.getRevision(pc),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
id: pc.id,
component: {
id: pc.id,
name: this.editParameterContextForm.get('name')?.value,

View File

@ -23,14 +23,15 @@ import {
CreateFlowAnalysisRuleSuccess,
DeleteFlowAnalysisRuleRequest,
DeleteFlowAnalysisRuleSuccess,
EditFlowAnalysisRuleDialogRequest,
LoadFlowAnalysisRulesResponse,
SelectFlowAnalysisRuleRequest,
DisableFlowAnalysisRuleRequest,
DisableFlowAnalysisRuleSuccess,
EditFlowAnalysisRuleDialogRequest,
EnableFlowAnalysisRuleRequest,
EnableFlowAnalysisRuleSuccess,
DisableFlowAnalysisRuleSuccess
LoadFlowAnalysisRulesResponse,
SelectFlowAnalysisRuleRequest
} from './index';
import { FetchComponentVersionsRequest } from '../../../../state/shared';
export const resetFlowAnalysisRulesState = createAction('[Flow Analysis Rules] Reset Flow Analysis Rules State');
@ -126,3 +127,8 @@ export const selectFlowAnalysisRule = createAction(
'[Flow Analysis Rules] Select Flow Analysis Rule',
props<{ request: SelectFlowAnalysisRuleRequest }>()
);
export const openChangeFlowAnalysisRuleVersionDialog = createAction(
`[Flow Analysis Rules] Open Change Flow Analysis Rule Version Dialog`,
props<{ request: FetchComponentVersionsRequest }>()
);

View File

@ -29,7 +29,7 @@ import { ManagementControllerServiceService } from '../../service/management-con
import { CreateFlowAnalysisRule } from '../../ui/flow-analysis-rules/create-flow-analysis-rule/create-flow-analysis-rule.component';
import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import { UpdateControllerServiceRequest } from '../../../../state/shared';
import { OpenChangeComponentVersionDialogRequest, UpdateControllerServiceRequest } from '../../../../state/shared';
import { EditFlowAnalysisRule } from '../../ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component';
import { CreateFlowAnalysisRuleSuccess, EditFlowAnalysisRuleDialogRequest } from './index';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
@ -38,6 +38,8 @@ import { ErrorHelper } from '../../../../service/error-helper.service';
import { selectStatus } from './flow-analysis-rules.selectors';
import { HttpErrorResponse } from '@angular/common/http';
import { LARGE_DIALOG, SMALL_DIALOG } from '../../../../index';
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
@Injectable()
export class FlowAnalysisRulesEffects {
@ -49,7 +51,8 @@ export class FlowAnalysisRulesEffects {
private errorHelper: ErrorHelper,
private dialog: MatDialog,
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
private propertyTableHelperService: PropertyTableHelperService,
private extensionTypesService: ExtensionTypesService
) {}
loadFlowAnalysisRule$ = createEffect(() =>
@ -464,4 +467,56 @@ export class FlowAnalysisRulesEffects {
),
{ dispatch: false }
);
openChangeFlowAnalysisRuleVersionDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowAnalysisRuleActions.openChangeFlowAnalysisRuleVersionDialog),
map((action) => action.request),
switchMap((request) =>
from(
this.extensionTypesService.getFlowAnalysisRuleVersionsForType(request.type, request.bundle)
).pipe(
map(
(response) =>
({
fetchRequest: request,
componentVersions: response.flowAnalysisRuleTypes
}) as OpenChangeComponentVersionDialogRequest
),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
})
)
),
tap((request) => {
const dialogRequest = this.dialog.open(ChangeComponentVersionDialog, {
...LARGE_DIALOG,
data: request
});
dialogRequest.componentInstance.changeVersion.pipe(take(1)).subscribe((newVersion) => {
this.store.dispatch(
FlowAnalysisRuleActions.configureFlowAnalysisRule({
request: {
id: request.fetchRequest.id,
uri: request.fetchRequest.uri,
payload: {
component: {
bundle: newVersion.bundle,
id: request.fetchRequest.id
},
revision: request.fetchRequest.revision
}
}
})
);
dialogRequest.close();
});
})
),
{ dispatch: false }
);
}

View File

@ -29,6 +29,7 @@ import {
CreateControllerServiceRequest,
DisableControllerServiceDialogRequest,
EditControllerServiceDialogRequest,
FetchComponentVersionsRequest,
SetEnableControllerServiceDialogRequest
} from '../../../../state/shared';
@ -128,3 +129,8 @@ export const selectControllerService = createAction(
'[Management Controller Services] Select Controller Service',
props<{ request: SelectControllerServiceRequest }>()
);
export const openChangeMgtControllerServiceVersionDialog = createAction(
`[Management Controller Services] Open Change Management Controller Service Version Dialog`,
props<{ request: FetchComponentVersionsRequest }>()
);

View File

@ -33,6 +33,7 @@ import {
ComponentType,
ControllerServiceReferencingComponent,
EditControllerServiceDialogRequest,
OpenChangeComponentVersionDialogRequest,
UpdateControllerServiceRequest
} from '../../../../state/shared';
import { Router } from '@angular/router';
@ -43,6 +44,8 @@ import { PropertyTableHelperService } from '../../../../service/property-table-h
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { LARGE_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index';
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
@Injectable()
export class ManagementControllerServicesEffects {
@ -54,7 +57,8 @@ export class ManagementControllerServicesEffects {
private errorHelper: ErrorHelper,
private dialog: MatDialog,
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
private propertyTableHelperService: PropertyTableHelperService,
private extensionTypesService: ExtensionTypesService
) {}
loadManagementControllerServices$ = createEffect(() =>
@ -507,6 +511,58 @@ export class ManagementControllerServicesEffects {
{ dispatch: false }
);
openChangeMgtControllerServiceVersionDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ManagementControllerServicesActions.openChangeMgtControllerServiceVersionDialog),
map((action) => action.request),
switchMap((request) =>
from(
this.extensionTypesService.getControllerServiceVersionsForType(request.type, request.bundle)
).pipe(
map(
(response) =>
({
fetchRequest: request,
componentVersions: response.controllerServiceTypes
}) as OpenChangeComponentVersionDialogRequest
),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
})
)
),
tap((request) => {
const dialogRequest = this.dialog.open(ChangeComponentVersionDialog, {
...LARGE_DIALOG,
data: request
});
dialogRequest.componentInstance.changeVersion.pipe(take(1)).subscribe((newVersion) => {
this.store.dispatch(
ManagementControllerServicesActions.configureControllerService({
request: {
id: request.fetchRequest.id,
uri: request.fetchRequest.uri,
payload: {
component: {
bundle: newVersion.bundle,
id: request.fetchRequest.id
},
revision: request.fetchRequest.revision
}
}
})
);
dialogRequest.close();
});
})
),
{ dispatch: false }
);
private getRouteForReference(reference: ControllerServiceReferencingComponent): string[] {
if (reference.referenceType == 'ControllerService') {
if (reference.groupId == null) {

View File

@ -17,6 +17,8 @@
import { createAction, props } from '@ngrx/store';
import {
ConfigureReportingTaskRequest,
ConfigureReportingTaskSuccess,
CreateReportingTaskRequest,
CreateReportingTaskSuccess,
DeleteReportingTaskRequest,
@ -27,10 +29,9 @@ import {
StartReportingTaskRequest,
StartReportingTaskSuccess,
StopReportingTaskRequest,
StopReportingTaskSuccess,
ConfigureReportingTaskRequest,
ConfigureReportingTaskSuccess
StopReportingTaskSuccess
} from './index';
import { FetchComponentVersionsRequest } from '../../../../state/shared';
export const resetReportingTasksState = createAction('[Reporting Tasks] Reset Reporting Tasks State');
@ -127,3 +128,8 @@ export const selectReportingTask = createAction(
'[Reporting Tasks] Select Reporting Task',
props<{ request: SelectReportingTaskRequest }>()
);
export const openChangeReportingTaskVersionDialog = createAction(
`[Reporting Tasks] Open Change Reporting Task Version Dialog`,
props<{ request: FetchComponentVersionsRequest }>()
);

View File

@ -28,7 +28,7 @@ import { ReportingTaskService } from '../../service/reporting-task.service';
import { CreateReportingTask } from '../../ui/reporting-tasks/create-reporting-task/create-reporting-task.component';
import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import { UpdateControllerServiceRequest } from '../../../../state/shared';
import { OpenChangeComponentVersionDialogRequest, UpdateControllerServiceRequest } from '../../../../state/shared';
import { EditReportingTask } from '../../ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component';
import { CreateReportingTaskSuccess, EditReportingTaskDialogRequest } from './index';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
@ -38,6 +38,8 @@ import { ErrorHelper } from '../../../../service/error-helper.service';
import { selectStatus } from './reporting-tasks.selectors';
import { HttpErrorResponse } from '@angular/common/http';
import { LARGE_DIALOG, SMALL_DIALOG } from '../../../../index';
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
@Injectable()
export class ReportingTasksEffects {
@ -49,7 +51,8 @@ export class ReportingTasksEffects {
private errorHelper: ErrorHelper,
private dialog: MatDialog,
private router: Router,
private propertyTableHelperService: PropertyTableHelperService
private propertyTableHelperService: PropertyTableHelperService,
private extensionTypesService: ExtensionTypesService
) {}
loadReportingTasks$ = createEffect(() =>
@ -444,4 +447,54 @@ export class ReportingTasksEffects {
),
{ dispatch: false }
);
openChangeReportingTaskVersionDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ReportingTaskActions.openChangeReportingTaskVersionDialog),
map((action) => action.request),
switchMap((request) =>
from(this.extensionTypesService.getReportingTaskVersionsForType(request.type, request.bundle)).pipe(
map(
(response) =>
({
fetchRequest: request,
componentVersions: response.reportingTaskTypes
}) as OpenChangeComponentVersionDialogRequest
),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(ErrorActions.snackBarError({ error: errorResponse.error }));
}
})
)
),
tap((request) => {
const dialogRequest = this.dialog.open(ChangeComponentVersionDialog, {
...LARGE_DIALOG,
data: request
});
dialogRequest.componentInstance.changeVersion.pipe(take(1)).subscribe((newVersion) => {
this.store.dispatch(
ReportingTaskActions.configureReportingTask({
request: {
id: request.fetchRequest.id,
uri: request.fetchRequest.uri,
payload: {
component: {
bundle: newVersion.bundle,
id: request.fetchRequest.id
},
revision: request.fetchRequest.revision
}
}
})
);
dialogRequest.close();
});
})
),
{ dispatch: false }
);
}

View File

@ -142,7 +142,10 @@
title="Enable"></div>
}
@if (canChangeVersion(item)) {
<div class="pointer fa fa-exchange primary-color" title="Change Version"></div>
<div
class="pointer fa fa-exchange primary-color"
(click)="changeVersionClicked(item)"
title="Change Version"></div>
}
@if (canDelete(item)) {
<div

View File

@ -66,6 +66,8 @@ export class FlowAnalysisRuleTable {
new EventEmitter<FlowAnalysisRuleEntity>();
@Output() disableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
new EventEmitter<FlowAnalysisRuleEntity>();
@Output() changeFlowAnalysisRuleVersion: EventEmitter<FlowAnalysisRuleEntity> =
new EventEmitter<FlowAnalysisRuleEntity>();
sort: Sort = {
active: 'name',
@ -230,6 +232,10 @@ export class FlowAnalysisRuleTable {
this.enableFlowAnalysisRule.next(entity);
}
changeVersionClicked(entity: FlowAnalysisRuleEntity): void {
this.changeFlowAnalysisRuleVersion.next(entity);
}
canDisable(entity: FlowAnalysisRuleEntity): boolean {
const userAuthorized: boolean = this.canRead(entity) && this.canOperate(entity);
return userAuthorized && this.isEnabled(entity);

View File

@ -41,6 +41,7 @@
(enableFlowAnalysisRule)="enableFlowAnalysisRule($event)"
(disableFlowAnalysisRule)="disableFlowAnalysisRule($event)"
(viewStateFlowAnalysisRule)="viewStateFlowAnalysisRule($event)"
(changeFlowAnalysisRuleVersion)="changeFlowAnalysisRuleVersion($event)"
(deleteFlowAnalysisRule)="deleteFlowAnalysisRule($event)"></flow-analysis-rule-table>
</div>
<div class="flex justify-between">

View File

@ -30,6 +30,7 @@ import {
enableFlowAnalysisRule,
loadFlowAnalysisRules,
navigateToEditFlowAnalysisRule,
openChangeFlowAnalysisRuleVersionDialog,
openConfigureFlowAnalysisRuleDialog,
openNewFlowAnalysisRuleDialog,
promptFlowAnalysisRuleDeletion,
@ -155,6 +156,20 @@ export class FlowAnalysisRules implements OnInit, OnDestroy {
);
}
changeFlowAnalysisRuleVersion(entity: FlowAnalysisRuleEntity): void {
this.store.dispatch(
openChangeFlowAnalysisRuleVersionDialog({
request: {
id: entity.id,
bundle: entity.component.bundle,
uri: entity.uri,
type: entity.component.type,
revision: entity.revision
}
})
);
}
deleteFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void {
this.store.dispatch(
promptFlowAnalysisRuleDeletion({

View File

@ -46,6 +46,7 @@
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"
(viewStateControllerService)="viewStateControllerService($event)"
(changeControllerServiceVersion)="changeControllerServiceVersion($event)"
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
</div>
<div class="flex justify-between">

View File

@ -28,6 +28,7 @@ import {
loadManagementControllerServices,
navigateToAdvancedServiceUi,
navigateToEditService,
openChangeMgtControllerServiceVersionDialog,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
openEnableControllerServiceDialog,
@ -175,6 +176,20 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
);
}
changeControllerServiceVersion(entity: ControllerServiceEntity): void {
this.store.dispatch(
openChangeMgtControllerServiceVersionDialog({
request: {
id: entity.id,
bundle: entity.component.bundle,
uri: entity.uri,
type: entity.component.type,
revision: entity.revision
}
})
);
}
deleteControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
promptControllerServiceDeletion({

View File

@ -143,7 +143,10 @@
title="Start"></div>
}
@if (canChangeVersion(item)) {
<div class="pointer fa fa-exchange primary-color" title="Change Version"></div>
<div
class="pointer fa fa-exchange primary-color"
(click)="changeVersionClicked(item)"
title="Change Version"></div>
}
@if (canDelete(item)) {
<div

View File

@ -57,6 +57,7 @@ export class ReportingTaskTable {
@Output() openAdvancedUi: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() viewStateReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() stopReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() changeReportingTaskVersion: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
protected readonly TextTip = TextTip;
protected readonly BulletinsTip = BulletinsTip;
@ -235,6 +236,10 @@ export class ReportingTaskTable {
);
}
changeVersionClicked(entity: ReportingTaskEntity): void {
this.changeReportingTaskVersion.next(entity);
}
deleteClicked(entity: ReportingTaskEntity): void {
this.deleteReportingTask.next(entity);
}

View File

@ -43,6 +43,7 @@
(viewReportingTaskDocumentation)="viewReportingTaskDocumentation($event)"
(deleteReportingTask)="deleteReportingTask($event)"
(stopReportingTask)="stopReportingTask($event)"
(changeReportingTaskVersion)="changeReportingTaskVersion($event)"
(startReportingTask)="startReportingTask($event)"></reporting-task-table>
</div>
<div class="flex justify-between">

View File

@ -28,15 +28,16 @@ import {
} from '../../state/reporting-tasks/reporting-tasks.selectors';
import {
loadReportingTasks,
navigateToAdvancedReportingTaskUi,
navigateToEditReportingTask,
openChangeReportingTaskVersionDialog,
openConfigureReportingTaskDialog,
openNewReportingTaskDialog,
promptReportingTaskDeletion,
resetReportingTasksState,
startReportingTask,
stopReportingTask,
selectReportingTask,
navigateToAdvancedReportingTaskUi
startReportingTask,
stopReportingTask
} from '../../state/reporting-tasks/reporting-tasks.actions';
import { initialState } from '../../state/reporting-tasks/reporting-tasks.reducer';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
@ -184,6 +185,20 @@ export class ReportingTasks implements OnInit, OnDestroy {
);
}
changeReportingTaskVersion(entity: ReportingTaskEntity): void {
this.store.dispatch(
openChangeReportingTaskVersionDialog({
request: {
id: entity.id,
bundle: entity.component.bundle,
uri: entity.uri,
type: entity.component.type,
revision: entity.revision
}
})
);
}
ngOnDestroy(): void {
this.store.dispatch(resetReportingTasksState());
}

View File

@ -30,10 +30,28 @@ export class ExtensionTypesService {
return this.httpClient.get(`${ExtensionTypesService.API}/flow/processor-types`);
}
getProcessorVersionsForType(processorType: string, bundle: Bundle): Observable<any> {
const params = {
bundleGroupFilter: bundle.group,
bundleArtifactFilter: bundle.artifact,
type: processorType
};
return this.httpClient.get(`${ExtensionTypesService.API}/flow/processor-types`, { params });
}
getControllerServiceTypes(): Observable<any> {
return this.httpClient.get(`${ExtensionTypesService.API}/flow/controller-service-types`);
}
getControllerServiceVersionsForType(serviceType: string, bundle: Bundle): Observable<any> {
const params: any = {
serviceBundleGroup: bundle.group,
serviceBundleArtifact: bundle.artifact,
typeFilter: serviceType
};
return this.httpClient.get(`${ExtensionTypesService.API}/flow/controller-service-types`, { params });
}
getImplementingControllerServiceTypes(serviceType: string, bundle: Bundle): Observable<any> {
const params: any = {
serviceType,
@ -48,6 +66,15 @@ export class ExtensionTypesService {
return this.httpClient.get(`${ExtensionTypesService.API}/flow/reporting-task-types`);
}
getReportingTaskVersionsForType(reportingTaskType: string, bundle: Bundle): Observable<any> {
const params = {
serviceBundleGroup: bundle.group,
serviceBundleArtifact: bundle.artifact,
type: reportingTaskType
};
return this.httpClient.get(`${ExtensionTypesService.API}/flow/reporting-task-types`, { params });
}
getRegistryClientTypes(): Observable<any> {
return this.httpClient.get(`${ExtensionTypesService.API}/controller/registry-types`);
}
@ -60,6 +87,15 @@ export class ExtensionTypesService {
return this.httpClient.get(`${ExtensionTypesService.API}/flow/flow-analysis-rule-types`);
}
getFlowAnalysisRuleVersionsForType(flowAnalysisRuleType: string, bundle: Bundle): Observable<any> {
const params: any = {
serviceBundleGroup: bundle.group,
serviceBundleArtifact: bundle.artifact,
type: flowAnalysisRuleType
};
return this.httpClient.get(`${ExtensionTypesService.API}/flow/flow-analysis-rule-types`, { params });
}
getParameterProviderTypes(): Observable<any> {
return this.httpClient.get(`${ExtensionTypesService.API}/flow/parameter-provider-types`);
}

View File

@ -646,3 +646,16 @@ export interface ParameterProviderConfigurationEntity {
id: string;
component: ParameterProviderConfiguration;
}
export interface FetchComponentVersionsRequest {
id: string;
uri: string;
revision: Revision;
type: string;
bundle: Bundle;
}
export interface OpenChangeComponentVersionDialogRequest {
fetchRequest: FetchComponentVersionsRequest;
componentVersions: DocumentedType[];
}

View File

@ -0,0 +1,84 @@
<!--
~ 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>Change Version</h2>
<div class="change-version">
<form [formGroup]="changeComponentVersionForm">
<mat-dialog-content>
<div class="flex flex-col gap-y-4 w-full">
<div>
<div>Name</div>
<div class="accent-color font-medium">
{{ getName(selected) }}
</div>
</div>
<div>
<div>Bundle</div>
<div class="accent-color font-medium">
{{ selected?.bundle?.group }} - {{ selected?.bundle?.artifact }}
</div>
</div>
<mat-form-field subscriptSizing="dynamic">
<mat-label>Version</mat-label>
<mat-select [(value)]="selected" tabindex="0">
@for (version of versions; track version) {
<mat-option [value]="version" [disabled]="isCurrent(version)">
{{ version.bundle.version }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (selected?.controllerServiceApis) {
<div>
<div>Supports Controller Services</div>
<div class="accent-color font-medium">
<ul>
@for (serviceApi of selected?.controllerServiceApis; track serviceApi) {
<li>
<controller-service-api
[type]="serviceApi.type"
[bundle]="serviceApi.bundle"></controller-service-api>
</li>
}
</ul>
</div>
</div>
}
<div>
<div>Restriction</div>
@if (selected?.usageRestriction) {
<div class="accent-color font-medium">
{{ selected?.usageRestriction }}
</div>
} @else {
<div class="unset nifi-surface-default">Not Restricted</div>
}
</div>
<div>
<div>Description</div>
<div class="accent-color font-medium">{{ selected?.description }}</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button type="button" color="primary" mat-button (click)="apply()" [disabled]="isCurrent(selected)">
Apply
</button>
</mat-dialog-actions>
</form>
</div>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,86 @@
/*
* 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 { ChangeComponentVersionDialog } from './change-component-version-dialog';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatFormFieldModule } from '@angular/material/form-field';
import { OpenChangeComponentVersionDialogRequest } from '../../../state/shared';
describe('ChangeComponentVersionDialog', () => {
let component: ChangeComponentVersionDialog;
let fixture: ComponentFixture<ChangeComponentVersionDialog>;
const data: OpenChangeComponentVersionDialogRequest = {
fetchRequest: {
id: 'd3acc2a0-018e-1000-e4c1-1fd32cb579b6',
uri: 'https://localhost:4200/nifi-api/processors/d3acc2a0-018e-1000-e4c1-1fd32cb579b6',
revision: {
clientId: 'e23146d1-018e-1000-9d09-6a3c09c72420',
version: 8
},
type: 'org.apache.nifi.processors.standard.GenerateFlowFile',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-nar',
version: '2.0.0-M2'
}
},
componentVersions: [
{
type: 'org.apache.nifi.processors.standard.GenerateFlowFile',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-nar',
version: '2.0.0-SNAPSHOT'
},
description:
'This processor creates FlowFiles with random data or custom content. GenerateFlowFile is useful for load testing, configuration, and simulation. Also see DuplicateFlowFile for additional load testing.',
restricted: false,
tags: ['random', 'test', 'load', 'generate']
},
{
type: 'org.apache.nifi.processors.standard.GenerateFlowFile',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-nar',
version: '2.0.0-M2'
},
description:
'This processor creates FlowFiles with random data or custom content. GenerateFlowFile is useful for load testing, configuration, and simulation. Also see DuplicateFlowFile for additional load testing.',
restricted: false,
tags: ['random', 'test', 'load', 'generate']
}
]
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChangeComponentVersionDialog, MatDialogModule, NoopAnimationsModule, MatFormFieldModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
}).compileComponents();
fixture = TestBed.createComponent(ChangeComponentVersionDialog);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Inject, Output } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatButton } from '@angular/material/button';
import { Bundle, DocumentedType, OpenChangeComponentVersionDialogRequest } from '../../../state/shared';
import { MatFormField, MatLabel, MatOption, MatSelect } from '@angular/material/select';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { TextTip } from '../tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ControllerServiceApi } from '../controller-service/controller-service-api/controller-service-api.component';
@Component({
selector: 'change-component-version-dialog',
standalone: true,
imports: [
MatDialogModule,
MatButton,
MatSelect,
MatLabel,
MatOption,
MatFormField,
ReactiveFormsModule,
NifiTooltipDirective,
ControllerServiceApi
],
templateUrl: './change-component-version-dialog.html',
styleUrl: './change-component-version-dialog.scss'
})
export class ChangeComponentVersionDialog {
versions: DocumentedType[];
selected: DocumentedType | null = null;
changeComponentVersionForm: FormGroup;
private currentBundle: Bundle;
@Output() changeVersion: EventEmitter<DocumentedType> = new EventEmitter<DocumentedType>();
constructor(
@Inject(MAT_DIALOG_DATA) private dialogRequest: OpenChangeComponentVersionDialogRequest,
private formBuilder: FormBuilder,
private nifiCommon: NiFiCommon
) {
this.versions = dialogRequest.componentVersions;
this.currentBundle = dialogRequest.fetchRequest.bundle;
const idx = this.versions.findIndex(
(version: DocumentedType) => version.bundle.version === this.currentBundle.version
);
this.selected = this.versions[idx > 0 ? idx : 0];
this.changeComponentVersionForm = this.formBuilder.group({
bundle: new FormControl(this.selected, [Validators.required])
});
}
apply(): void {
if (this.selected) {
this.changeVersion.next(this.selected);
}
}
isCurrent(selection: DocumentedType | null): boolean {
return selection?.bundle.version === this.currentBundle.version;
}
getName(selected: DocumentedType | null): string {
return this.nifiCommon.substringAfterLast(selected?.type || '', '.');
}
protected readonly TextTip = TextTip;
}

View File

@ -150,7 +150,10 @@
title="Enable"></div>
}
@if (canChangeVersion(item)) {
<div class="pointer fa fa-exchange primary-color" title="Change Version"></div>
<div
class="pointer fa fa-exchange primary-color"
(click)="changeVersionClicked(item)"
title="Change Version"></div>
}
@if (canDelete(item)) {
<div

View File

@ -85,6 +85,8 @@ export class ControllerServiceTable {
new EventEmitter<ControllerServiceEntity>();
@Output() viewStateControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() changeControllerServiceVersion: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
protected readonly TextTip = TextTip;
protected readonly BulletinsTip = BulletinsTip;
@ -266,6 +268,10 @@ export class ControllerServiceTable {
this.deleteControllerService.next(entity);
}
changeVersionClicked(entity: ControllerServiceEntity) {
this.changeControllerServiceVersion.next(entity);
}
canViewState(entity: ControllerServiceEntity): boolean {
return this.canRead(entity) && this.canWrite(entity) && entity.component.persistsState === true;
}

View File

@ -159,8 +159,20 @@ export class ExtensionCreation {
}
isSelected(documentedType: DocumentedType): boolean {
if (this.selectedType) {
return documentedType.type == this.selectedType.type;
return this.areDocumentedTypesTheSame(this.selectedType, documentedType);
}
private areDocumentedTypesTheSame(type1: DocumentedType | null, type2: DocumentedType | null): boolean {
if (type1 == type2) {
return true;
}
if (type1 && type2) {
return (
type1.type === type2.type &&
type1.bundle.version === type2.bundle.version &&
type1.bundle.group === type2.bundle.group &&
type1.bundle.artifact === type2.bundle.artifact
);
}
return false;
}
@ -197,8 +209,8 @@ export class ExtensionCreation {
private selectRow(offset: number) {
if (this.selectedType && this.dataSource.filteredData.length > 0) {
// find the index of the currently selected row
const selectedIndex = this.dataSource.filteredData.findIndex(
(data) => data.type === this.selectedType?.type
const selectedIndex = this.dataSource.filteredData.findIndex((data) =>
this.areDocumentedTypesTheSame(this.selectedType, data)
);
if (selectedIndex > -1) {