[NIFI-12963] Process Group Versioning (#8596)

* [NIFI-12963] - Flow Versioning
* Start version control

* Stop version control

* Commit local changes

* Force commit local changes

This closes #8596
This commit is contained in:
Rob Fellows 2024-04-04 10:39:18 -04:00 committed by GitHub
parent 84df025ccf
commit 307c4017d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1246 additions and 52 deletions

View File

@ -36,6 +36,9 @@ import {
navigateToProvenanceForComponent,
navigateToQueueListing,
navigateToViewStatusHistoryForComponent,
openCommitLocalChangesDialogRequest,
openForceCommitLocalChangesDialogRequest,
openSaveVersionDialogRequest,
reloadFlow,
replayLastProvenanceEvent,
requestRefreshRemoteProcessGroup,
@ -43,14 +46,16 @@ import {
startComponents,
startCurrentProcessGroup,
stopComponents,
stopCurrentProcessGroup
stopCurrentProcessGroup,
stopVersionControlRequest
} from '../state/flow/flow.actions';
import { ComponentType } from '../../../state/shared';
import {
DeleteComponentRequest,
MoveComponentRequest,
StartComponentRequest,
StopComponentRequest
StopComponentRequest,
StopVersionControlRequest
} from '../state/flow';
import {
ContextMenuDefinition,
@ -67,45 +72,78 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
id: 'version',
menuItems: [
{
condition: (selection: any) => {
// TODO - supportsStartFlowVersioning
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.supportsStartFlowVersioning(selection);
},
clazz: 'fa fa-upload',
text: 'Start version control',
action: () => {
// TODO - saveFlowVersion
action: (selection: d3.Selection<any, any, any, any>) => {
let pgId;
if (selection.empty()) {
pgId = this.canvasUtils.getProcessGroupId();
} else {
pgId = selection.datum().id;
}
this.store.dispatch(
openSaveVersionDialogRequest({
request: {
processGroupId: pgId
}
})
);
}
},
{
isSeparator: true
},
{
condition: (selection: any) => {
// TODO - supportsCommitFlowVersion
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.supportsCommitFlowVersion(selection);
},
clazz: 'fa fa-upload',
text: 'Commit local changes',
action: () => {
// TODO - saveFlowVersion
action: (selection: d3.Selection<any, any, any, any>) => {
let pgId;
if (selection.empty()) {
pgId = this.canvasUtils.getProcessGroupId();
} else {
pgId = selection.datum().id;
}
this.store.dispatch(
openCommitLocalChangesDialogRequest({
request: {
processGroupId: pgId
}
})
);
}
},
{
condition: (selection: any) => {
// TODO - supportsForceCommitFlowVersion
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.supportsForceCommitFlowVersion(selection);
},
clazz: 'fa fa-upload',
text: 'Commit local changes',
action: () => {
// TODO - forceSaveFlowVersion
action: (selection: d3.Selection<any, any, any, any>) => {
let pgId;
if (selection.empty()) {
pgId = this.canvasUtils.getProcessGroupId();
} else {
pgId = selection.datum().id;
}
this.store.dispatch(
openForceCommitLocalChangesDialogRequest({
request: {
processGroupId: pgId,
forceCommit: true
}
})
);
}
},
{
condition: (selection: any) => {
// TODO - hasLocalChanges
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.hasLocalChanges(selection);
},
clazz: 'fa',
text: 'Show local changes',
@ -114,9 +152,8 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}
},
{
condition: (selection: any) => {
// TODO - hasLocalChanges
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.hasLocalChanges(selection);
},
clazz: 'fa fa-undo',
text: 'Revert local changes',
@ -125,9 +162,8 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}
},
{
condition: (selection: any) => {
// TODO - supportsChangeFlowVersion
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.supportsChangeFlowVersion(selection);
},
clazz: 'fa',
text: 'Change version',
@ -139,14 +175,18 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
isSeparator: true
},
{
condition: (selection: any) => {
// TODO - supportsStopFlowVersioning
return false;
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.supportsStopFlowVersioning(selection);
},
clazz: 'fa',
text: 'Stop version control',
action: () => {
// TODO - stopVersionControl
action: (selection: d3.Selection<any, any, any, any>) => {
const selectionData = selection.datum();
const request: StopVersionControlRequest = {
revision: selectionData.revision,
processGroupId: selectionData.id
};
this.store.dispatch(stopVersionControlRequest({ request }));
}
}
]
@ -377,7 +417,9 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
isSeparator: true
},
{
clazz: 'fa',
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.supportsFlowVersioning(selection);
},
text: 'Version',
subMenuId: this.VERSION_MENU.id
},

View File

@ -21,6 +21,7 @@ import { humanizer, Humanizer } from 'humanize-duration';
import { Store } from '@ngrx/store';
import { CanvasState } from '../state';
import {
selectBreadcrumbs,
selectCanvasPermissions,
selectConnections,
selectCurrentProcessGroupId,
@ -29,7 +30,7 @@ import {
import { initialState as initialFlowState } from '../state/flow/flow.reducer';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
import { Position } from '../state/shared';
import { BreadcrumbEntity, Position } from '../state/shared';
import { ComponentType, Permissions } from '../../../state/shared';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { CurrentUser } from '../../../state/current-user';
@ -38,6 +39,7 @@ import { selectCurrentUser } from '../../../state/current-user/current-user.sele
import { FlowConfiguration } from '../../../state/flow-configuration';
import { initialState as initialFlowConfigurationState } from '../../../state/flow-configuration/flow-configuration.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
import { VersionControlInformation } from '../state/flow';
@Injectable({
providedIn: 'root'
@ -54,6 +56,7 @@ export class CanvasUtils {
private currentUser: CurrentUser = initialUserState.user;
private flowConfiguration: FlowConfiguration | null = initialFlowConfigurationState.flowConfiguration;
private connections: any[] = [];
private breadcrumbs: BreadcrumbEntity | null = null;
private readonly humanizeDuration: Humanizer;
@ -105,6 +108,13 @@ export class CanvasUtils {
.subscribe((flowConfiguration) => {
this.flowConfiguration = flowConfiguration;
});
this.store
.select(selectBreadcrumbs)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((breadcrumbs) => {
this.breadcrumbs = breadcrumbs;
});
}
public hasDownstream(selection: any): boolean {
@ -1523,4 +1533,165 @@ export class CanvasUtils {
return selectionSize === writableSize;
}
/**
* Determines whether the current selection supports starting flow versioning.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection supports starting flow versioning
*/
public supportsStartFlowVersioning(selection: d3.Selection<any, any, any, any>): boolean {
if (!this.supportsFlowVersioning(selection)) {
return false;
}
if (selection.empty()) {
// check bread crumbs for version control information in the current group
if (this.breadcrumbs) {
if (this.breadcrumbs.permissions.canRead) {
return !this.breadcrumbs.breadcrumb.versionControlInformation;
}
return false;
}
}
// check the selection for version control information
const pgData = selection.datum();
return !pgData.component.versionControlInformation;
}
/**
* Determines whether the current selection supports flow versioning.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection supports flow versioning
*/
public supportsFlowVersioning(selection: d3.Selection<any, any, any, any>): boolean {
if (!this.canVersionFlows()) {
return false;
}
if (selection.empty()) {
// prevent versioning of the root group
if (!this.getParentProcessGroupId()) {
return false;
}
// if not root group, ensure adequate permissions
return this.canvasPermissions.canRead && this.canvasPermissions.canWrite;
}
if (this.isProcessGroup(selection)) {
return this.canRead(selection) && this.canModify(selection);
}
return false;
}
/**
* Returns whether the process group support supports commit.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection supports commit.
*/
public supportsCommitFlowVersion(selection: d3.Selection<any, any, any, any>): boolean {
const versionControlInformation = this.getFlowVersionControlInformation(selection);
// check the selection for version control information
return versionControlInformation !== null && versionControlInformation.state === 'LOCALLY_MODIFIED';
}
/**
* Returns whether the process group support supports force commit.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection supports force commit.
*/
public supportsForceCommitFlowVersion(selection: d3.Selection<any, any, any, any>): boolean {
const versionControlInformation = this.getFlowVersionControlInformation(selection);
// check the selection for version control information
return versionControlInformation !== null && versionControlInformation.state === 'LOCALLY_MODIFIED_AND_STALE';
}
/**
* Returns whether the process group supports revert local changes.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection has local changes.
*/
public hasLocalChanges(selection: d3.Selection<any, any, any, any>): boolean {
const versionControlInformation = this.getFlowVersionControlInformation(selection);
// check the selection for version control information
return (
versionControlInformation !== null &&
(versionControlInformation.state === 'LOCALLY_MODIFIED' ||
versionControlInformation.state === 'LOCALLY_MODIFIED_AND_STALE')
);
}
/**
* Returns whether the process group supports changing the flow version.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection supports change flow version.
*/
public supportsChangeFlowVersion(selection: d3.Selection<any, any, any, any>): boolean {
const versionControlInformation = this.getFlowVersionControlInformation(selection);
return (
versionControlInformation !== null &&
versionControlInformation.state !== 'LOCALLY_MODIFIED' &&
versionControlInformation.state !== 'LOCALLY_MODIFIED_AND_STALE' &&
versionControlInformation.state !== 'SYNC_FAILURE'
);
}
/**
* Determines whether the current selection supports stopping flow versioning.
*
* @argument {d3.Selection} selection The selection
* @return {boolean} Whether the selection supports stopping flow versioning.
*/
public supportsStopFlowVersioning(selection: d3.Selection<any, any, any, any>): boolean {
const versionControlInformation = this.getFlowVersionControlInformation(selection);
return versionControlInformation !== null;
}
/**
* Determines whether the current user can version flows.
*/
public canVersionFlows(): boolean {
return this.currentUser.canVersionFlows;
}
/**
* Convenience function to perform all flow versioning pre-checks and retrieve
* valid version information.
*
* @argument {d3.Selection} selection The selection
*/
public getFlowVersionControlInformation(
selection: d3.Selection<any, any, any, any>
): VersionControlInformation | null {
if (!this.supportsFlowVersioning(selection)) {
return null;
}
if (selection.empty()) {
// check bread crumbs for version control information in the current group
if (this.breadcrumbs) {
if (this.breadcrumbs.permissions.canRead) {
return this.breadcrumbs.breadcrumb.versionControlInformation || null;
}
}
return null;
} else {
// check the selection for version control information
const pgData = selection.datum();
return pgData.component.versionControlInformation || null;
}
}
}

View File

@ -31,13 +31,16 @@ import {
ProcessGroupRunStatusRequest,
ReplayLastProvenanceEventRequest,
RunOnceRequest,
SaveToVersionControlRequest,
Snippet,
StartComponentRequest,
StartProcessGroupRequest,
StopComponentRequest,
StopProcessGroupRequest,
StopVersionControlRequest,
UpdateComponentRequest,
UploadProcessGroupRequest
UploadProcessGroupRequest,
VersionControlInformationEntity
} from '../state/flow';
import { ComponentType, PropertyDescriptorRetriever } from '../../../state/shared';
import { Client } from '../../../service/client.service';
@ -323,4 +326,33 @@ export class FlowService implements PropertyDescriptorRetriever {
stopRequest
);
}
getVersionInformation(processGroupId: string): Observable<VersionControlInformationEntity> {
return this.httpClient.get(
`${FlowService.API}/versions/process-groups/${processGroupId}`
) as Observable<VersionControlInformationEntity>;
}
saveToFlowRegistry(request: SaveToVersionControlRequest): Observable<VersionControlInformationEntity> {
const saveRequest = {
...request,
disconnectedNodeAcknowledged: false
};
return this.httpClient.post(
`${FlowService.API}/versions/process-groups/${request.processGroupId}`,
saveRequest
) as Observable<VersionControlInformationEntity>;
}
stopVersionControl(request: StopVersionControlRequest): Observable<VersionControlInformationEntity> {
const params: any = {
version: request.revision.version,
clientId: request.revision.clientId,
disconnectedNodeAcknowledged: false
};
return this.httpClient.delete(`${FlowService.API}/versions/process-groups/${request.processGroupId}`, {
params
}) as Observable<VersionControlInformationEntity>;
}
}

View File

@ -17,6 +17,7 @@
import { createAction, props } from '@ngrx/store';
import {
CenterComponentRequest,
ComponentEntity,
CreateComponentRequest,
CreateComponentResponse,
@ -24,19 +25,23 @@ import {
CreateConnectionDialogRequest,
CreateConnectionRequest,
CreatePortRequest,
CreateRemoteProcessGroupRequest,
CreateProcessGroupDialogRequest,
CreateProcessGroupRequest,
CreateProcessorRequest,
CreateRemoteProcessGroupRequest,
DeleteComponentRequest,
DeleteComponentResponse,
EditComponentDialogRequest,
EditConnectionDialogRequest,
EditCurrentProcessGroupRequest,
EnterProcessGroupRequest,
GoToRemoteProcessGroupRequest,
GroupComponentsDialogRequest,
GroupComponentsRequest,
GroupComponentsSuccess,
ImportFromRegistryDialogRequest,
ImportFromRegistryRequest,
LoadChildProcessGroupRequest,
LoadConnectionSuccess,
LoadInputPortSuccess,
LoadProcessGroupRequest,
@ -47,21 +52,30 @@ import {
NavigateToComponentRequest,
NavigateToControllerServicesRequest,
NavigateToManageComponentPoliciesRequest,
NavigateToQueueListing,
OpenComponentDialogRequest,
OpenGroupComponentsDialogRequest,
LoadChildProcessGroupRequest,
OpenSaveVersionDialogRequest,
RefreshRemoteProcessGroupRequest,
ReplayLastProvenanceEventRequest,
RpgManageRemotePortsRequest,
RunOnceRequest,
RunOnceResponse,
SaveToVersionControlRequest,
SaveVersionDialogRequest,
SelectComponentsRequest,
StartComponentRequest,
StartComponentResponse,
StartComponentsRequest,
StartProcessGroupRequest,
StartProcessGroupResponse,
StopComponentRequest,
StopComponentResponse,
StopComponentsRequest,
StopProcessGroupRequest,
StopProcessGroupResponse,
StopVersionControlRequest,
StopVersionControlResponse,
UpdateComponentFailure,
UpdateComponentRequest,
UpdateComponentResponse,
@ -69,15 +83,7 @@ import {
UpdateConnectionSuccess,
UpdatePositionsRequest,
UploadProcessGroupRequest,
NavigateToQueueListing,
StartProcessGroupResponse,
StopProcessGroupResponse,
CenterComponentRequest,
ImportFromRegistryDialogRequest,
ImportFromRegistryRequest,
GoToRemoteProcessGroupRequest,
RefreshRemoteProcessGroupRequest,
RpgManageRemotePortsRequest
VersionControlInformationEntity
} from './index';
import { StatusHistoryRequest } from '../../../../state/status-history';
@ -597,3 +603,53 @@ export const stopProcessGroupSuccess = createAction(
export const startCurrentProcessGroup = createAction(`${CANVAS_PREFIX} Start Current Process Group`);
export const stopCurrentProcessGroup = createAction(`${CANVAS_PREFIX} Stop Current Process Group`);
export const openSaveVersionDialogRequest = createAction(
`${CANVAS_PREFIX} Open Save Flow Version Dialog Request`,
props<{ request: OpenSaveVersionDialogRequest }>()
);
export const openCommitLocalChangesDialogRequest = createAction(
`${CANVAS_PREFIX} Open Commit Local Changes Dialog Request`,
props<{ request: OpenSaveVersionDialogRequest }>()
);
export const openForceCommitLocalChangesDialogRequest = createAction(
`${CANVAS_PREFIX} Open Force Commit Local Changes Dialog Request`,
props<{ request: OpenSaveVersionDialogRequest }>()
);
export const openSaveVersionDialog = createAction(
`${CANVAS_PREFIX} Open Save Flow Version Dialog`,
props<{ request: SaveVersionDialogRequest }>()
);
export const saveToFlowRegistry = createAction(
`${CANVAS_PREFIX} Save To Version Control`,
props<{ request: SaveToVersionControlRequest }>()
);
export const saveToFlowRegistrySuccess = createAction(
`${CANVAS_PREFIX} Save To Version Control Success`,
props<{ response: VersionControlInformationEntity }>()
);
export const flowVersionBannerError = createAction(
`${CANVAS_PREFIX} Flow Version Banner Error`,
props<{ error: string }>()
);
export const stopVersionControlRequest = createAction(
`${CANVAS_PREFIX} Stop Version Control Request`,
props<{ request: StopVersionControlRequest }>()
);
export const stopVersionControl = createAction(
`${CANVAS_PREFIX} Stop Version Control`,
props<{ request: StopVersionControlRequest }>()
);
export const stopVersionControlSuccess = createAction(
`${CANVAS_PREFIX} Stop Version Control Success`,
props<{ response: StopVersionControlResponse }>()
);

View File

@ -44,7 +44,10 @@ import {
ImportFromRegistryDialogRequest,
LoadProcessGroupRequest,
LoadProcessGroupResponse,
SaveVersionDialogRequest,
SaveVersionRequest,
Snippet,
StopVersionControlResponse,
UpdateComponentFailure,
UpdateComponentResponse,
UpdateConnectionSuccess,
@ -58,9 +61,10 @@ import {
selectParentProcessGroupId,
selectProcessGroup,
selectProcessor,
selectRemoteProcessGroup,
selectRefreshRpgDetails,
selectSaving
selectRemoteProcessGroup,
selectSaving,
selectVersionSaving
} from './flow.selectors';
import { ConnectionManager } from '../../service/manager/connection-manager.service';
import { MatDialog } from '@angular/material/dialog';
@ -101,6 +105,7 @@ import { NoRegistryClientsDialog } from '../../ui/common/no-registry-clients-dia
import { EditRemoteProcessGroup } from '../../ui/canvas/items/remote-process-group/edit-remote-process-group/edit-remote-process-group.component';
import { LARGE_DIALOG, MEDIUM_DIALOG, SMALL_DIALOG } from '../../../../index';
import { HttpErrorResponse } from '@angular/common/http';
import { SaveVersionDialog } from '../../ui/canvas/items/flow/save-version-dialog/save-version-dialog.component';
@Injectable()
export class FlowEffects {
@ -2499,4 +2504,244 @@ export class FlowEffects {
})
)
);
openSaveVersionDialogRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.openSaveVersionDialogRequest),
map((action) => action.request),
switchMap((request) => {
return combineLatest([
this.registryService.getRegistryClients(),
this.flowService.getVersionInformation(request.processGroupId)
]).pipe(
map(([registryClients, versionInfo]) => {
const dialogRequest: SaveVersionDialogRequest = {
processGroupId: request.processGroupId,
revision: versionInfo.processGroupRevision,
registryClients: registryClients.registries
};
return FlowActions.openSaveVersionDialog({ request: dialogRequest });
}),
catchError((error) => of(FlowActions.flowApiError({ error: error.error })))
);
})
)
);
openSaveVersionDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.openSaveVersionDialog),
map((action) => action.request),
tap((request) => {
const dialogReference = this.dialog.open(SaveVersionDialog, {
...MEDIUM_DIALOG,
data: request
});
dialogReference.componentInstance.getBuckets = (registryId: string): Observable<BucketEntity[]> => {
return this.registryService.getBuckets(registryId).pipe(
take(1),
map((response) => response.buckets)
);
};
dialogReference.componentInstance.saving = this.store.selectSignal(selectVersionSaving);
dialogReference.componentInstance.save
.pipe(takeUntil(dialogReference.afterClosed()))
.subscribe((saveRequest: SaveVersionRequest) => {
if (saveRequest.existingFlowId) {
this.store.dispatch(
FlowActions.saveToFlowRegistry({
request: {
versionedFlow: {
action: request.forceCommit ? 'FORCE_COMMIT' : 'COMMIT',
flowId: saveRequest.existingFlowId,
bucketId: saveRequest.bucket,
registryId: saveRequest.registry,
comments: saveRequest.comments || ''
},
processGroupId: saveRequest.processGroupId,
processGroupRevision: saveRequest.revision
}
})
);
} else {
this.store.dispatch(
FlowActions.saveToFlowRegistry({
request: {
versionedFlow: {
action: 'COMMIT',
bucketId: saveRequest.bucket,
registryId: saveRequest.registry,
flowName: saveRequest.flowName,
description: saveRequest.flowDescription || '',
comments: saveRequest.comments || ''
},
processGroupId: saveRequest.processGroupId,
processGroupRevision: saveRequest.revision
}
})
);
}
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
});
})
),
{ dispatch: false }
);
saveToFlowRegistry$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.saveToFlowRegistry),
map((action) => action.request),
switchMap((request) => {
return from(this.flowService.saveToFlowRegistry(request)).pipe(
map((response) => {
return FlowActions.saveToFlowRegistrySuccess({ response });
}),
catchError((error) => of(FlowActions.flowVersionBannerError({ error: error.error })))
);
})
)
);
saveToFlowRegistrySuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.saveToFlowRegistrySuccess),
tap(() => {
this.dialog.closeAll();
}),
switchMap(() => of(FlowActions.reloadFlow()))
)
);
flowVersionBannerError$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.flowVersionBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
)
);
stopVersionControlRequest$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.stopVersionControlRequest),
map((action) => action.request),
tap((request) => {
const dialogRef = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
data: {
title: 'Stop Version Control',
message: `Are you sure you want to stop version control?`
}
});
dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
this.store.dispatch(FlowActions.stopVersionControl({ request }));
});
dialogRef.componentInstance.no.pipe(take(1)).subscribe(() => {
dialogRef.close();
});
})
),
{ dispatch: false }
);
stopVersionControl$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.stopVersionControl),
map((action) => action.request),
switchMap((request) =>
from(this.flowService.stopVersionControl(request)).pipe(
map((response) => {
const stopResponse: StopVersionControlResponse = {
processGroupRevision: response.processGroupRevision,
processGroupId: request.processGroupId
};
return FlowActions.stopVersionControlSuccess({ response: stopResponse });
}),
catchError((errorResponse) => of(ErrorActions.snackBarError({ error: errorResponse.error })))
)
)
)
);
stopVersionControlSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.stopVersionControlSuccess),
tap(() => {
this.store.dispatch(
FlowActions.showOkDialog({
title: 'Disconnect',
message: 'This Process Group is no longer under version control.'
})
);
}),
switchMap(() => of(FlowActions.reloadFlow()))
)
);
openCommitLocalChangesDialogRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.openCommitLocalChangesDialogRequest),
map((action) => action.request),
switchMap((request) => {
return from(this.flowService.getVersionInformation(request.processGroupId)).pipe(
map((response) => {
const dialogRequest: SaveVersionDialogRequest = {
processGroupId: request.processGroupId,
revision: response.processGroupRevision,
versionControlInformation: response.versionControlInformation,
forceCommit: request.forceCommit
};
return FlowActions.openSaveVersionDialog({ request: dialogRequest });
}),
catchError((error) => of(FlowActions.flowApiError({ error: error.error })))
);
})
)
);
openForceCommitLocalChangesDialogRequest$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.openForceCommitLocalChangesDialogRequest),
map((action) => action.request),
tap((request) => {
const dialogRef = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
data: {
title: 'Commit',
message:
'Committing will ignore available upgrades and commit local changes as the next version. Are you sure you want to proceed?'
}
});
dialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
this.store.dispatch(
FlowActions.openCommitLocalChangesDialogRequest({
request: {
...request,
forceCommit: true
}
})
);
});
dialogRef.componentInstance.no.pipe(take(1)).subscribe(() => {
dialogRef.close();
});
})
),
{ dispatch: false }
);
}

View File

@ -28,6 +28,7 @@ import {
createProcessor,
deleteComponentsSuccess,
flowApiError,
flowVersionBannerError,
groupComponents,
groupComponentsSuccess,
loadChildProcessGroupSuccess,
@ -42,6 +43,8 @@ import {
resetFlowState,
runOnce,
runOnceSuccess,
saveToFlowRegistry,
saveToFlowRegistrySuccess,
setAllowTransition,
setDragging,
setNavigationCollapsed,
@ -53,6 +56,8 @@ import {
startRemoteProcessGroupPolling,
stopComponentSuccess,
stopRemoteProcessGroupPolling,
stopVersionControl,
stopVersionControlSuccess,
updateComponent,
updateComponentFailure,
updateComponentSuccess,
@ -133,6 +138,7 @@ export const initialState: FlowState = {
},
dragging: false,
saving: false,
versionSaving: false,
transitionRequired: false,
skipTransform: false,
allowTransition: false,
@ -382,7 +388,47 @@ export const flowReducer = createReducer(
draftState.saving = false;
});
})
}),
on(saveToFlowRegistry, stopVersionControl, (state) => ({
...state,
versionSaving: true
})),
on(saveToFlowRegistrySuccess, (state, { response }) => {
return produce(state, (draftState) => {
const collection: any[] | null = getComponentCollection(draftState, ComponentType.ProcessGroup);
if (collection) {
const componentIndex: number = collection.findIndex(
(f: any) => response.versionControlInformation?.groupId === f.id
);
if (componentIndex > -1) {
collection[componentIndex].revision = response.processGroupRevision;
collection[componentIndex].versionedFlowState = response.versionControlInformation?.state;
}
}
draftState.versionSaving = false;
});
}),
on(stopVersionControlSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const collection: any[] | null = getComponentCollection(draftState, ComponentType.ProcessGroup);
if (collection) {
const componentIndex: number = collection.findIndex((f: any) => response.processGroupId === f.id);
if (componentIndex > -1) {
collection[componentIndex].revision = response.processGroupRevision;
collection[componentIndex].versionedFlowState = null;
}
}
draftState.versionSaving = false;
});
}),
on(flowVersionBannerError, (state) => ({
...state,
versionSaving: false
}))
);
function getComponentCollection(draftState: FlowState, componentType: ComponentType): any[] | null {

View File

@ -30,6 +30,8 @@ export const selectApiError = createSelector(selectFlowState, (state: FlowState)
export const selectSaving = createSelector(selectFlowState, (state: FlowState) => state.saving);
export const selectVersionSaving = createSelector(selectFlowState, (state: FlowState) => state.versionSaving);
export const selectCurrentProcessGroupId = createSelector(selectFlowState, (state: FlowState) => state.id);
export const selectRefreshRpgDetails = createSelector(selectFlowState, (state: FlowState) => state.refreshRpgDetails);

View File

@ -25,7 +25,8 @@ import {
Permissions,
RegistryClientEntity,
Revision,
SelectOption
SelectOption,
SparseVersionedFlow
} from '../../../../state/shared';
import { ParameterContextEntity } from '../../../parameter-contexts/state/parameter-context-listing';
@ -179,6 +180,67 @@ export interface ImportFromRegistryRequest {
keepExistingParameterContext: boolean;
}
export interface OpenSaveVersionDialogRequest {
processGroupId: string;
forceCommit?: boolean;
}
export interface SaveVersionDialogRequest {
processGroupId: string;
revision: Revision;
registryClients?: RegistryClientEntity[];
versionControlInformation?: VersionControlInformation;
forceCommit?: boolean;
}
export interface SaveToVersionControlRequest {
processGroupId: string;
versionedFlow: SparseVersionedFlow;
processGroupRevision: Revision;
}
export interface StopVersionControlRequest {
revision: Revision;
processGroupId: string;
}
export interface StopVersionControlResponse {
processGroupId: string;
processGroupRevision: Revision;
}
export interface SaveVersionRequest {
processGroupId: string;
registry: string;
bucket: string;
flowName: string;
revision: Revision;
flowDescription?: string;
comments?: string;
existingFlowId?: string;
}
export interface VersionControlInformation {
groupId: string;
registryId: string;
registryName: string;
bucketId: string;
bucketName: string;
flowId: string;
flowName: string;
flowDescription: string;
version: number;
storageLocation: string;
state: string;
stateExplanation: string;
}
export interface VersionControlInformationEntity {
processGroupRevision: Revision;
versionControlInformation?: VersionControlInformation;
disconnectedNodeAcknowledged?: boolean;
}
export interface OpenGroupComponentsDialogRequest {
position: Position;
moveComponents: MoveComponentRequest[];
@ -522,6 +584,7 @@ export interface FlowState {
navigationCollapsed: boolean;
operationCollapsed: boolean;
error: string | null;
versionSaving: boolean;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -213,11 +213,11 @@ export class ImportFromRegistry implements OnInit {
.subscribe((versionedFlows: VersionedFlowEntity[]) => {
if (versionedFlows.length > 0) {
versionedFlows.forEach((entity: VersionedFlowEntity) => {
this.flowLookup.set(entity.versionedFlow.flowId, entity.versionedFlow);
this.flowLookup.set(entity.versionedFlow.flowId!, entity.versionedFlow);
this.flowOptions.push({
text: entity.versionedFlow.flowName,
value: entity.versionedFlow.flowId,
value: entity.versionedFlow.flowId!,
description: entity.versionedFlow.description
});
});

View File

@ -0,0 +1,37 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use 'sass:map';
@use '@angular/material' as mat;
@use '../../../../../../../../assets/utils.scss' as utils;
@mixin nifi-theme($theme) {
// Get the color config from the theme.
$color-config: mat.get-color-config($theme);
// Get the color palette from the color-config.
$primary-palette: map.get($color-config, 'primary');
// Get hues from palette
$primary-palette-default: mat.get-color-from-palette($primary-palette, default);
$primary-palette-default-contrast: mat.get-color-from-palette($primary-palette, default-contrast);
.save-flow-version-label {
background-color: $primary-palette-default;
color: $primary-palette-default-contrast;
}
}

View File

@ -0,0 +1,126 @@
<!--
~ 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>Save Flow Version</h2>
<form class="save-version-form" [formGroup]="saveVersionForm">
<error-banner></error-banner>
<mat-dialog-content>
@if (versionControlInformation) {
<div class="flex flex-col gap-y-4 mb-6">
<div>
<div>Registry</div>
<div class="value">{{ versionControlInformation.registryName }}</div>
</div>
<div>
<div>Bucket</div>
<div class="value">{{ versionControlInformation.bucketName }}</div>
</div>
<div class="flex">
<div class="flex-1">
<div>Flow Name</div>
<div class="value">{{ versionControlInformation.flowName }}</div>
</div>
@if (!forceCommit) {
<div class="save-flow-version-label ml-3">{{ versionControlInformation.version + 1 }}</div>
}
</div>
<div>
<div>Flow Description</div>
@if (versionControlInformation.flowDescription === '') {
<div class="unset">Empty string set</div>
} @else {
<div class="value">{{ versionControlInformation.flowDescription }}</div>
}
</div>
</div>
} @else {
<mat-form-field>
<mat-label>Registry</mat-label>
<mat-select formControlName="registry" (selectionChange)="registryChanged($event.value)">
@for (option of registryClientOptions; track option) {
@if (option.description) {
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
[delayClose]="false"
>{{ option.text }}
</mat-option>
} @else {
<mat-option [value]="option.value">{{ option.text }}</mat-option>
}
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Bucket</mat-label>
<mat-select formControlName="bucket">
<ng-container *ngFor="let option of bucketOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
[delayClose]="false"
>{{ option.text }}
</mat-option>
</ng-container>
<ng-template #noDescription>
<mat-option [value]="option.value">{{ option.text }}</mat-option>
</ng-template>
</ng-container>
</mat-select>
@if (saveVersionForm.controls['bucket'].hasError('required')) {
<mat-error>No buckets available</mat-error>
}
</mat-form-field>
<div class="flex w-full">
<mat-form-field class="flex-1">
<mat-label>Flow Name</mat-label>
<input matInput formControlName="flowName" type="text" />
</mat-form-field>
<div class="save-flow-version-label ml-3">1</div>
</div>
<mat-form-field>
<mat-label>Flow Description</mat-label>
<textarea matInput formControlName="flowDescription" type="text"></textarea>
</mat-form-field>
}
<mat-form-field>
<mat-label>Version Comments</mat-label>
<textarea matInput formControlName="comments" type="text"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="saveVersionForm.invalid || saving()"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving()">Save</span>
</button>
</mat-dialog-actions>
</form>

View File

@ -0,0 +1,38 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use '@angular/material' as mat;
.save-version-form {
@include mat.button-density(-1);
.mat-mdc-form-field {
width: 100%;
}
.mat-mdc-form-field-error {
font-size: 12px;
}
.save-flow-version-label {
flex: 0 0 44px;
height: 44px;
line-height: 44px;
text-align: center;
border-radius: 50%;
padding: 0 1px;
}
}

View File

@ -0,0 +1,132 @@
/*
* 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 { SaveVersionDialog } from './save-version-dialog.component';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { SaveVersionDialogRequest } from '../../../../../state/flow';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { EMPTY } from 'rxjs';
import { Signal } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SaveVersionDialog', () => {
let component: SaveVersionDialog;
let fixture: ComponentFixture<SaveVersionDialog>;
const data: SaveVersionDialogRequest = {
processGroupId: '5752a5ae-018d-1000-0990-c3709f5466f3',
revision: {
version: 0
},
registryClients: [
{
revision: {
version: 0
},
id: '80441509-018e-1000-12b2-d70361a7f661',
uri: 'https://localhost:4200/nifi-api/controller/registry-clients/80441509-018e-1000-12b2-d70361a7f661',
permissions: {
canRead: true,
canWrite: true
},
component: {
id: '80441509-018e-1000-12b2-d70361a7f661',
name: 'Local Registry',
description: '',
type: 'org.apache.nifi.registry.flow.NifiRegistryFlowRegistryClient',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-flow-registry-client-nar',
version: '2.0.0-SNAPSHOT'
},
properties: {
url: 'http://localhost:18080/nifi-registry',
'ssl-context-service': null
},
descriptors: {
url: {
name: 'url',
displayName: 'URL',
description: 'URL of the NiFi Registry',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'ssl-context-service': {
name: 'ssl-context-service',
displayName: 'SSL Context Service',
description: 'Specifies the SSL Context Service to use for communicating with NiFiRegistry',
allowableValues: [
{
allowableValue: {
displayName: 'StandardSSLContextService',
value: '5c272e23-018d-1000-72ef-f31b82cda378'
},
canRead: true
}
],
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.ssl.SSLContextService',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: []
}
},
supportsSensitiveDynamicProperties: false,
restricted: false,
deprecated: false,
validationStatus: 'VALID',
multipleVersionsAvailable: false,
extensionMissing: false
}
}
]
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SaveVersionDialog, MatDialogModule, NoopAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
}).compileComponents();
fixture = TestBed.createComponent(SaveVersionDialog);
component = fixture.componentInstance;
component.getBuckets = () => {
return EMPTY;
};
component.saving = (() => false) as Signal<boolean>;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,191 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Inject, Input, OnInit, Output, Signal } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogActions,
MatDialogClose,
MatDialogContent,
MatDialogTitle
} from '@angular/material/dialog';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { MatButton } from '@angular/material/button';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { Observable, of, take } from 'rxjs';
import { BucketEntity, RegistryClientEntity, SelectOption, TextTipInput } from '../../../../../../../state/shared';
import { NiFiCommon } from '../../../../../../../service/nifi-common.service';
import { SaveVersionDialogRequest, SaveVersionRequest, VersionControlInformation } from '../../../../../state/flow';
import { TextTip } from '../../../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { NgForOf, NgIf } from '@angular/common';
import { MatInput } from '@angular/material/input';
@Component({
selector: 'save-version-dialog',
standalone: true,
imports: [
MatDialogTitle,
ReactiveFormsModule,
ErrorBanner,
MatDialogContent,
MatDialogActions,
MatButton,
MatDialogClose,
NifiSpinnerDirective,
MatFormField,
MatSelect,
MatOption,
NifiTooltipDirective,
MatError,
MatLabel,
NgForOf,
NgIf,
MatInput
],
templateUrl: './save-version-dialog.component.html',
styleUrl: './save-version-dialog.component.scss'
})
export class SaveVersionDialog implements OnInit {
@Input() getBuckets: (registryId: string) => Observable<BucketEntity[]> = () => of([]);
@Input({ required: true }) saving!: Signal<boolean>;
@Output() save: EventEmitter<SaveVersionRequest> = new EventEmitter<SaveVersionRequest>();
saveVersionForm: FormGroup;
registryClientOptions: SelectOption[] = [];
bucketOptions: SelectOption[] = [];
versionControlInformation?: VersionControlInformation;
forceCommit = false;
constructor(
@Inject(MAT_DIALOG_DATA) private dialogRequest: SaveVersionDialogRequest,
private formBuilder: FormBuilder,
private nifiCommon: NiFiCommon
) {
this.versionControlInformation = dialogRequest.versionControlInformation;
this.forceCommit = !!dialogRequest.forceCommit;
if (dialogRequest.registryClients) {
const sortedRegistries = dialogRequest.registryClients.slice().sort((a, b) => {
return this.nifiCommon.compareString(a.component.name, b.component.name);
});
sortedRegistries.forEach((registryClient: RegistryClientEntity) => {
if (registryClient.permissions.canRead) {
this.registryClientOptions.push({
text: registryClient.component.name,
value: registryClient.id,
description: registryClient.component.description
});
}
});
this.saveVersionForm = formBuilder.group({
registry: new FormControl(this.registryClientOptions[0].value, Validators.required),
bucket: new FormControl(null, Validators.required),
flowName: new FormControl(null, Validators.required),
flowDescription: new FormControl(null),
comments: new FormControl(null)
});
} else {
this.saveVersionForm = formBuilder.group({
comments: new FormControl('')
});
}
}
ngOnInit(): void {
if (this.dialogRequest.registryClients) {
const selectedRegistryId: string | null = this.saveVersionForm.get('registry')?.value;
if (selectedRegistryId) {
this.loadBuckets(selectedRegistryId);
}
}
}
loadBuckets(registryId: string): void {
if (registryId) {
this.bucketOptions = [];
this.getBuckets(registryId)
.pipe(take(1))
.subscribe((buckets: BucketEntity[]) => {
if (buckets.length > 0) {
buckets.forEach((entity: BucketEntity) => {
if (entity.permissions.canRead) {
this.bucketOptions.push({
text: entity.bucket.name,
value: entity.id,
description: entity.bucket.description
});
}
});
const bucketId = this.bucketOptions[0].value;
if (bucketId) {
this.saveVersionForm.get('bucket')?.setValue(bucketId);
}
}
});
}
}
getSelectOptionTipData(option: SelectOption): TextTipInput {
return {
text: option.description || ''
};
}
registryChanged(registryId: string): void {
this.loadBuckets(registryId);
}
submitForm() {
let request: SaveVersionRequest;
const vci = this.versionControlInformation;
if (vci) {
request = {
existingFlowId: vci.flowId,
processGroupId: this.dialogRequest.processGroupId,
revision: this.dialogRequest.revision,
registry: vci.registryId,
bucket: vci.bucketId,
comments: this.saveVersionForm.get('comments')?.value,
flowDescription: vci.flowDescription,
flowName: vci.flowName
};
} else {
request = {
processGroupId: this.dialogRequest.processGroupId,
revision: this.dialogRequest.revision,
registry: this.saveVersionForm.get('registry')?.value,
bucket: this.saveVersionForm.get('bucket')?.value,
comments: this.saveVersionForm.get('comments')?.value,
flowDescription: this.saveVersionForm.get('flowDescription')?.value,
flowName: this.saveVersionForm.get('flowName')?.value
};
}
this.save.next(request);
}
protected readonly TextTip = TextTip;
}

View File

@ -512,13 +512,23 @@ export interface VersionedFlowEntity {
export interface VersionedFlow {
registryId: string;
bucketId: string;
flowId: string;
flowId?: string;
flowName: string;
description: string;
comments: string;
action: string;
}
export interface SparseVersionedFlow {
registryId: string;
bucketId: string;
action: string;
comments?: string;
flowId?: string;
flowName?: string;
description?: string;
}
export interface VersionedFlowSnapshotMetadataEntity {
registryId: string;
versionedFlowSnapshotMetadata: VersionedFlowSnapshotMetadata;

View File

@ -40,6 +40,7 @@
@use 'app/pages/flow-designer/ui/canvas/items/connection/prioritizers/prioritizers.component-theme' as prioritizers;
@use 'app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component-theme' as create-process-group;
@use 'app/pages/flow-designer/ui/canvas/items/remote-process-group/create-remote-process-group/create-remote-process-group.component-theme' as create-remote-process-group;
@use 'app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component-theme' as save-version-dialog;
@use 'app/pages/flow-designer/ui/common/banner/banner.component-theme' as banner;
@use 'app/pages/flow-designer/ui/controller-service/controller-services.component-theme' as controller-service;
@use 'app/pages/flow-designer/ui/manage-remote-ports/manage-remote-ports.component-theme' as manage-remote-ports;
@ -118,6 +119,7 @@
@include new-canvas-item.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include search.nifi-theme($nifi-canvas-theme-light);
@include prioritizers.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@include save-version-dialog.nifi-theme($material-theme-light);
@include create-process-group.nifi-theme($material-theme-light);
@include create-remote-process-group.nifi-theme($material-theme-light);
@include login.nifi-theme($material-theme-light, $nifi-canvas-theme-light);
@ -194,4 +196,5 @@
@include provenance-event-dialog.nifi-theme($material-theme-dark);
@include processor-status-table.nifi-theme($material-theme-dark);
@include component-context.nifi-theme($material-theme-dark, $nifi-canvas-theme-dark);
@include save-version-dialog.nifi-theme($material-theme-dark);
}