mirror of
https://github.com/apache/nifi.git
synced 2025-02-12 04:55:19 +00:00
[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:
parent
84df025ccf
commit
307c4017d9
@ -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
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
@ -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 }>()
|
||||
);
|
||||
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user