diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts index d7e091bd0c..781b0e4160 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-context-menu.service.ts @@ -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) => { + return this.canvasUtils.supportsStartFlowVersioning(selection); }, clazz: 'fa fa-upload', text: 'Start version control', - action: () => { - // TODO - saveFlowVersion + action: (selection: d3.Selection) => { + 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) => { + return this.canvasUtils.supportsCommitFlowVersion(selection); }, clazz: 'fa fa-upload', text: 'Commit local changes', - action: () => { - // TODO - saveFlowVersion + action: (selection: d3.Selection) => { + 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) => { + return this.canvasUtils.supportsForceCommitFlowVersion(selection); }, clazz: 'fa fa-upload', text: 'Commit local changes', - action: () => { - // TODO - forceSaveFlowVersion + action: (selection: d3.Selection) => { + 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) => { + 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) => { + 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) => { + 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) => { + return this.canvasUtils.supportsStopFlowVersioning(selection); }, clazz: 'fa', text: 'Stop version control', - action: () => { - // TODO - stopVersionControl + action: (selection: d3.Selection) => { + 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) => { + return this.canvasUtils.supportsFlowVersioning(selection); + }, text: 'Version', subMenuId: this.VERSION_MENU.id }, diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts index 0084298d3f..bf064649e5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts @@ -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): 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): 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): 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): 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): 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): 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): 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 + ): 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; + } + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts index fe23575a33..f5460185c4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts @@ -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 { + return this.httpClient.get( + `${FlowService.API}/versions/process-groups/${processGroupId}` + ) as Observable; + } + + saveToFlowRegistry(request: SaveToVersionControlRequest): Observable { + const saveRequest = { + ...request, + disconnectedNodeAcknowledged: false + }; + + return this.httpClient.post( + `${FlowService.API}/versions/process-groups/${request.processGroupId}`, + saveRequest + ) as Observable; + } + + stopVersionControl(request: StopVersionControlRequest): Observable { + 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; + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts index f2276dca01..3828a8cf9f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts @@ -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 }>() +); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts index deb393b4b4..336bdf8516 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts @@ -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 => { + 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 } + ); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts index 9bc4344654..48fd23043c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts @@ -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 { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts index 06c3674c56..9b968a3ed4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts @@ -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); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts index 2d929ced3b..33ff0c4c82 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts @@ -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'; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts index 3496b0512f..f6935b4306 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts @@ -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 }); }); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/_save-version-dialog.component-theme.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/_save-version-dialog.component-theme.scss new file mode 100644 index 0000000000..b5a0451696 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/_save-version-dialog.component-theme.scss @@ -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; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.html new file mode 100644 index 0000000000..2ae727aaf5 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.html @@ -0,0 +1,126 @@ + + +

Save Flow Version

+
+ + + @if (versionControlInformation) { +
+
+
Registry
+
{{ versionControlInformation.registryName }}
+
+
+
Bucket
+
{{ versionControlInformation.bucketName }}
+
+
+
+
Flow Name
+
{{ versionControlInformation.flowName }}
+
+ @if (!forceCommit) { +
{{ versionControlInformation.version + 1 }}
+ } +
+
+
Flow Description
+ @if (versionControlInformation.flowDescription === '') { +
Empty string set
+ } @else { +
{{ versionControlInformation.flowDescription }}
+ } +
+
+ } @else { + + Registry + + @for (option of registryClientOptions; track option) { + @if (option.description) { + {{ option.text }} + + } @else { + {{ option.text }} + } + } + + + + + Bucket + + + + {{ option.text }} + + + + {{ option.text }} + + + + @if (saveVersionForm.controls['bucket'].hasError('required')) { + No buckets available + } + + +
+ + Flow Name + + +
1
+
+ + + Flow Description + + + } + + + Version Comments + + +
+ + + + + +
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.scss new file mode 100644 index 0000000000..b3e807f780 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.scss @@ -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; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.spec.ts new file mode 100644 index 0000000000..1ddff00509 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.spec.ts @@ -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; + + 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; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.ts new file mode 100644 index 0000000000..615ff5dfa2 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/save-version-dialog/save-version-dialog.component.ts @@ -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 = () => of([]); + @Input({ required: true }) saving!: Signal; + + @Output() save: EventEmitter = new EventEmitter(); + + 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; +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts index 6883d30a58..deaa963c67 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts @@ -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; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss index f596ec6899..8412bd2b01 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss @@ -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); }