NIFI-13127 Add branching support to Registry dialogs

This closes #8817

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Rob Fellows 2024-05-10 16:50:23 -04:00 committed by exceptionfactory
parent bffacdec98
commit 0c9ef91507
No known key found for this signature in database
11 changed files with 414 additions and 173 deletions

View File

@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { ImportFromRegistryRequest } from '../state/flow';
@Injectable({ providedIn: 'root' })
@ -30,17 +30,36 @@ export class RegistryService {
return this.httpClient.get(`${RegistryService.API}/flow/registries`);
}
getBuckets(registryId: string): Observable<any> {
return this.httpClient.get(`${RegistryService.API}/flow/registries/${registryId}/buckets`);
getBranches(registryId: string): Observable<any> {
return this.httpClient.get(`${RegistryService.API}/flow/registries/${registryId}/branches`);
}
getFlows(registryId: string, bucketId: string): Observable<any> {
return this.httpClient.get(`${RegistryService.API}/flow/registries/${registryId}/buckets/${bucketId}/flows`);
getBuckets(registryId: string, branch?: string): Observable<any> {
const params: HttpParams = new HttpParams();
if (branch) {
params.set('branch', branch);
}
return this.httpClient.get(`${RegistryService.API}/flow/registries/${registryId}/buckets`, { params });
}
getFlowVersions(registryId: string, bucketId: string, flowId: string): Observable<any> {
getFlows(registryId: string, bucketId: string, branch?: string): Observable<any> {
const params: HttpParams = new HttpParams();
if (branch) {
params.set('branch', branch);
}
return this.httpClient.get(`${RegistryService.API}/flow/registries/${registryId}/buckets/${bucketId}/flows`, {
params
});
}
getFlowVersions(registryId: string, bucketId: string, flowId: string, branch?: string): Observable<any> {
const params: HttpParams = new HttpParams();
if (branch) {
params.set('branch', branch);
}
return this.httpClient.get(
`${RegistryService.API}/flow/registries/${registryId}/buckets/${bucketId}/flows/${flowId}/versions`
`${RegistryService.API}/flow/registries/${registryId}/buckets/${bucketId}/flows/${flowId}/versions`,
{ params }
);
}

View File

@ -86,6 +86,7 @@ import { MatDialog } from '@angular/material/dialog';
import { CreatePort } from '../../ui/canvas/items/port/create-port/create-port.component';
import { EditPort } from '../../ui/canvas/items/port/edit-port/edit-port.component';
import {
BranchEntity,
BucketEntity,
ComponentType,
isDefinedAndNotNull,
@ -809,10 +810,29 @@ export class FlowEffects {
data: request
});
dialogReference.componentInstance.getBuckets = (
dialogReference.componentInstance.getBranches = (
registryId: string
): Observable<BranchEntity[]> => {
return this.registryService.getBranches(registryId).pipe(
take(1),
map((response) => response.branches),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
})
);
}
})
);
};
dialogReference.componentInstance.getBuckets = (
registryId: string,
branch?: string
): Observable<BucketEntity[]> => {
return this.registryService.getBuckets(registryId).pipe(
return this.registryService.getBuckets(registryId, branch).pipe(
take(1),
map((response) => response.buckets),
tap({
@ -829,9 +849,10 @@ export class FlowEffects {
dialogReference.componentInstance.getFlows = (
registryId: string,
bucketId: string
bucketId: string,
branch?: string
): Observable<VersionedFlowEntity[]> => {
return this.registryService.getFlows(registryId, bucketId).pipe(
return this.registryService.getFlows(registryId, bucketId, branch).pipe(
take(1),
map((response) => response.versionedFlows),
tap({
@ -849,9 +870,10 @@ export class FlowEffects {
dialogReference.componentInstance.getFlowVersions = (
registryId: string,
bucketId: string,
flowId: string
flowId: string,
branch?: string
): Observable<VersionedFlowSnapshotMetadataEntity[]> => {
return this.registryService.getFlowVersions(registryId, bucketId, flowId).pipe(
return this.registryService.getFlowVersions(registryId, bucketId, flowId, branch).pipe(
take(1),
map((response) => response.versionedFlowSnapshotMetadataSet),
tap({
@ -3192,8 +3214,29 @@ export class FlowEffects {
data: request
});
dialogReference.componentInstance.getBuckets = (registryId: string): Observable<BucketEntity[]> => {
return this.registryService.getBuckets(registryId).pipe(
dialogReference.componentInstance.getBranches = (
registryId: string
): Observable<BranchEntity[]> => {
return this.registryService.getBranches(registryId).pipe(
take(1),
map((response) => response.branches),
tap({
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
})
);
}
})
);
};
dialogReference.componentInstance.getBuckets = (
registryId: string,
branch?: string
): Observable<BucketEntity[]> => {
return this.registryService.getBuckets(registryId, branch).pipe(
take(1),
map((response) => response.buckets),
tap({

View File

@ -238,6 +238,7 @@ export interface SaveVersionRequest {
flowDescription?: string;
comments?: string;
existingFlowId?: string;
branch?: string;
}
export interface VersionControlInformation {
@ -253,6 +254,7 @@ export interface VersionControlInformation {
storageLocation?: string;
state: string;
stateExplanation: string;
branch?: string;
}
export interface VersionControlInformationEntity {

View File

@ -15,7 +15,9 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Change Version</h2>
<h2 mat-dialog-title>
Change Version <span class="font-light">({{ versionControlInformation.version }})</span>
</h2>
<div class="change-version">
<mat-dialog-content>
<div class="dialog-content flex flex-col gap-y-4 w-full">
@ -26,18 +28,20 @@
<div class="accent-color font-medium">{{ versionControlInformation.registryName }}</div>
</div>
<div>
<div>Flow Name</div>
<div class="accent-color font-medium">{{ versionControlInformation.flowName }}</div>
<div>Bucket</div>
<div class="accent-color font-medium">{{ versionControlInformation.bucketName }}</div>
</div>
</div>
<div class="flex flex-1 flex-col gap-y-4">
<div>
<div>Bucket</div>
<div class="accent-color font-medium">{{ versionControlInformation.bucketName }}</div>
@if (versionControlInformation.branch) {
<div>Branch</div>
<div class="accent-color font-medium">{{ versionControlInformation.branch }}</div>
}
</div>
<div>
<div>Current Version</div>
<div class="accent-color font-medium">{{ versionControlInformation.version }}</div>
<div>Flow Name</div>
<div class="accent-color font-medium">{{ versionControlInformation.flowName }}</div>
</div>
</div>
</div>
@ -61,7 +65,11 @@
<!-- Version Column -->
<ng-container matColumnDef="version">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Version</th>
<td mat-cell *matCellDef="let item">{{ item.version }}</td>
<td mat-cell *matCellDef="let item">
<div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.version">
{{ item.version }}
</div>
</td>
</ng-container>
<!-- Create Column -->
@ -73,7 +81,13 @@
<!-- Comments Column -->
<ng-container matColumnDef="comments">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Comments</th>
<td mat-cell *matCellDef="let item">{{ item.comments }}</td>
<td mat-cell *matCellDef="let item">
<div
class="overflow-ellipsis overflow-hidden whitespace-nowrap"
[title]="item.comments">
{{ item.comments }}
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>

View File

@ -22,14 +22,12 @@
.listing-table {
table {
table-layout: unset;
.mat-column-version {
width: 75px;
width: 180px;
}
.mat-column-created {
width: 200px;
width: 180px;
}
}
}

View File

@ -19,138 +19,163 @@
<form class="import-from-registry-form" [formGroup]="importFromRegistryForm">
<error-banner></error-banner>
<mat-dialog-content>
<mat-form-field>
<mat-label>Registry</mat-label>
<mat-select formControlName="registry" (selectionChange)="registryChanged($event.value)">
<ng-container *ngFor="let option of registryClientOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
<div class="dialog-content flex flex-col h-full gap-y-2">
<div>
<mat-form-field>
<mat-label>Registry</mat-label>
<mat-select formControlName="registry" (selectionChange)="registryChanged($event.value)">
<ng-container *ngFor="let option of registryClientOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="option.description"
[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>
</mat-form-field>
@if (supportsBranching) {
<mat-form-field>
<mat-label>Branch</mat-label>
<mat-select formControlName="branch" (selectionChange)="branchChanged($event.value)">
<ng-container *ngFor="let option of branchOptions">
<mat-option [value]="option.value">{{ option.text }}</mat-option>
</ng-container>
</mat-select>
@if (importFromRegistryForm.controls['branch'].hasError('required')) {
<mat-error>No branches available</mat-error>
}
</mat-form-field>
}
<mat-form-field>
<mat-label>Bucket</mat-label>
<mat-select formControlName="bucket" (selectionChange)="bucketChanged($event.value)">
<ng-container *ngFor="let option of bucketOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="option.description"
[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>
<mat-error *ngIf="importFromRegistryForm.controls['bucket'].hasError('required')"
>No buckets available</mat-error
>
</mat-form-field>
<mat-form-field>
<mat-label>Flow</mat-label>
<mat-select formControlName="flow" (selectionChange)="flowChanged($event.value)">
<ng-container *ngFor="let option of flowOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="option.description"
[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>
<mat-error *ngIf="importFromRegistryForm.controls['flow'].hasError('required')"
>No flows available</mat-error
>
</mat-form-field>
<div class="mb-5">
<mat-checkbox color="primary" formControlName="keepParameterContexts">
Keep existing Parameter Contexts
<i
class="fa fa-info-circle"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="option.description"
[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>
</mat-form-field>
<mat-form-field>
<mat-label>Bucket</mat-label>
<mat-select formControlName="bucket" (selectionChange)="bucketChanged($event.value)">
<ng-container *ngFor="let option of bucketOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="option.description"
[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>
<mat-error *ngIf="importFromRegistryForm.controls['bucket'].hasError('required')"
>No buckets available</mat-error
>
</mat-form-field>
<mat-form-field>
<mat-label>Flow</mat-label>
<mat-select formControlName="flow" (selectionChange)="flowChanged($event.value)">
<ng-container *ngFor="let option of flowOptions">
<ng-container *ngIf="option.description; else noDescription">
<mat-option
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="option.description"
[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>
<mat-error *ngIf="importFromRegistryForm.controls['flow'].hasError('required')"
>No flows available</mat-error
>
</mat-form-field>
<div class="mb-5">
<mat-checkbox color="primary" formControlName="keepParameterContexts">
Keep existing Parameter Contexts
<i
class="fa fa-info-circle"
nifiTooltip
[tooltipComponentType]="TextTip"
tooltipInputData="When not selected, only directly associated Parameter Contexts will be copied, inherited Contexts with no direct assignment to a Process Group are ignored."></i>
</mat-checkbox>
</div>
<div class="flex flex-col mb-5">
<div>Flow Description</div>
<div class="accent-color font-medium">
{{ selectedFlowDescription || 'No description provided' }}
tooltipInputData="When not selected, only directly associated Parameter Contexts will be copied, inherited Contexts with no direct assignment to a Process Group are ignored."></i>
</mat-checkbox>
</div>
<div class="flex flex-col mb-5">
<div>Flow Description</div>
<div class="accent-color font-medium">
{{ selectedFlowDescription || 'No description provided' }}
</div>
</div>
</div>
</div>
<div class="listing-table">
<div class="h-48 overflow-y-auto overflow-x-hidden">
<table
mat-table
[dataSource]="dataSource"
matSort
matSortDisableClear
(matSortChange)="sortData($event)"
[matSortActive]="sort.active"
[matSortDirection]="sort.direction">
<!-- Version Column -->
<ng-container matColumnDef="version">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Version</th>
<td mat-cell *matCellDef="let item">
{{ item.version }}
</td>
</ng-container>
<div class="listing-table flex-1 relative min-h-48">
<div class="absolute inset-0 overflow-y-auto overflow-x-hidden">
<table
mat-table
[dataSource]="dataSource"
matSort
matSortDisableClear
(matSortChange)="sortData($event)"
[matSortActive]="sort.active"
[matSortDirection]="sort.direction">
<!-- Version Column -->
<ng-container matColumnDef="version">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Version</th>
<td mat-cell *matCellDef="let item">
<div class="overflow-ellipsis overflow-hidden whitespace-nowrap" [title]="item.version">
{{ item.version }}
</div>
</td>
</ng-container>
<!-- Create Column -->
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Created</th>
<td mat-cell *matCellDef="let item">
{{ formatTimestamp(item) }}
</td>
</ng-container>
<!-- Create Column -->
<ng-container matColumnDef="created">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Created</th>
<td mat-cell *matCellDef="let item">
{{ formatTimestamp(item) }}
</td>
</ng-container>
<!-- Comments Column -->
<ng-container matColumnDef="comments">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Comments</th>
<td mat-cell *matCellDef="let item">
{{ item.comments }}
</td>
</ng-container>
<!-- Comments Column -->
<ng-container matColumnDef="comments">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Comments</th>
<td mat-cell *matCellDef="let item">
<div
class="overflow-ellipsis overflow-hidden whitespace-nowrap"
[title]="item.comments">
{{ item.comments }}
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; let even = even; columns: displayedColumns"
(click)="select(row)"
(dblclick)="importFromRegistry()"
[class.selected]="isSelected(row)"
[class.even]="even"></tr>
</table>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; let even = even; columns: displayedColumns"
(click)="select(row)"
(dblclick)="importFromRegistry()"
[class.selected]="isSelected(row)"
[class.even]="even"></tr>
</table>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="importFromRegistryForm.invalid || saving.value"
[disabled]="importFromRegistryForm.invalid || saving.value || !selectedFlowVersion"
type="button"
color="primary"
(click)="importFromRegistry()"

View File

@ -30,14 +30,12 @@
.listing-table {
table {
table-layout: unset;
.mat-column-version {
width: 75px;
width: 150px;
}
.mat-column-created {
width: 200px;
width: 180px;
}
}
}

View File

@ -21,6 +21,7 @@ import { ImportFromRegistryDialogRequest } from '../../../../../state/flow';
import { Store } from '@ngrx/store';
import { CanvasState } from '../../../../../state';
import {
BranchEntity,
BucketEntity,
isDefinedAndNotNull,
RegistryClientEntity,
@ -43,7 +44,7 @@ import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators }
import { TextTip } from '../../../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { MatIconModule } from '@angular/material/icon';
import { Observable, take } from 'rxjs';
import { Observable, of, take } from 'rxjs';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
@ -82,12 +83,14 @@ import { ClusterConnectionService } from '../../../../../../../service/cluster-c
styleUrls: ['./import-from-registry.component.scss']
})
export class ImportFromRegistry implements OnInit {
@Input() getBuckets!: (registryId: string) => Observable<BucketEntity[]>;
@Input() getFlows!: (registryId: string, bucketId: string) => Observable<VersionedFlowEntity[]>;
@Input() getBranches: (registryId: string) => Observable<BranchEntity[]> = () => of([]);
@Input() getBuckets!: (registryId: string, branch?: string) => Observable<BucketEntity[]>;
@Input() getFlows!: (registryId: string, bucketId: string, branch?: string) => Observable<VersionedFlowEntity[]>;
@Input() getFlowVersions!: (
registryId: string,
bucketId: string,
flowId: string
flowId: string,
branch?: string
) => Observable<VersionedFlowSnapshotMetadataEntity[]>;
saving$ = this.store.select(selectSaving);
@ -97,11 +100,14 @@ export class ImportFromRegistry implements OnInit {
importFromRegistryForm: FormGroup;
registryClientOptions: SelectOption[] = [];
branchOptions: SelectOption[] = [];
bucketOptions: SelectOption[] = [];
flowOptions: SelectOption[] = [];
flowLookup: Map<string, VersionedFlow> = new Map<string, VersionedFlow>();
selectedFlowDescription: string | undefined;
supportsBranching = false;
private clientBranchingSupportMap: Map<string, boolean> = new Map<string, boolean>();
sort: Sort = {
active: 'created',
@ -139,10 +145,12 @@ export class ImportFromRegistry implements OnInit {
description: registryClient.component.description
});
}
this.clientBranchingSupportMap.set(registryClient.id, registryClient.component.supportsBranching);
});
this.importFromRegistryForm = this.formBuilder.group({
registry: new FormControl(this.registryClientOptions[0].value, Validators.required),
branch: new FormControl('default', Validators.required),
bucket: new FormControl(null, Validators.required),
flow: new FormControl(null, Validators.required),
keepParameterContexts: new FormControl(true, Validators.required)
@ -153,13 +161,36 @@ export class ImportFromRegistry implements OnInit {
const selectedRegistryId = this.importFromRegistryForm.get('registry')?.value;
if (selectedRegistryId) {
this.loadBuckets(selectedRegistryId);
this.supportsBranching = this.clientBranchingSupportMap.get(selectedRegistryId) || false;
if (this.supportsBranching) {
this.loadBranches(selectedRegistryId);
} else {
this.loadBuckets(selectedRegistryId);
}
}
}
registryChanged(registryId: string): void {
this.supportsBranching = this.clientBranchingSupportMap.get(registryId) || false;
if (this.supportsBranching) {
this.clearBranches();
this.loadBranches(registryId);
} else {
this.clearBuckets();
this.loadBuckets(registryId);
}
}
private clearBranches(): void {
this.branchOptions = [];
this.importFromRegistryForm.get('branch')?.setValue('default');
this.clearBuckets();
this.loadBuckets(registryId);
}
branchChanged(branch: string): void {
this.clearBuckets();
const registryId = this.importFromRegistryForm.get('registry')?.value;
this.loadBuckets(registryId, branch);
}
private clearBuckets(): void {
@ -186,10 +217,35 @@ export class ImportFromRegistry implements OnInit {
this.loadVersions(registryId, bucketId, flowId);
}
loadBuckets(registryId: string): void {
loadBranches(registryId: string): void {
if (registryId) {
this.branchOptions = [];
this.getBranches(registryId)
.pipe(take(1))
.subscribe((branches: BranchEntity[]) => {
if (branches.length > 0) {
branches.forEach((entity: BranchEntity) => {
this.branchOptions.push({
text: entity.branch.name,
value: entity.branch.name
});
});
const branchId = this.branchOptions[0].value;
if (branchId) {
this.importFromRegistryForm.get('branch')?.setValue(branchId);
this.loadBuckets(registryId, branchId);
}
}
});
}
}
loadBuckets(registryId: string, branch?: string): void {
this.bucketOptions = [];
this.getBuckets(registryId)
this.getBuckets(registryId, branch)
.pipe(take(1))
.subscribe((buckets: BucketEntity[]) => {
if (buckets.length > 0) {
@ -206,17 +262,17 @@ export class ImportFromRegistry implements OnInit {
const bucketId = this.bucketOptions[0].value;
if (bucketId) {
this.importFromRegistryForm.get('bucket')?.setValue(bucketId);
this.loadFlows(registryId, bucketId);
this.loadFlows(registryId, bucketId, branch);
}
}
});
}
loadFlows(registryId: string, bucketId: string): void {
loadFlows(registryId: string, bucketId: string, branch?: string): void {
this.flowOptions = [];
this.flowLookup.clear();
this.getFlows(registryId, bucketId)
this.getFlows(registryId, bucketId, branch)
.pipe(take(1))
.subscribe((versionedFlows: VersionedFlowEntity[]) => {
if (versionedFlows.length > 0) {
@ -233,17 +289,17 @@ export class ImportFromRegistry implements OnInit {
const flowId = this.flowOptions[0].value;
if (flowId) {
this.importFromRegistryForm.get('flow')?.setValue(flowId);
this.loadVersions(registryId, bucketId, flowId);
this.loadVersions(registryId, bucketId, flowId, branch);
}
}
});
}
loadVersions(registryId: string, bucketId: string, flowId: string): void {
loadVersions(registryId: string, bucketId: string, flowId: string, branch?: string): void {
this.dataSource.data = [];
this.selectedFlowDescription = this.flowLookup.get(flowId)?.description;
this.getFlowVersions(registryId, bucketId, flowId)
this.getFlowVersions(registryId, bucketId, flowId, branch)
.pipe(take(1))
.subscribe((metadataEntities: VersionedFlowSnapshotMetadataEntity[]) => {
if (metadataEntities.length > 0) {
@ -340,6 +396,10 @@ export class ImportFromRegistry implements OnInit {
}
};
if (this.supportsBranching) {
payload.component.versionControlInformation.branch = this.importFromRegistryForm.get('branch')?.value;
}
this.store.dispatch(
importFromRegistry({
request: {

View File

@ -25,6 +25,12 @@
<div>Registry</div>
<div class="accent-color font-medium">{{ versionControlInformation.registryName }}</div>
</div>
@if (versionControlInformation.branch) {
<div>
<div>Branch</div>
<div class="accent-color font-medium">{{ versionControlInformation.branch }}</div>
</div>
}
<div>
<div>Bucket</div>
<div class="accent-color font-medium">{{ versionControlInformation.bucketName }}</div>
@ -65,6 +71,20 @@
</mat-select>
</mat-form-field>
@if (supportsBranching) {
<mat-form-field>
<mat-label>Branch</mat-label>
<mat-select formControlName="branch" (selectionChange)="branchChanged($event.value)">
<ng-container *ngFor="let option of branchOptions">
<mat-option [value]="option.value">{{ option.text }}</mat-option>
</ng-container>
</mat-select>
@if (saveVersionForm.controls['branch'].hasError('required')) {
<mat-error>No branches available</mat-error>
}
</mat-form-field>
}
<mat-form-field>
<mat-label>Bucket</mat-label>
<mat-select formControlName="bucket">

View File

@ -30,7 +30,7 @@ import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nif
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 } from '../../../../../../../state/shared';
import { BranchEntity, BucketEntity, RegistryClientEntity, SelectOption } 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';
@ -64,16 +64,21 @@ import { MatInput } from '@angular/material/input';
styleUrl: './save-version-dialog.component.scss'
})
export class SaveVersionDialog implements OnInit {
@Input() getBuckets: (registryId: string) => Observable<BucketEntity[]> = () => of([]);
@Input() getBranches: (registryId: string) => Observable<BranchEntity[]> = () => of([]);
@Input() getBuckets: (registryId: string, branch?: string) => Observable<BucketEntity[]> = () => of([]);
@Input({ required: true }) saving!: Signal<boolean>;
@Output() save: EventEmitter<SaveVersionRequest> = new EventEmitter<SaveVersionRequest>();
saveVersionForm: FormGroup;
registryClientOptions: SelectOption[] = [];
branchOptions: SelectOption[] = [];
bucketOptions: SelectOption[] = [];
versionControlInformation?: VersionControlInformation;
forceCommit = false;
supportsBranching = false;
private clientBranchingSupportMap: Map<string, boolean> = new Map<string, boolean>();
constructor(
@Inject(MAT_DIALOG_DATA) private dialogRequest: SaveVersionDialogRequest,
@ -96,10 +101,12 @@ export class SaveVersionDialog implements OnInit {
description: registryClient.component.description
});
}
this.clientBranchingSupportMap.set(registryClient.id, registryClient.component.supportsBranching);
});
this.saveVersionForm = formBuilder.group({
registry: new FormControl(this.registryClientOptions[0].value, Validators.required),
branch: new FormControl('default', Validators.required),
bucket: new FormControl(null, Validators.required),
flowName: new FormControl(null, Validators.required),
flowDescription: new FormControl(null),
@ -117,16 +124,46 @@ export class SaveVersionDialog implements OnInit {
const selectedRegistryId: string | null = this.saveVersionForm.get('registry')?.value;
if (selectedRegistryId) {
this.loadBuckets(selectedRegistryId);
this.supportsBranching = this.clientBranchingSupportMap.get(selectedRegistryId) || false;
if (this.supportsBranching) {
this.loadBranches(selectedRegistryId);
} else {
this.loadBuckets(selectedRegistryId);
}
}
}
}
loadBuckets(registryId: string): void {
loadBranches(registryId: string): void {
if (registryId) {
this.branchOptions = [];
this.getBranches(registryId)
.pipe(take(1))
.subscribe((branches: BranchEntity[]) => {
if (branches.length > 0) {
branches.forEach((entity: BranchEntity) => {
this.branchOptions.push({
text: entity.branch.name,
value: entity.branch.name
});
});
const branchId = this.branchOptions[0].value;
if (branchId) {
this.saveVersionForm.get('branch')?.setValue(branchId);
this.loadBuckets(registryId, branchId);
}
}
});
}
}
loadBuckets(registryId: string, branch?: string): void {
if (registryId) {
this.bucketOptions = [];
this.getBuckets(registryId)
this.getBuckets(registryId, branch)
.pipe(take(1))
.subscribe((buckets: BucketEntity[]) => {
if (buckets.length > 0) {
@ -150,7 +187,17 @@ export class SaveVersionDialog implements OnInit {
}
registryChanged(registryId: string): void {
this.loadBuckets(registryId);
this.supportsBranching = this.clientBranchingSupportMap.get(registryId) || false;
if (this.supportsBranching) {
this.loadBranches(registryId);
} else {
this.loadBuckets(registryId);
}
}
branchChanged(branch: string): void {
const registryId = this.saveVersionForm.get('registry')?.value;
this.loadBuckets(registryId, branch);
}
submitForm() {
@ -165,7 +212,8 @@ export class SaveVersionDialog implements OnInit {
bucket: vci.bucketId,
comments: this.saveVersionForm.get('comments')?.value,
flowDescription: vci.flowDescription,
flowName: vci.flowName
flowName: vci.flowName,
branch: vci.branch
};
} else {
request = {
@ -177,6 +225,9 @@ export class SaveVersionDialog implements OnInit {
flowDescription: this.saveVersionForm.get('flowDescription')?.value,
flowName: this.saveVersionForm.get('flowName')?.value
};
if (this.supportsBranching) {
request.branch = this.saveVersionForm.get('branch')?.value;
}
}
this.save.next(request);
}

View File

@ -566,6 +566,16 @@ export interface Bucket {
name: string;
}
export interface BranchEntity {
id: string;
permissions: Permissions;
branch: Branch;
}
export interface Branch {
name: string;
}
export interface VersionedFlowEntity {
versionedFlow: VersionedFlow;
}
@ -602,6 +612,7 @@ export interface VersionedFlowSnapshotMetadata {
timestamp: number;
author: string;
comments: string;
branch?: string;
}
export interface SelectOption {