This is an automated email from the ASF dual-hosted git repository.
mcgilman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new feb72efc23 [NIFI-14648] - Skeleton/Spinner in Import from Registry
dialog (#10005)
feb72efc23 is described below
commit feb72efc23d40416c68642ae9516b150f23d8faa
Author: Rob Fellows <[email protected]>
AuthorDate: Mon Jun 16 11:28:07 2025 -0400
[NIFI-14648] - Skeleton/Spinner in Import from Registry dialog (#10005)
---
.../import-from-registry.component.html | 111 ++++++++++++---------
.../import-from-registry.component.spec.ts | 33 ++++++
.../import-from-registry.component.ts | 88 ++++++++++++++--
3 files changed, 177 insertions(+), 55 deletions(-)
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
index 12801cea5e..9bd696f254 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
@@ -119,57 +119,74 @@
</div>
</div>
</div>
- <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>
+ @if (loadingVersions()) {
+ <div data-qa="skeleton-loader-versions" class="w-full">
+ <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+ </div>
+ } @else if (loadingVersionsError()) {
+ <div
+ class="flex flex-1 flex-col gap-y-4 justify-center
items-center tertiary-color pb-4"
+ data-qa="loading-versions-error">
+ <i class="fa fa-exclamation-circle error-color fa-4x"></i>
+ <div class="text-lg font-semibold">Something went
wrong</div>
+ <div class="text-center">{{ loadingVersionsError() }}</div>
+ </div>
+ } @else {
+ <div class="listing-table flex-1 relative min-h-48"
data-qa="versions-listing-table">
+ <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>
- <!-- 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>
+ <!-- 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">
+ <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>
+ }
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as
saving">
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
index 82c5fcc3a9..a5e7327107 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
@@ -26,6 +26,7 @@ import { initialState } from
'../../../../../state/flow/flow.reducer';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EMPTY } from 'rxjs';
import { ClusterConnectionService } from
'../../../../../../../service/cluster-connection.service';
+import { By } from '@angular/platform-browser';
describe('ImportFromRegistry', () => {
let component: ImportFromRegistry;
@@ -144,4 +145,36 @@ describe('ImportFromRegistry', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should show the skeleton loader for versions', () => {
+ component.loadingVersions.set(true);
+ fixture.detectChanges();
+ const skeleton =
fixture.debugElement.query(By.css('div[data-qa="skeleton-loader-versions"]'));
+ const error =
fixture.debugElement.query(By.css('div[data-qa="loading-versions-error"]'));
+ expect(skeleton).toBeTruthy();
+ expect(error).toBeFalsy();
+ });
+
+ it('should show the loading error panel if there is an error', () => {
+ component.loadingVersions.set(false);
+ component.loadingVersionsError.set('some error happened');
+ fixture.detectChanges();
+ const skeleton =
fixture.debugElement.query(By.css('div[data-qa="skeleton-loader-versions"]'));
+ const error =
fixture.debugElement.query(By.css('div[data-qa="loading-versions-error"]'));
+ const versions =
fixture.debugElement.query(By.css('div[data-qa="versions-listing-table"]'));
+ expect(skeleton).toBeFalsy();
+ expect(error).toBeTruthy();
+ expect(versions).toBeFalsy();
+ });
+
+ it('should show the versions', () => {
+ component.loadingVersions.set(false);
+ fixture.detectChanges();
+ const skeleton =
fixture.debugElement.query(By.css('div[data-qa="skeleton-loader-versions"]'));
+ const error =
fixture.debugElement.query(By.css('div[data-qa="loading-versions-error"]'));
+ const versions =
fixture.debugElement.query(By.css('div[data-qa="versions-listing-table"]'));
+ expect(skeleton).toBeFalsy();
+ expect(error).toBeFalsy();
+ expect(versions).toBeTruthy();
+ });
});
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
index 8952e8e70b..00167b19a5 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { Component, Inject, Input, OnInit } from '@angular/core';
+import { Component, Inject, Input, OnInit, signal, WritableSignal } from
'@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { ImportFromRegistryDialogRequest } from '../../../../../state/flow';
import { Store } from '@ngrx/store';
@@ -39,7 +39,7 @@ import { MatSelectModule } from '@angular/material/select';
import { NifiSpinnerDirective } from
'../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators
} from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
-import { Observable, of, take } from 'rxjs';
+import { catchError, EMPTY, 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';
@@ -58,6 +58,9 @@ import { importFromRegistry } from
'../../../../../state/flow/flow.actions';
import { ClusterConnectionService } from
'../../../../../../../service/cluster-connection.service';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from
'../../../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { ErrorHelper } from
'../../../../../../../service/error-helper.service';
+import { HttpErrorResponse } from '@angular/common/http';
+import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
@Component({
selector: 'import-from-registry',
@@ -78,7 +81,8 @@ import { ContextErrorBanner } from
'../../../../../../../ui/common/context-error
MatCheckboxModule,
MatSortModule,
MatTableModule,
- ContextErrorBanner
+ ContextErrorBanner,
+ NgxSkeletonLoaderComponent
],
templateUrl: './import-from-registry.component.html',
styleUrls: ['./import-from-registry.component.scss']
@@ -123,13 +127,20 @@ export class ImportFromRegistry extends
CloseOnEscapeDialog implements OnInit {
new MatTableDataSource<VersionedFlowSnapshotMetadata>();
selectedFlowVersion: string | null = null;
+ loadingBranches: WritableSignal<boolean> = signal(false);
+ loadingBuckets: WritableSignal<boolean> = signal(false);
+ loadingFlows: WritableSignal<boolean> = signal(false);
+ loadingVersions: WritableSignal<boolean> = signal(false);
+ loadingVersionsError: WritableSignal<string | null> = signal(null);
+
constructor(
@Inject(MAT_DIALOG_DATA) private dialogRequest:
ImportFromRegistryDialogRequest,
private formBuilder: FormBuilder,
private store: Store<CanvasState>,
private nifiCommon: NiFiCommon,
private client: Client,
- private clusterConnectionService: ClusterConnectionService
+ private clusterConnectionService: ClusterConnectionService,
+ private errorHelper: ErrorHelper
) {
super();
this.store
@@ -230,12 +241,47 @@ export class ImportFromRegistry extends
CloseOnEscapeDialog implements OnInit {
this.loadVersions(registryId, bucketId, flowId, branch);
}
+ private setLoading(resourceType: string, loading: boolean): void {
+ let formControl;
+ switch (resourceType) {
+ case 'branch':
+ this.loadingBranches.set(loading);
+ formControl = this.importFromRegistryForm.get('branch');
+ break;
+ case 'bucket':
+ this.loadingBuckets.set(loading);
+ formControl = this.importFromRegistryForm.get('bucket');
+ break;
+ case 'flow':
+ this.loadingFlows.set(loading);
+ formControl = this.importFromRegistryForm.get('flow');
+ break;
+ case 'version':
+ this.loadingVersions.set(loading);
+ break;
+ }
+ if (formControl) {
+ if (loading) {
+ formControl.disable();
+ } else {
+ formControl.enable();
+ }
+ }
+ }
+
loadBranches(registryId: string): void {
if (registryId) {
+ this.setLoading('branch', true);
this.branchOptions = [];
this.getBranches(registryId)
- .pipe(take(1))
+ .pipe(
+ take(1),
+ catchError(() => {
+ this.setLoading('branch', false);
+ return EMPTY;
+ })
+ )
.subscribe((branches: BranchEntity[]) => {
if (branches.length > 0) {
branches.forEach((entity: BranchEntity) => {
@@ -251,15 +297,23 @@ export class ImportFromRegistry extends
CloseOnEscapeDialog implements OnInit {
this.loadBuckets(registryId, branchId);
}
}
+ this.setLoading('branch', false);
});
}
}
loadBuckets(registryId: string, branch?: string | null): void {
+ this.setLoading('bucket', true);
this.bucketOptions = [];
this.getBuckets(registryId, branch)
- .pipe(take(1))
+ .pipe(
+ take(1),
+ catchError(() => {
+ this.setLoading('bucket', false);
+ return EMPTY;
+ })
+ )
.subscribe((buckets: BucketEntity[]) => {
if (buckets.length > 0) {
buckets.forEach((entity: BucketEntity) => {
@@ -278,15 +332,23 @@ export class ImportFromRegistry extends
CloseOnEscapeDialog implements OnInit {
this.loadFlows(registryId, bucketId, branch);
}
}
+ this.setLoading('bucket', false);
});
}
loadFlows(registryId: string, bucketId: string, branch?: string | null):
void {
+ this.setLoading('flow', true);
this.flowOptions = [];
this.flowLookup.clear();
this.getFlows(registryId, bucketId, branch)
- .pipe(take(1))
+ .pipe(
+ take(1),
+ catchError(() => {
+ this.setLoading('flow', false);
+ return EMPTY;
+ })
+ )
.subscribe((versionedFlows: VersionedFlowEntity[]) => {
if (versionedFlows.length > 0) {
versionedFlows.forEach((entity: VersionedFlowEntity) => {
@@ -305,16 +367,25 @@ export class ImportFromRegistry extends
CloseOnEscapeDialog implements OnInit {
this.loadVersions(registryId, bucketId, flowId,
branch);
}
}
+ this.setLoading('flow', false);
});
}
loadVersions(registryId: string, bucketId: string, flowId: string,
branch?: string | null): void {
+ this.setLoading('version', true);
this.dataSource.data = [];
this.selectedFlowVersion = null;
this.selectedFlowDescription =
this.flowLookup.get(flowId)?.description;
this.getFlowVersions(registryId, bucketId, flowId, branch)
- .pipe(take(1))
+ .pipe(
+ take(1),
+ catchError((errorResponse: HttpErrorResponse) => {
+ this.setLoading('version', false);
+
this.loadingVersionsError.set(this.errorHelper.getErrorString(errorResponse));
+ return EMPTY;
+ })
+ )
.subscribe((metadataEntities:
VersionedFlowSnapshotMetadataEntity[]) => {
if (metadataEntities.length > 0) {
const flowVersions = metadataEntities.map(
@@ -326,6 +397,7 @@ export class ImportFromRegistry extends CloseOnEscapeDialog
implements OnInit {
this.dataSource.data = sortedFlowVersions;
}
+ this.setLoading('version', false);
});
}