[NIFI-13876] - Adding context aware error banners (#9396)

This commit is contained in:
Rob Fellows 2024-10-17 10:13:22 -04:00 committed by GitHub
parent 1823a52e36
commit a53e0ab81e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 778 additions and 244 deletions

View File

@ -29,16 +29,16 @@ import { AccessPolicyService } from '../../service/access-policy.service';
import { AccessPolicyEntity, ComponentResourceAction, PolicyStatus, ResourceAction } from '../shared';
import { selectAccessPolicy, selectResourceAction, selectSaving } from './access-policy.selectors';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { isDefinedAndNotNull } from 'libs/shared/src';
import { isDefinedAndNotNull, MEDIUM_DIALOG, SMALL_DIALOG } from 'libs/shared/src';
import { TenantEntity } from '../../../../state/shared';
import { AddTenantToPolicyDialog } from '../../ui/common/add-tenant-to-policy-dialog/add-tenant-to-policy-dialog.component';
import { AddTenantsToPolicyRequest } from './index';
import { selectUserGroups, selectUsers } from '../tenants/tenants.selectors';
import { OverridePolicyDialog } from '../../ui/common/override-policy-dialog/override-policy-dialog.component';
import { MEDIUM_DIALOG, SMALL_DIALOG } from 'libs/shared/src';
import { HttpErrorResponse } from '@angular/common/http';
import { loadCurrentUser } from '../../../../state/current-user/current-user.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class AccessPolicyEffects {
@ -495,7 +495,16 @@ export class AccessPolicyEffects {
ofType(AccessPolicyActions.accessPolicyApiBannerError),
map((action) => action.response),
tap(() => this.dialog.closeAll()),
switchMap((response) => of(ErrorActions.addBannerError({ error: response.error })))
switchMap((response) =>
of(
ErrorActions.addBannerError({
errorContext: {
errors: [response.error],
context: ErrorContextKey.ACCESS_POLICIES
}
})
)
)
)
);
}

View File

@ -24,7 +24,7 @@
@if (policyComponentState$ | async; as policyComponentState) {
@if (flowConfiguration$ | async; as flowConfiguration) {
<div class="component-access-policies flex flex-col h-full">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.ACCESS_POLICIES"></context-error-banner>
<div class="flex justify-between items-center">
<div>
<form [formGroup]="policyForm" class="my-2">

View File

@ -47,7 +47,7 @@ import { loadTenants, resetTenantsState } from '../../state/tenants/tenants.acti
import { loadPolicyComponent, resetPolicyComponentState } from '../../state/policy-component/policy-component.actions';
import { selectPolicyComponentState } from '../../state/policy-component/policy-component.selectors';
import { PolicyComponentState } from '../../state/policy-component';
import { clearBannerErrors } from '../../../../state/error/error.actions';
import { ErrorContextKey } from '../../../../state/error';
@Component({
selector: 'global-access-policies',
@ -464,9 +464,10 @@ export class ComponentAccessPolicies implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors());
this.store.dispatch(resetAccessPolicyState());
this.store.dispatch(resetTenantsState());
this.store.dispatch(resetPolicyComponentState());
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -29,6 +29,7 @@ import { PolicyTable } from '../common/policy-table/policy-table.component';
import { MatButtonModule } from '@angular/material/button';
import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.component';
import { ComponentContext, NifiTooltipDirective } from '@nifi/shared';
import { ContextErrorBanner } from '../../../../ui/common/context-error-banner/context-error-banner.component';
@NgModule({
declarations: [ComponentAccessPolicies],
@ -46,7 +47,8 @@ import { ComponentContext, NifiTooltipDirective } from '@nifi/shared';
PolicyTable,
MatButtonModule,
ErrorBanner,
ComponentContext
ComponentContext,
ContextErrorBanner
]
})
export class ComponentAccessPoliciesModule {}

View File

@ -23,7 +23,7 @@
} @else {
@if (flowConfiguration$ | async; as flowConfiguration) {
<div class="global-access-policies flex flex-col h-full">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.ACCESS_POLICIES"></context-error-banner>
<div class="flex justify-between items-center">
<div>
<form [formGroup]="policyForm" class="my-2">

View File

@ -21,8 +21,8 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import {
createAccessPolicy,
openAddTenantToPolicyDialog,
promptOverrideAccessPolicy,
promptDeleteAccessPolicy,
promptOverrideAccessPolicy,
promptRemoveTenantFromPolicy,
reloadAccessPolicy,
resetAccessPolicyState,
@ -48,7 +48,7 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
import { AccessPoliciesState } from '../../state';
import { loadTenants, resetTenantsState } from '../../state/tenants/tenants.actions';
import { loadCurrentUser } from '../../../../state/current-user/current-user.actions';
import { clearBannerErrors } from '../../../../state/error/error.actions';
import { ErrorContextKey } from '../../../../state/error';
@Component({
selector: 'global-access-policies',
@ -291,8 +291,9 @@ export class GlobalAccessPolicies implements OnInit, OnDestroy {
// reload the current user to ensure the latest global policies
this.store.dispatch(loadCurrentUser());
this.store.dispatch(clearBannerErrors());
this.store.dispatch(resetAccessPolicyState());
this.store.dispatch(resetTenantsState());
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -29,6 +29,7 @@ import { NifiTooltipDirective } from '@nifi/shared';
import { PolicyTable } from '../common/policy-table/policy-table.component';
import { MatButtonModule } from '@angular/material/button';
import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.component';
import { ContextErrorBanner } from '../../../../ui/common/context-error-banner/context-error-banner.component';
@NgModule({
declarations: [GlobalAccessPolicies],
@ -45,7 +46,8 @@ import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.com
NifiTooltipDirective,
PolicyTable,
MatButtonModule,
ErrorBanner
ErrorBanner,
ContextErrorBanner
]
})
export class GlobalAccessPoliciesModule {}

View File

@ -22,7 +22,7 @@
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Cluster</h3>
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.CLUSTER"></context-error-banner>
<div class="flex flex-col h-full gap-y-2">
@if (getTabLinks(); as tabs) {
<!-- Don't show the tab bar if there is only 1 tab to show -->

View File

@ -25,10 +25,10 @@ import { MatTabsModule } from '@angular/material/tabs';
import { selectClusterListing } from '../state/cluster-listing/cluster-listing.selectors';
import { clusterListingFeatureKey } from '../state/cluster-listing';
import { ClusterState } from '../state';
import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.component';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
describe('Cluster', () => {
let component: Cluster;
@ -46,7 +46,7 @@ describe('Cluster', () => {
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation),
MockComponent(ErrorBanner)
MockComponent(ContextErrorBanner)
],
providers: [
provideMockStore({

View File

@ -33,7 +33,7 @@ import { CurrentUser } from '../../../state/current-user';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { selectCurrentRoute } from '@nifi/shared';
import { resetSystemDiagnostics } from '../../../state/system-diagnostics/system-diagnostics.actions';
import { clearBannerErrors } from '../../../state/error/error.actions';
import { ErrorContextKey } from '../../../state/error';
interface TabLink {
label: string;
@ -83,7 +83,6 @@ export class Cluster implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.store.dispatch(resetClusterState());
this.store.dispatch(resetSystemDiagnostics());
this.store.dispatch(clearBannerErrors());
}
refresh() {
@ -118,4 +117,6 @@ export class Cluster implements OnInit, OnDestroy {
const path = route.routeConfig.path;
return this._tabLinks.find((tabLink) => tabLink.link === path);
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -28,6 +28,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatIconButton } from '@angular/material/button';
import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
import { ContextErrorBanner } from '../../../ui/common/context-error-banner/context-error-banner.component';
@NgModule({
declarations: [Cluster],
@ -41,7 +42,8 @@ import { BannerText } from '../../../ui/common/banner-text/banner-text.component
MatTabsModule,
MatIconButton,
ErrorBanner,
BannerText
BannerText,
ContextErrorBanner
]
})
export class ClusterModule {}

View File

@ -64,6 +64,7 @@ import {
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
import { NiFiCommon, Storage } from '@nifi/shared';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ControllerServicesEffects {
@ -519,7 +520,6 @@ export class ControllerServicesEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(resetPropertyVerificationState());
if (response != 'ROUTED') {
@ -574,7 +574,13 @@ export class ControllerServicesEffects {
this.actions$.pipe(
ofType(ControllerServicesActions.controllerServicesBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.CONTROLLER_SERVICES }
})
)
)
)
);

View File

@ -113,6 +113,7 @@ import {
} from './index';
import { StatusHistoryRequest } from '../../../../state/status-history';
import { FetchComponentVersionsRequest } from '../../../../state/shared';
import { ErrorContext } from '../../../../state/error';
const CANVAS_PREFIX = '[Canvas]';
@ -809,7 +810,10 @@ export const stopVersionControlSuccess = createAction(
export const flowSnackbarError = createAction(`${CANVAS_PREFIX} Flow Snackbar Error`, props<{ error: string }>());
export const flowBannerError = createAction(`${CANVAS_PREFIX} Flow Banner Error`, props<{ error: string }>());
export const flowBannerError = createAction(
`${CANVAS_PREFIX} Flow Banner Error`,
props<{ errorContext: ErrorContext }>()
);
export const openShowLocalChangesDialogRequest = createAction(
`${CANVAS_PREFIX} Open Show Local Changes Dialog Request`,

View File

@ -152,11 +152,12 @@ import {
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
import { Storage, NiFiCommon } from '@nifi/shared';
import { NiFiCommon, Storage } from '@nifi/shared';
import { resetPollingFlowAnalysis } from '../flow-analysis/flow-analysis.actions';
import { selectDocumentVisibilityState } from '../../../../state/document-visibility/document-visibility.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DocumentVisibility } from '../../../../state/document-visibility';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class FlowEffects {
@ -410,7 +411,6 @@ export class FlowEffects {
})
.afterClosed()
.subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(FlowActions.setDragging({ dragging: false }));
});
})
@ -433,7 +433,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.REMOTE_PROCESS_GROUP))
)
)
)
)
@ -537,7 +539,6 @@ export class FlowEffects {
})
.afterClosed()
.subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(FlowActions.setDragging({ dragging: false }));
});
})
@ -560,7 +561,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.PROCESS_GROUP))
)
)
)
)
@ -581,7 +584,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.PROCESS_GROUP))
)
)
)
)
@ -651,7 +656,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.PROCESS_GROUP))
)
)
)
)
@ -727,7 +734,6 @@ export class FlowEffects {
dialogReference.afterClosed().subscribe(() => {
this.canvasUtils.removeTempEdge();
this.store.dispatch(ErrorActions.clearBannerErrors());
});
})
),
@ -749,7 +755,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.CONNECTION))
)
)
)
)
@ -769,7 +777,6 @@ export class FlowEffects {
.afterClosed()
.subscribe(() => {
this.store.dispatch(FlowActions.setDragging({ dragging: false }));
this.store.dispatch(ErrorActions.clearBannerErrors());
});
})
),
@ -791,7 +798,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.PORT))
)
)
)
)
@ -866,7 +875,10 @@ export class FlowEffects {
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
context: ErrorContextKey.REGISTRY_IMPORT,
errors: [this.errorHelper.getErrorString(errorResponse)]
}
})
);
}
@ -885,7 +897,10 @@ export class FlowEffects {
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
context: ErrorContextKey.REGISTRY_IMPORT,
errors: [this.errorHelper.getErrorString(errorResponse)]
}
})
);
}
@ -905,7 +920,10 @@ export class FlowEffects {
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
context: ErrorContextKey.REGISTRY_IMPORT,
errors: [this.errorHelper.getErrorString(errorResponse)]
}
})
);
}
@ -926,7 +944,10 @@ export class FlowEffects {
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
context: ErrorContextKey.REGISTRY_IMPORT,
errors: [this.errorHelper.getErrorString(errorResponse)]
}
})
);
}
@ -936,7 +957,6 @@ export class FlowEffects {
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(FlowActions.setDragging({ dragging: false }));
this.store.dispatch(ErrorActions.clearBannerErrors());
});
} else {
this.dialog
@ -971,7 +991,9 @@ export class FlowEffects {
}
})
),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.REGISTRY_IMPORT))
)
)
)
)
@ -1281,7 +1303,6 @@ export class FlowEffects {
})
.afterClosed()
.subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
FlowActions.selectComponents({
request: {
@ -1313,7 +1334,6 @@ export class FlowEffects {
})
.afterClosed()
.subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
FlowActions.selectComponents({
request: {
@ -1531,7 +1551,6 @@ export class FlowEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(resetPropertyVerificationState());
if (response != 'ROUTED') {
this.store.dispatch(
@ -1603,7 +1622,6 @@ export class FlowEffects {
});
}
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
FlowActions.selectComponents({
request: {
@ -1660,7 +1678,6 @@ export class FlowEffects {
});
editDialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (request.entity.id === currentProcessGroupId) {
this.store.dispatch(
FlowActions.loadProcessGroup({
@ -1732,8 +1749,6 @@ export class FlowEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response != 'ROUTED') {
this.store.dispatch(
FlowActions.selectComponents({
@ -1802,7 +1817,12 @@ export class FlowEffects {
map((action) => action.response),
switchMap((response) => {
if (response.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(response.errorResponse));
return of(
this.bannerOrFullScreenError(
response.errorResponse,
this.componentTypeToErrorContext(response.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(response.errorResponse));
}
@ -1810,6 +1830,30 @@ export class FlowEffects {
)
);
private componentTypeToErrorContext(type: ComponentType): ErrorContextKey {
switch (type) {
case ComponentType.Connection:
return ErrorContextKey.CONNECTION;
case ComponentType.Label:
return ErrorContextKey.LABEL;
case ComponentType.InputPort:
case ComponentType.OutputPort:
return ErrorContextKey.PORT;
case ComponentType.Processor:
return ErrorContextKey.PROCESSOR;
case ComponentType.ProcessGroup:
return ErrorContextKey.PROCESS_GROUP;
case ComponentType.RemoteProcessGroup:
return ErrorContextKey.REMOTE_PROCESS_GROUP;
case ComponentType.Funnel:
return ErrorContextKey.FUNNEL;
case ComponentType.ControllerService:
return ErrorContextKey.CONTROLLER_SERVICES;
default:
return ErrorContextKey.FLOW;
}
}
updateProcessor$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.updateProcessor),
@ -2775,7 +2819,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -2799,7 +2848,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -2901,7 +2955,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -2925,7 +2984,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -3028,7 +3092,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -3055,7 +3124,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -3218,7 +3292,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -3245,7 +3324,12 @@ export class FlowEffects {
}),
catchError((errorResponse: HttpErrorResponse) => {
if (request.errorStrategy === 'banner') {
return of(this.bannerOrFullScreenError(errorResponse));
return of(
this.bannerOrFullScreenError(
errorResponse,
this.componentTypeToErrorContext(request.type)
)
);
} else {
return of(this.snackBarOrFullScreenError(errorResponse));
}
@ -3403,7 +3487,10 @@ export class FlowEffects {
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
context: ErrorContextKey.FLOW_VERSION,
errors: [this.errorHelper.getErrorString(errorResponse)]
}
})
);
}
@ -3422,7 +3509,10 @@ export class FlowEffects {
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
context: ErrorContextKey.FLOW_VERSION,
errors: [this.errorHelper.getErrorString(errorResponse)]
}
})
);
}
@ -3472,10 +3562,6 @@ export class FlowEffects {
);
}
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
});
})
),
{ dispatch: false }
@ -3490,7 +3576,9 @@ export class FlowEffects {
map((response) => {
return FlowActions.saveToFlowRegistrySuccess({ response });
}),
catchError((errorResponse: HttpErrorResponse) => of(this.bannerOrFullScreenError(errorResponse)))
catchError((errorResponse: HttpErrorResponse) =>
of(this.bannerOrFullScreenError(errorResponse, ErrorContextKey.FLOW_VERSION))
)
);
})
)
@ -3509,8 +3597,8 @@ export class FlowEffects {
flowBannerError$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowActions.flowBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
map((action) => action.errorContext),
switchMap((errorContext) => of(ErrorActions.addBannerError({ errorContext })))
)
);
@ -3525,10 +3613,13 @@ export class FlowEffects {
)
);
private bannerOrFullScreenError(errorResponse: HttpErrorResponse) {
private bannerOrFullScreenError(errorResponse: HttpErrorResponse, context: ErrorContextKey) {
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
return FlowActions.flowBannerError({
error: this.errorHelper.getErrorString(errorResponse)
errorContext: {
errors: [this.errorHelper.getErrorString(errorResponse)],
context
}
});
} else {
return this.errorHelper.fullScreenError(errorResponse);

View File

@ -37,6 +37,7 @@ import { selectTimeOffset } from '../../../../state/flow-configuration/flow-conf
import { selectAbout } from '../../../../state/about/about.selectors';
import { MEDIUM_DIALOG } from 'libs/shared/src';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ManageRemotePortsEffects {
@ -126,7 +127,13 @@ export class ManageRemotePortsEffects {
this.actions$.pipe(
ofType(ManageRemotePortsActions.remotePortsBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.MANAGE_REMOTE_PORTS }
})
)
)
)
);
@ -220,8 +227,6 @@ export class ManageRemotePortsEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response != 'ROUTED') {
this.store.dispatch(
ManageRemotePortsActions.selectRemotePort({

View File

@ -33,7 +33,8 @@ describe('Birdseye', () => {
provide: BirdseyeView,
useValue: {
init: jest.fn(),
refresh: jest.fn()
refresh: jest.fn(),
destroy: jest.fn()
}
}
]

View File

@ -18,7 +18,7 @@
<h2 mat-dialog-title>Create Connection</h2>
@if (breadcrumbs$ | async; as breadcrumbs) {
<form class="create-connection-form" [formGroup]="createConnectionForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.CONNECTION"></context-error-banner>
<mat-tab-group>
<mat-tab label="Details">
<mat-dialog-content>

View File

@ -57,6 +57,8 @@ import { BreadcrumbEntity } from '../../../../../state/shared';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'create-connection',
@ -86,7 +88,8 @@ import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-b
DestinationProcessGroup,
SourceRemoteProcessGroup,
DestinationRemoteProcessGroup,
ErrorBanner
ErrorBanner,
ContextErrorBanner
],
templateUrl: './create-connection.component.html',
styleUrls: ['./create-connection.component.scss']
@ -311,4 +314,6 @@ export class CreateConnection extends CloseOnEscapeDialog {
override isDirty(): boolean {
return this.createConnectionForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -19,7 +19,7 @@
{{ connectionReadonly || sourceReadonly || destinationReadonly ? 'Connection Details' : 'Edit Connection' }}
</h2>
<form class="edit-connection-form" [formGroup]="editConnectionForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.CONNECTION"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Details">
<mat-dialog-content>

View File

@ -56,6 +56,8 @@ import { DestinationRemoteProcessGroup } from '../destination/destination-remote
import { BreadcrumbEntity } from '../../../../../state/shared';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { TabbedDialog } from '../../../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-connection',
@ -85,7 +87,8 @@ import { TabbedDialog } from '../../../../../../../ui/common/tabbed-dialog/tabbe
DestinationProcessGroup,
SourceRemoteProcessGroup,
DestinationRemoteProcessGroup,
ErrorBanner
ErrorBanner,
ContextErrorBanner
],
templateUrl: './edit-connection.component.html',
styleUrls: ['./edit-connection.component.scss']
@ -438,4 +441,6 @@ export class EditConnectionComponent extends TabbedDialog {
override getCancelDialogResult(): any {
return 'CANCELLED';
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>Import From Registry</h2>
<form class="import-from-registry-form" [formGroup]="importFromRegistryForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.REGISTRY_IMPORT"></context-error-banner>
<mat-dialog-content>
<div class="dialog-content flex flex-col h-full gap-y-2">
<div>

View File

@ -51,6 +51,8 @@ import { Client } from '../../../../../../../service/client.service';
import { importFromRegistry } from '../../../../../state/flow/flow.actions';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import { isDefinedAndNotNull, SelectOption } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'import-from-registry',
@ -74,7 +76,8 @@ import { isDefinedAndNotNull, SelectOption } from 'libs/shared/src';
JsonPipe,
MatCheckboxModule,
MatSortModule,
MatTableModule
MatTableModule,
ContextErrorBanner
],
templateUrl: './import-from-registry.component.html',
styleUrls: ['./import-from-registry.component.scss']
@ -414,4 +417,6 @@ export class ImportFromRegistry extends CloseOnEscapeDialog implements OnInit {
override isDirty(): boolean {
return this.importFromRegistryForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>Save Flow Version</h2>
<form class="save-version-form" [formGroup]="saveVersionForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.FLOW_VERSION"></context-error-banner>
<mat-dialog-content>
@if (versionControlInformation) {
<div class="flex flex-col gap-y-4 mb-6">

View File

@ -36,6 +36,8 @@ import { SaveVersionDialogRequest, SaveVersionRequest, VersionControlInformation
import { TextTip, NiFiCommon, NifiTooltipDirective, CloseOnEscapeDialog } from '@nifi/shared';
import { NgForOf, NgIf } from '@angular/common';
import { MatInput } from '@angular/material/input';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'save-version-dialog',
@ -57,7 +59,8 @@ import { MatInput } from '@angular/material/input';
MatLabel,
NgForOf,
NgIf,
MatInput
MatInput,
ContextErrorBanner
],
templateUrl: './save-version-dialog.component.html',
styleUrl: './save-version-dialog.component.scss'
@ -238,4 +241,6 @@ export class SaveVersionDialog extends CloseOnEscapeDialog implements OnInit {
override isDirty(): boolean {
return this.saveVersionForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>{{ readonly ? 'Label Details' : 'Edit Label' }}</h2>
<form class="edit-label-form" [formGroup]="editLabelForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.LABEL"></context-error-banner>
<mat-dialog-content>
<div>
<mat-form-field>

View File

@ -35,6 +35,8 @@ import { ClusterConnectionService } from '../../../../../../../service/cluster-c
import { MatOption } from '@angular/material/autocomplete';
import { MatSelect } from '@angular/material/select';
import { NifiTooltipDirective, CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-label',
@ -51,7 +53,8 @@ import { NifiTooltipDirective, CloseOnEscapeDialog } from '@nifi/shared';
NifiSpinnerDirective,
MatOption,
MatSelect,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
styleUrls: ['./edit-label.component.scss']
})
@ -115,4 +118,6 @@ export class EditLabel extends CloseOnEscapeDialog {
})
);
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>Create New {{ portTypeLabel }}</h2>
<form class="create-port-form" [formGroup]="createPortForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PORT"></context-error-banner>
<mat-dialog-content>
<mat-form-field>
<mat-label>{{ portTypeLabel }} Name</mat-label>

View File

@ -32,6 +32,8 @@ import { AsyncPipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { NifiTooltipDirective, TextTip, CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'create-port',
@ -45,7 +47,8 @@ import { NifiTooltipDirective, TextTip, CloseOnEscapeDialog } from '@nifi/shared
MatButtonModule,
AsyncPipe,
NifiSpinnerDirective,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
templateUrl: './create-port.component.html',
styleUrls: ['./create-port.component.scss']
@ -113,4 +116,5 @@ export class CreatePort extends CloseOnEscapeDialog {
}
protected readonly ComponentType = ComponentType;
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>{{ readonly ? portTypeLabel + ' Details' : 'Edit ' + portTypeLabel }}</h2>
<form class="edit-port-form" [formGroup]="editPortForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PORT"></context-error-banner>
<mat-dialog-content>
<div>
<mat-form-field>

View File

@ -34,6 +34,8 @@ import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nif
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
import { NifiTooltipDirective, TextTip, CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-port',
standalone: true,
@ -47,7 +49,8 @@ import { NifiTooltipDirective, TextTip, CloseOnEscapeDialog } from '@nifi/shared
MatButtonModule,
AsyncPipe,
NifiSpinnerDirective,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
styleUrls: ['./edit-port.component.scss']
})
@ -127,4 +130,5 @@ export class EditPort extends CloseOnEscapeDialog {
}
protected readonly TextTip = TextTip;
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>Create Process Group</h2>
<form class="create-process-group-form" [formGroup]="createProcessGroupForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PROCESS_GROUP"></context-error-banner>
<mat-dialog-content>
<mat-form-field>
<mat-label>Name</mat-label>

View File

@ -35,6 +35,8 @@ import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators }
import { MatIconModule } from '@angular/material/icon';
import { NiFiCommon, TextTip, NifiTooltipDirective } from '@nifi/shared';
import { CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'create-process-group',
@ -51,7 +53,8 @@ import { CloseOnEscapeDialog } from '@nifi/shared';
MatOptionModule,
MatSelectModule,
NifiTooltipDirective,
MatIconModule
MatIconModule,
ContextErrorBanner
],
templateUrl: './create-process-group.component.html',
styleUrls: ['./create-process-group.component.scss']
@ -142,4 +145,6 @@ export class CreateProcessGroup extends CloseOnEscapeDialog {
);
}
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>{{ readonly ? 'Process Group Details' : 'Edit Process Group' }}</h2>
<form class="process-group-edit-form" [formGroup]="editProcessGroupForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PROCESS_GROUP"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>

View File

@ -36,6 +36,8 @@ import { EditComponentDialogRequest } from '../../../../../state/flow';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { TabbedDialog } from '../../../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-process-group',
@ -55,7 +57,8 @@ import { TabbedDialog } from '../../../../../../../ui/common/tabbed-dialog/tabbe
NifiTooltipDirective,
FormsModule,
ControllerServiceTable,
ErrorBanner
ErrorBanner,
ContextErrorBanner
],
styleUrls: ['./edit-process-group.component.scss']
})
@ -256,4 +259,6 @@ export class EditProcessGroup extends TabbedDialog {
override isDirty(): boolean {
return this.editProcessGroupForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -26,7 +26,7 @@
</div>
</h2>
<form class="processor-edit-form" [formGroup]="editProcessorForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PROCESSOR"></context-error-banner>
<!-- TODO - Stop & Configure -->
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">

View File

@ -26,8 +26,8 @@ import { ClusterConnectionService } from '../../../../../../../service/cluster-c
import 'codemirror/addon/hint/show-hint';
import { MockComponent } from 'ng-mocks';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
describe('EditProcessor', () => {
let component: EditProcessor;
@ -727,7 +727,7 @@ describe('EditProcessor', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditProcessor, MockComponent(ErrorBanner), NoopAnimationsModule],
imports: [EditProcessor, MockComponent(ContextErrorBanner), NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{

View File

@ -63,6 +63,8 @@ import {
} from '../../../../../../../state/property-verification';
import { TabbedDialog } from '../../../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { ComponentType, SelectOption } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-processor',
@ -84,7 +86,8 @@ import { ComponentType, SelectOption } from 'libs/shared/src';
RunDurationSlider,
RelationshipSettings,
ErrorBanner,
PropertyVerification
PropertyVerification,
ContextErrorBanner
],
styleUrls: ['./edit-processor.component.scss']
})
@ -418,4 +421,6 @@ export class EditProcessor extends TabbedDialog {
properties: this.getModifiedProperties()
});
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>Create Remote Process Group</h2>
<form class="create-remote-process-group-form" [formGroup]="createRemoteProcessGroupForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.REMOTE_PROCESS_GROUP"></context-error-banner>
<mat-dialog-content>
<div class="flex gap-x-4">
<div class="w-full">

View File

@ -34,6 +34,8 @@ import { MatIconModule } from '@angular/material/icon';
import { CreateComponentRequest } from '../../../../../state/flow';
import { NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
standalone: true,
@ -49,7 +51,8 @@ import { CloseOnEscapeDialog } from '@nifi/shared';
MatOptionModule,
MatSelectModule,
MatIconModule,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
templateUrl: './create-remote-process-group.component.html',
styleUrls: ['./create-remote-process-group.component.scss']
@ -98,4 +101,5 @@ export class CreateRemoteProcessGroup extends CloseOnEscapeDialog {
}
protected readonly TextTip = TextTip;
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>{{ readonly ? 'Remote Process Group Details' : 'Edit Remote Process Group' }}</h2>
<form class="edit-remote-process-group-form" [formGroup]="editRemoteProcessGroupForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.REMOTE_PROCESS_GROUP"></context-error-banner>
<mat-dialog-content>
<div class="flex flex-col mb-4">
<div>Name</div>

View File

@ -32,6 +32,8 @@ import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-b
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
import { NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
standalone: true,
@ -48,7 +50,8 @@ import { CloseOnEscapeDialog } from '@nifi/shared';
NifiSpinnerDirective,
FormsModule,
ErrorBanner,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
styleUrls: ['./edit-remote-process-group.component.scss']
})
@ -108,4 +111,6 @@ export class EditRemoteProcessGroup extends CloseOnEscapeDialog {
override isDirty(): boolean {
return this.editRemoteProcessGroupForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>Edit Remote {{ portTypeLabel }}</h2>
<form class="edit-remote-port-form" [formGroup]="editPortForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.MANAGE_REMOTE_PORTS"></context-error-banner>
<mat-dialog-content>
<div>
<div class="flex flex-col mb-5">

View File

@ -35,6 +35,8 @@ import { ClusterConnectionService } from '../../../../../service/cluster-connect
import { NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CloseOnEscapeDialog } from '@nifi/shared';
import { CanvasState } from '../../../state';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
standalone: true,
@ -48,7 +50,8 @@ import { CanvasState } from '../../../state';
MatButtonModule,
AsyncPipe,
NifiSpinnerDirective,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
styleUrls: ['./edit-remote-port.component.scss']
})
@ -117,4 +120,6 @@ export class EditRemotePortComponent extends CloseOnEscapeDialog {
override isDirty(): boolean {
return this.editPortForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -18,7 +18,7 @@
@if (loading) {
<div class="splash h-full p-20">
<div class="splash-img h-full flex items-center justify-center">
<mat-spinner class="tertiary-spinner "></mat-spinner>
<mat-spinner class="tertiary-spinner"></mat-spinner>
</div>
</div>
} @else {

View File

@ -60,6 +60,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { isDefinedAndNotNull, MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG } from 'libs/shared/src';
import { BackNavigation } from '../../../../state/navigation';
import { NiFiCommon, Storage } from '@nifi/shared';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ParameterContextListingEffects {
@ -170,10 +171,6 @@ export class ParameterContextListingEffects {
})
);
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
});
})
),
{ dispatch: false }
@ -224,7 +221,13 @@ export class ParameterContextListingEffects {
this.actions$.pipe(
ofType(ParameterContextListingActions.parameterContextListingBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.PARAMETER_CONTEXTS }
})
)
)
)
);
@ -385,8 +388,6 @@ export class ParameterContextListingEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response != 'ROUTED') {
this.store.dispatch(
ParameterContextListingActions.selectParameterContext({

View File

@ -19,7 +19,7 @@
{{ readonly ? 'Parameter Context Details' : isNew ? 'Add Parameter Context' : 'Edit Parameter Context' }}
</h2>
<form class="parameter-context-edit-form" [formGroup]="editParameterContextForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PARAMETER_CONTEXTS"></context-error-banner>
@if ((updateRequest | async)!; as requestEntity) {
<mat-dialog-content>
<div class="dialog-tab-content flex gap-x-8">

View File

@ -46,6 +46,8 @@ import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { NiFiCommon, TextTip, NifiTooltipDirective } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-parameter-context',
@ -69,7 +71,8 @@ import { NiFiCommon, TextTip, NifiTooltipDirective } from '@nifi/shared';
ParameterReferences,
RouterLink,
ErrorBanner,
NifiTooltipDirective
NifiTooltipDirective,
ContextErrorBanner
],
styleUrls: ['./edit-parameter-context.component.scss']
})
@ -204,4 +207,6 @@ export class EditParameterContext extends TabbedDialog {
override isDirty(): boolean {
return this.editParameterContextForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -30,6 +30,7 @@ import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { isDefinedAndNotNull, NiFiCommon } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class LineageEffects {
@ -81,7 +82,11 @@ export class LineageEffects {
const query: Lineage = response.lineage;
if (query.finished || !this.nifiCommon.isEmpty(query.results.errors)) {
response.lineage.results.errors?.forEach((error) => {
this.store.dispatch(ErrorActions.addBannerError({ error }));
this.store.dispatch(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.LINEAGE }
})
);
});
return of(LineageActions.deleteLineageQuery());
@ -147,7 +152,14 @@ export class LineageEffects {
),
switchMap((response) => {
response.lineage.results.errors?.forEach((error) => {
this.store.dispatch(ErrorActions.addBannerError({ error }));
this.store.dispatch(
ErrorActions.addBannerError({
errorContext: {
errors: [error],
context: ErrorContextKey.LINEAGE
}
})
);
});
return of(LineageActions.stopPollingLineageQuery());
@ -186,7 +198,16 @@ export class LineageEffects {
tap(() => {
this.store.dispatch(LineageActions.stopPollingLineageQuery());
}),
switchMap(({ error }) => of(ErrorActions.addBannerError({ error })))
switchMap(({ error }) =>
of(
ErrorActions.addBannerError({
errorContext: {
errors: [error],
context: ErrorContextKey.LINEAGE
}
})
)
)
)
);
}

View File

@ -46,6 +46,7 @@ import { selectClusterSummary } from '../../../../state/cluster-summary/cluster-
import { ClusterService } from '../../../../service/cluster.service';
import { LARGE_DIALOG, MEDIUM_DIALOG } from 'libs/shared/src';
import { Attribute } from '../../../../state/shared';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ProvenanceEventListingEffects {
@ -147,7 +148,11 @@ export class ProvenanceEventListingEffects {
const query: Provenance = response.provenance;
if (query.finished || !this.nifiCommon.isEmpty(query.results.errors)) {
response.provenance.results.errors?.forEach((error) => {
this.store.dispatch(ErrorActions.addBannerError({ error }));
this.store.dispatch(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.REPORTING_TASKS }
})
);
});
return of(ProvenanceEventListingActions.deleteProvenanceQuery());
@ -214,7 +219,11 @@ export class ProvenanceEventListingEffects {
),
switchMap((response) => {
response.provenance.results.errors?.forEach((error) => {
this.store.dispatch(ErrorActions.addBannerError({ error }));
this.store.dispatch(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.REPORTING_TASKS }
})
);
});
return of(ProvenanceEventListingActions.stopPollingProvenanceQuery());
@ -462,7 +471,13 @@ export class ProvenanceEventListingEffects {
tap(() => {
this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
}),
switchMap(({ error }) => of(ErrorActions.addBannerError({ error })))
switchMap(({ error }) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.REPORTING_TASKS }
})
)
)
)
);

View File

@ -26,8 +26,8 @@ import {
ProvenanceResults
} from '../../state/provenance-event-listing';
import {
selectLoadedTimestamp,
selectCompletedProvenance,
selectLoadedTimestamp,
selectProvenanceRequest,
selectSearchableFieldsFromRoute,
selectStatus
@ -50,6 +50,7 @@ import { selectCompletedLineage } from '../../state/lineage/lineage.selectors';
import { clearBannerErrors } from '../../../../state/error/error.actions';
import { selectClusterSummary } from '../../../../state/cluster-summary/cluster-summary.selectors';
import { loadClusterSummary } from '../../../../state/cluster-summary/cluster-summary.actions';
import { ErrorContextKey } from '../../../../state/error';
@Component({
selector: 'provenance-event-listing',
@ -209,7 +210,7 @@ export class ProvenanceEventListing implements OnInit, OnDestroy {
}
clearBannerErrors(): void {
this.store.dispatch(clearBannerErrors());
this.store.dispatch(clearBannerErrors({ context: ErrorContextKey.PROVENANCE }));
}
resetLineage(): void {
@ -220,6 +221,5 @@ export class ProvenanceEventListing implements OnInit, OnDestroy {
this.stateReset = true;
this.store.dispatch(resetProvenanceState());
this.store.dispatch(resetLineage());
this.store.dispatch(clearBannerErrors());
}
}

View File

@ -16,7 +16,7 @@
-->
<div class="provenance-event-table h-full flex flex-col">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PROVENANCE"></context-error-banner>
<div [class.hidden]="showLineage" class="h-full flex flex-col">
<div class="flex flex-col">
<div class="flex justify-between mb-1">

View File

@ -21,7 +21,7 @@ import { ProvenanceEventTable } from './provenance-event-table.component';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MockComponent } from 'ng-mocks';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
describe('ProvenanceEventTable', () => {
let component: ProvenanceEventTable;
@ -29,7 +29,7 @@ describe('ProvenanceEventTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProvenanceEventTable, MockComponent(ErrorBanner), MatTableModule, NoopAnimationsModule]
imports: [ProvenanceEventTable, MockComponent(ContextErrorBanner), MatTableModule, NoopAnimationsModule]
});
fixture = TestBed.createComponent(ProvenanceEventTable);
component = fixture.componentInstance;

View File

@ -41,6 +41,8 @@ import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.
import { ClusterSummary } from '../../../../../state/cluster-summary';
import { MatButtonModule } from '@angular/material/button';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
import { ErrorContextKey } from '../../../../../state/error';
@Component({
selector: 'provenance-event-table',
@ -64,7 +66,8 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
MatButtonModule,
MatMenu,
MatMenuItem,
MatMenuTrigger
MatMenuTrigger,
ContextErrorBanner
],
styleUrls: ['./provenance-event-table.component.scss']
})
@ -402,4 +405,6 @@ export class ProvenanceEventTable implements AfterViewInit {
refreshClicked(): void {
this.resubmitProvenanceQuery.next();
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -36,6 +36,7 @@ import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { stopPollingQueueListingRequest } from './queue-listing.actions';
import { LARGE_DIALOG } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class QueueListingEffects {
@ -339,7 +340,9 @@ export class QueueListingEffects {
tap(() => {
this.store.dispatch(QueueListingActions.stopPollingQueueListingRequest());
}),
switchMap(({ error }) => of(ErrorActions.addBannerError({ error })))
switchMap(({ error }) =>
of(ErrorActions.addBannerError({ errorContext: { errors: [error], context: ErrorContextKey.QUEUE } }))
)
)
);
}

View File

@ -17,7 +17,7 @@
<div class="flowfile-table h-full flex flex-col">
<h3 class="queue-listing-header primary-color">{{ selectedConnection?.label }}</h3>
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.QUEUE"></context-error-banner>
<div class="flex justify-between mb-2">
<div class="tertiary-color font-medium">
Display {{ displayObjectCount }} of {{ formatCount(queueSizeObjectCount) }} ({{

View File

@ -21,7 +21,7 @@ import { FlowFileTable } from './flowfile-table.component';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MockComponent } from 'ng-mocks';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
describe('FlowFileTable', () => {
let component: FlowFileTable;
@ -29,7 +29,7 @@ describe('FlowFileTable', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FlowFileTable, MockComponent(ErrorBanner), MatTableModule, NoopAnimationsModule]
imports: [FlowFileTable, MockComponent(ContextErrorBanner), MatTableModule, NoopAnimationsModule]
});
fixture = TestBed.createComponent(FlowFileTable);
component = fixture.componentInstance;

View File

@ -28,12 +28,23 @@ import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.
import { ClusterSummary } from '../../../../../state/cluster-summary';
import { MatIconButton } from '@angular/material/button';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'flowfile-table',
standalone: true,
templateUrl: './flowfile-table.component.html',
imports: [MatTableModule, RouterLink, ErrorBanner, MatIconButton, MatMenu, MatMenuItem, MatMenuTrigger],
imports: [
MatTableModule,
RouterLink,
ErrorBanner,
MatIconButton,
MatMenu,
MatMenuItem,
MatMenuTrigger,
ContextErrorBanner
],
styleUrls: ['./flowfile-table.component.scss']
})
export class FlowFileTable {
@ -152,4 +163,6 @@ export class FlowFileTable {
viewContentClicked(summary: FlowFileSummary): void {
this.viewContent.next(summary);
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -19,10 +19,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { distinctUntilChanged, filter } from 'rxjs';
import {
selectConnectionIdFromRoute,
selectSelectedConnection,
selectCompletedListingRequest,
selectConnectionIdFromRoute,
selectLoadedTimestamp,
selectSelectedConnection,
selectStatus
} from '../../state/queue-listing/queue-listing.selectors';
import { FlowFileSummary } from '../../state/queue-listing';
@ -40,7 +40,6 @@ import { NiFiState } from '../../../../state';
import { selectAbout } from '../../../../state/about/about.selectors';
import { About } from '../../../../state/about';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { clearBannerErrors } from '../../../../state/error/error.actions';
import { selectClusterSummary } from '../../../../state/cluster-summary/cluster-summary.selectors';
import { loadClusterSummary } from '../../../../state/cluster-summary/cluster-summary.actions';
@ -125,6 +124,5 @@ export class QueueListing implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.store.dispatch(resetQueueListingState());
this.store.dispatch(clearBannerErrors());
}
}

View File

@ -55,6 +55,7 @@ import {
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class FlowAnalysisRulesEffects {
@ -158,7 +159,13 @@ export class FlowAnalysisRulesEffects {
this.actions$.pipe(
ofType(FlowAnalysisRuleActions.flowAnalysisRuleBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.FLOW_ANALYSIS_RULES }
})
)
)
)
);
@ -364,7 +371,6 @@ export class FlowAnalysisRulesEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(resetPropertyVerificationState());
if (response != 'ROUTED') {

View File

@ -56,6 +56,7 @@ import {
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ManagementControllerServicesEffects {
@ -391,7 +392,6 @@ export class ManagementControllerServicesEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(resetPropertyVerificationState());
if (response != 'ROUTED') {
@ -445,7 +445,13 @@ export class ManagementControllerServicesEffects {
this.actions$.pipe(
ofType(ManagementControllerServicesActions.managementControllerServicesBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.CONTROLLER_SERVICES }
})
)
)
)
);

View File

@ -67,6 +67,7 @@ import {
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ParameterProvidersEffects {
@ -442,7 +443,6 @@ export class ParameterProvidersEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(resetPropertyVerificationState());
if (response !== 'ROUTED') {
@ -613,7 +613,6 @@ export class ParameterProvidersEffects {
dialogRef.afterClosed().subscribe((response) => {
this.store.dispatch(ParameterProviderActions.resetFetchedParameterProvider());
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response !== 'ROUTED') {
this.store.dispatch(
@ -640,7 +639,13 @@ export class ParameterProvidersEffects {
tap(() =>
this.store.dispatch(ParameterProviderActions.stopPollingParameterProviderParametersUpdateRequest())
),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.PARAMETER_PROVIDERS }
})
)
)
)
);

View File

@ -38,6 +38,7 @@ import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { LARGE_DIALOG, MEDIUM_DIALOG, SMALL_DIALOG } from 'libs/shared/src';
import { BackNavigation } from '../../../../state/navigation';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class RegistryClientsEffects {
@ -150,7 +151,13 @@ export class RegistryClientsEffects {
this.actions$.pipe(
ofType(RegistryClientsActions.registryClientsBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.REGISTRY_CLIENTS }
})
)
)
)
);
@ -252,8 +259,6 @@ export class RegistryClientsEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response != 'ROUTED') {
this.store.dispatch(
RegistryClientsActions.selectClient({

View File

@ -51,6 +51,7 @@ import {
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class ReportingTasksEffects {
@ -154,7 +155,13 @@ export class ReportingTasksEffects {
this.actions$.pipe(
ofType(ReportingTaskActions.reportingTasksBannerApiError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.REPORTING_TASKS }
})
)
)
)
);
@ -402,7 +409,6 @@ export class ReportingTasksEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(resetPropertyVerificationState());
if (response != 'ROUTED') {

View File

@ -26,7 +26,7 @@
</div>
</h2>
<form class="flow-analysis-rule-edit-form" [formGroup]="editFlowAnalysisRuleForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.FLOW_ANALYSIS_RULES"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>

View File

@ -25,7 +25,7 @@ import { ClusterConnectionService } from '../../../../../service/cluster-connect
import 'codemirror/addon/hint/show-hint';
import { MockComponent } from 'ng-mocks';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
describe('EditFlowAnalysisRule', () => {
let component: EditFlowAnalysisRule;
@ -96,7 +96,7 @@ describe('EditFlowAnalysisRule', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditFlowAnalysisRule, MockComponent(ErrorBanner), NoopAnimationsModule],
imports: [EditFlowAnalysisRule, MockComponent(ContextErrorBanner), NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{

View File

@ -46,6 +46,8 @@ import {
import { PropertyVerification } from '../../../../../ui/common/property-verification/property-verification.component';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { SelectOption } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-flow-analysis-rule',
@ -65,7 +67,8 @@ import { SelectOption } from 'libs/shared/src';
NifiTooltipDirective,
FlowAnalysisRuleTable,
ErrorBanner,
PropertyVerification
PropertyVerification,
ContextErrorBanner
],
styleUrls: ['./edit-flow-analysis-rule.component.scss']
})
@ -184,4 +187,6 @@ export class EditFlowAnalysisRule extends TabbedDialog {
properties: this.getModifiedProperties()
});
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -24,7 +24,7 @@
</div>
</h2>
<form class="parameter-provider-edit-form" [formGroup]="editParameterProviderForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PARAMETER_PROVIDERS"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>

View File

@ -51,6 +51,8 @@ import {
} from '../../../../../state/property-verification';
import { PropertyVerification } from '../../../../../ui/common/property-verification/property-verification.component';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-parameter-provider',
@ -69,7 +71,8 @@ import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dial
ErrorBanner,
CommonModule,
NifiTooltipDirective,
PropertyVerification
PropertyVerification,
ContextErrorBanner
],
templateUrl: './edit-parameter-provider.component.html',
styleUrls: ['./edit-parameter-provider.component.scss']
@ -183,4 +186,6 @@ export class EditParameterProvider extends TabbedDialog {
properties: this.getModifiedProperties()
});
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -18,7 +18,7 @@
<div>
<h2 mat-dialog-title>Fetch Parameters</h2>
<form class="parameter-provider-fetch-form" [formGroup]="fetchParametersForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.PARAMETER_PROVIDERS"></context-error-banner>
<mat-dialog-content *ngIf="(updateRequest | async)! as requestEntity; else fetchFormContent">
<div class="dialog-content flex gap-x-4 w-full pt-2">

View File

@ -47,6 +47,8 @@ import { Store } from '@ngrx/store';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import { CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'fetch-parameter-provider-parameters',
@ -65,7 +67,8 @@ import { CloseOnEscapeDialog } from '@nifi/shared';
MatCheckboxModule,
MatInputModule,
ParameterReferences,
PipesModule
PipesModule,
ContextErrorBanner
],
templateUrl: './fetch-parameter-provider-parameters.component.html',
styleUrls: ['./fetch-parameter-provider-parameters.component.scss']
@ -615,4 +618,6 @@ export class FetchParameterProviderParameters extends CloseOnEscapeDialog implem
override isDirty(): boolean {
return this.fetchParametersForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -24,7 +24,7 @@
</div>
</h2>
<form class="edit-registry-client-form" [formGroup]="editRegistryClientForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.REGISTRY_CLIENTS"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>

View File

@ -25,7 +25,7 @@ import { ClusterConnectionService } from '../../../../../service/cluster-connect
import 'codemirror/addon/hint/show-hint';
import { MockComponent } from 'ng-mocks';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
describe('EditRegistryClient', () => {
let component: EditRegistryClient;
@ -109,7 +109,7 @@ describe('EditRegistryClient', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditRegistryClient, MockComponent(ErrorBanner), NoopAnimationsModule],
imports: [EditRegistryClient, MockComponent(ContextErrorBanner), NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{

View File

@ -39,6 +39,8 @@ import { PropertyTable } from '../../../../../ui/common/property-table/property-
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-registry-client',
@ -56,7 +58,8 @@ import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dial
NifiTooltipDirective,
MatTabsModule,
PropertyTable,
ErrorBanner
ErrorBanner,
ContextErrorBanner
],
styleUrls: ['./edit-registry-client.component.scss']
})
@ -138,4 +141,6 @@ export class EditRegistryClient extends TabbedDialog {
override isDirty(): boolean {
return this.editRegistryClientForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -26,7 +26,7 @@
</div>
</h2>
<form class="reporting-task-edit-form" [formGroup]="editReportingTaskForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.REPORTING_TASKS"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>

View File

@ -24,8 +24,8 @@ import { EditReportingTaskDialogRequest } from '../../../state/reporting-tasks';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { MockComponent } from 'ng-mocks';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
describe('EditReportingTask', () => {
let component: EditReportingTask;
@ -389,7 +389,7 @@ describe('EditReportingTask', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditReportingTask, MockComponent(ErrorBanner), NoopAnimationsModule],
imports: [EditReportingTask, MockComponent(ContextErrorBanner), NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{

View File

@ -52,6 +52,8 @@ import {
import { PropertyVerification } from '../../../../../ui/common/property-verification/property-verification.component';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { SelectOption } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-reporting-task',
@ -72,7 +74,8 @@ import { SelectOption } from 'libs/shared/src';
NifiSpinnerDirective,
NifiTooltipDirective,
ErrorBanner,
PropertyVerification
PropertyVerification,
ContextErrorBanner
],
styleUrls: ['./edit-reporting-task.component.scss']
})
@ -246,4 +249,6 @@ export class EditReportingTask extends TabbedDialog {
properties: this.getModifiedProperties()
});
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -37,6 +37,7 @@ import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { LARGE_DIALOG, MEDIUM_DIALOG, SMALL_DIALOG } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../state/error';
@Injectable()
export class UserListingEffects {
@ -233,7 +234,9 @@ export class UserListingEffects {
this.actions$.pipe(
ofType(UserListingActions.usersApiBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(ErrorActions.addBannerError({ errorContext: { errors: [error], context: ErrorContextKey.USERS } }))
)
)
);
@ -393,8 +396,6 @@ export class UserListingEffects {
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
selectTenant({
id: request.user.id
@ -601,8 +602,6 @@ export class UserListingEffects {
});
dialogReference.afterClosed().subscribe(() => {
this.store.dispatch(ErrorActions.clearBannerErrors());
this.store.dispatch(
selectTenant({
id: request.userGroup.id

View File

@ -31,6 +31,7 @@ import { isDefinedAndNotNull, LARGE_DIALOG } from 'libs/shared/src';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHelper } from '../../service/error-helper.service';
import { ErrorContextKey } from '../error';
@Injectable()
export class ComponentStateEffects {
@ -109,10 +110,15 @@ export class ComponentStateEffects {
catchError((errorResponse: HttpErrorResponse) =>
of(
ErrorActions.addBannerError({
error: this.errorHelper.getErrorString(
errorResponse,
'Failed to clear the component state.'
)
errorContext: {
errors: [
this.errorHelper.getErrorString(
errorResponse,
'Failed to clear the component state.'
)
],
context: ErrorContextKey.COMPONENT_STATE
}
})
)
)
@ -139,10 +145,15 @@ export class ComponentStateEffects {
catchError((errorResponse: HttpErrorResponse) =>
of(
ErrorActions.addBannerError({
error: this.errorHelper.getErrorString(
errorResponse,
'Failed to reload the component state.'
)
errorContext: {
errors: [
this.errorHelper.getErrorString(
errorResponse,
'Failed to reload the component state.'
)
],
context: ErrorContextKey.COMPONENT_STATE
}
})
)
)

View File

@ -16,7 +16,7 @@
*/
import { createAction, props } from '@ngrx/store';
import { ErrorDetail } from './index';
import { ErrorContext, ErrorContextKey, ErrorDetail } from './index';
export const fullScreenError = createAction(
'[Error] Full Screen Error',
@ -25,9 +25,9 @@ export const fullScreenError = createAction(
export const snackBarError = createAction('[Error] Snackbar Error', props<{ error: string }>());
export const addBannerError = createAction('[Error] Add Banner Error', props<{ error: string }>());
export const addBannerError = createAction('[Error] Add Banner Error', props<{ errorContext: ErrorContext }>());
export const clearBannerErrors = createAction('[Error] Clear Banner Errors');
export const clearBannerErrors = createAction('[Error] Clear Banner Errors', props<{ context: ErrorContextKey }>());
export const resetErrorState = createAction('[Error] Reset Error State');

View File

@ -27,7 +27,7 @@ import {
import { produce } from 'immer';
export const initialState: ErrorState = {
bannerErrors: null,
bannerErrors: {},
fullScreenError: null,
routedToFullScreenError: false
};
@ -38,19 +38,25 @@ export const errorReducer = createReducer(
...state,
fullScreenError: errorDetail
})),
on(addBannerError, (state, { error }) => {
on(addBannerError, (state, { errorContext }) => {
return produce(state, (draftState) => {
if (draftState.bannerErrors === null) {
draftState.bannerErrors = [];
draftState.bannerErrors = {};
}
if (!draftState.bannerErrors[errorContext.context]) {
draftState.bannerErrors[errorContext.context] = [];
}
draftState.bannerErrors.push(error);
draftState.bannerErrors[errorContext.context].push(...errorContext.errors);
});
}),
on(clearBannerErrors, (state, { context }) => {
return produce(state, (draftState) => {
if (draftState.bannerErrors && draftState.bannerErrors[context]) {
delete draftState.bannerErrors[context];
}
});
}),
on(clearBannerErrors, (state) => ({
...state,
bannerErrors: null
})),
on(setRoutedToFullScreenError, (state, { routedToFullScreenError }) => ({
...state,
routedToFullScreenError

View File

@ -16,13 +16,16 @@
*/
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { errorFeatureKey, ErrorState } from './index';
import { BannerErrors, errorFeatureKey, ErrorState } from './index';
export const selectErrorState = createFeatureSelector<ErrorState>(errorFeatureKey);
export const selectFullScreenError = createSelector(selectErrorState, (state: ErrorState) => state.fullScreenError);
export const selectBannerErrors = createSelector(selectErrorState, (state: ErrorState) => state.bannerErrors);
export const selectAllBannerErrors = createSelector(selectErrorState, (state: ErrorState) => state.bannerErrors);
export const selectBannerErrors = (context: string) =>
createSelector(selectAllBannerErrors, (bannerErrors: BannerErrors) => bannerErrors[context] || []);
export const selectRoutedToFullScreenError = createSelector(
selectErrorState,

View File

@ -22,8 +22,48 @@ export interface ErrorDetail {
message: string;
}
export enum ErrorContextKey {
ACCESS_POLICIES = 'access-policies',
QUEUE = 'queue',
CLUSTER = 'cluster',
PROVENANCE = 'provenance',
COMPONENT_STATE = 'component-state',
STATUS_HISTORY = 'status-history',
SYSTEM_DIAGNOSTICS = 'system-diagnostics',
CONTROLLER_SERVICES = 'controller-services',
FLOW = 'flow',
MANAGE_REMOTE_PORTS = 'manage-remote-ports',
PARAMETER_CONTEXTS = 'parameter-contexts',
PARAMETER_PROVIDERS = 'parameter-providers',
REGISTRY_CLIENTS = 'registry-clients',
REPORTING_TASKS = 'report-tasks',
USERS = 'users',
PROCESS_GROUP = 'process-group',
REMOTE_PROCESS_GROUP = 'remote-process-group',
PROCESSOR = 'processor',
CONNECTION = 'connection',
PORT = 'port',
REGISTRY_IMPORT = 'registry-import',
LABEL = 'label',
FLOW_VERSION = 'flow-version',
FUNNEL = 'funnel',
LOCAL_EXTENSIONS = 'local-extensions',
LINEAGE = 'lineage',
FLOW_ANALYSIS_RULES = 'flow-analysis-rules'
}
export interface ErrorContext {
context: ErrorContextKey;
errors: string[];
}
export interface BannerErrors {
// key should be the ErrorContextKey of the banner error
[key: string]: string[];
}
export interface ErrorState {
bannerErrors: string[] | null;
bannerErrors: BannerErrors;
fullScreenError: ErrorDetail | null;
routedToFullScreenError: boolean;
}

View File

@ -28,6 +28,7 @@ import { StatusHistory } from '../../ui/common/status-history/status-history.com
import * as ErrorActions from '../../state/error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHelper } from '../../service/error-helper.service';
import { ErrorContextKey } from '../error';
@Injectable()
export class StatusHistoryEffects {
@ -200,7 +201,13 @@ export class StatusHistoryEffects {
this.actions$.pipe(
ofType(StatusHistoryActions.statusHistoryBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.STATUS_HISTORY }
})
)
)
)
);

View File

@ -29,6 +29,7 @@ import { LARGE_DIALOG } from 'libs/shared/src';
import * as ErrorActions from '../error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHelper } from '../../service/error-helper.service';
import { ErrorContextKey } from '../error';
@Injectable()
export class SystemDiagnosticsEffects {
@ -125,7 +126,13 @@ export class SystemDiagnosticsEffects {
this.actions$.pipe(
ofType(SystemDiagnosticsActions.systemDiagnosticsBannerError),
map((action) => action.error),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
switchMap((error) =>
of(
ErrorActions.addBannerError({
errorContext: { errors: [error], context: ErrorContextKey.SYSTEM_DIAGNOSTICS }
})
)
)
)
);

View File

@ -17,7 +17,7 @@
<div class="component-state-dialog">
<h2 mat-dialog-title>Component State</h2>
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.COMPONENT_STATE"></context-error-banner>
<mat-dialog-content>
<div class="dialog-content flex flex-col justify-between gap-y-5">
@if (componentName$ | async; as componentName) {

View File

@ -15,13 +15,13 @@
* limitations under the License.
*/
import { AfterViewInit, Component, DestroyRef, inject, Input, OnDestroy } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, inject, Input } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { AsyncPipe } from '@angular/common';
import { NifiTooltipDirective, NiFiCommon, CloseOnEscapeDialog } from '@nifi/shared';
import { CloseOnEscapeDialog, NiFiCommon, NifiTooltipDirective } from '@nifi/shared';
import { NifiSpinnerDirective } from '../spinner/nifi-spinner.directive';
import { ComponentStateState, StateEntry, StateItem, StateMap } from '../../../state/component-state';
import { Store } from '@ngrx/store';
@ -39,7 +39,8 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { clearBannerErrors } from '../../../state/error/error.actions';
import { ErrorContextKey } from '../../../state/error';
import { ContextErrorBanner } from '../context-error-banner/context-error-banner.component';
@Component({
selector: 'component-state',
@ -56,11 +57,12 @@ import { clearBannerErrors } from '../../../state/error/error.actions';
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
ErrorBanner
ErrorBanner,
ContextErrorBanner
],
styleUrls: ['./component-state.component.scss']
})
export class ComponentStateDialog extends CloseOnEscapeDialog implements AfterViewInit, OnDestroy {
export class ComponentStateDialog extends CloseOnEscapeDialog implements AfterViewInit {
@Input() initialSortColumn: 'key' | 'value' = 'key';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
@ -143,10 +145,6 @@ export class ComponentStateDialog extends CloseOnEscapeDialog implements AfterVi
});
}
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors());
}
processStateMap(stateMap: StateMap, clusterState: boolean): StateItem[] {
const stateEntries: StateEntry[] = stateMap.state ? stateMap.state : [];
@ -202,4 +200,6 @@ export class ComponentStateDialog extends CloseOnEscapeDialog implements AfterVi
clearState(): void {
this.store.dispatch(clearComponentState());
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -0,0 +1,20 @@
<!--
~ 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.
-->
<error-banner [messages]="messages$ | async" (dismiss)="dismiss()"></error-banner>

View File

@ -0,0 +1,18 @@
/*!
* 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.
*/

View File

@ -0,0 +1,51 @@
/*
* 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 { ContextErrorBanner } from './context-error-banner.component';
import { ErrorContextKey, errorFeatureKey } from '../../../state/error';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState as initialErrorState } from '../../../state/error/error.reducer';
describe('ContextErrorBanner', () => {
let component: ContextErrorBanner;
let fixture: ComponentFixture<ContextErrorBanner>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContextErrorBanner],
providers: [
provideMockStore({
initialState: {
[errorFeatureKey]: initialErrorState
}
})
]
}).compileComponents();
fixture = TestBed.createComponent(ContextErrorBanner);
component = fixture.componentInstance;
component.context = ErrorContextKey.ACCESS_POLICIES;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,61 @@
/*
* 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, DestroyRef, inject, Input, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NiFiState } from '../../../state';
import { Store } from '@ngrx/store';
import { clearBannerErrors } from '../../../state/error/error.actions';
import { Observable } from 'rxjs';
import { selectBannerErrors } from '../../../state/error/error.selectors';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { ErrorContextKey } from '../../../state/error';
@Component({
selector: 'context-error-banner',
standalone: true,
imports: [CommonModule, ErrorBanner],
templateUrl: './context-error-banner.component.html',
styleUrl: './context-error-banner.component.scss'
})
export class ContextErrorBanner implements OnDestroy {
private _context!: ErrorContextKey;
@Input({ required: true }) set context(context: ErrorContextKey) {
this._context = context;
this.messages$ = this.store.select(selectBannerErrors(this._context)).pipe(takeUntilDestroyed(this.destroyRef));
}
get context() {
return this._context;
}
messages$: Observable<string[]> | null = null;
private destroyRef: DestroyRef = inject(DestroyRef);
constructor(private store: Store<NiFiState>) {}
ngOnDestroy(): void {
this.store.dispatch(clearBannerErrors({ context: this.context }));
}
dismiss() {
this.store.dispatch(clearBannerErrors({ context: this.context }));
}
}

View File

@ -26,7 +26,7 @@
</div>
</h2>
<form class="controller-service-edit-form" [formGroup]="editControllerServiceForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.CONTROLLER_SERVICES"></context-error-banner>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>

View File

@ -24,8 +24,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
import 'codemirror/addon/hint/show-hint';
import { ErrorBanner } from '../../error-banner/error-banner.component';
import { MockComponent } from 'ng-mocks';
import { ContextErrorBanner } from '../../context-error-banner/context-error-banner.component';
describe('EditControllerService', () => {
let component: EditControllerService;
@ -548,7 +548,7 @@ describe('EditControllerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditControllerService, MockComponent(ErrorBanner), NoopAnimationsModule],
imports: [EditControllerService, MockComponent(ContextErrorBanner), NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{

View File

@ -52,6 +52,8 @@ import {
VerifyPropertiesRequestContext
} from '../../../../state/property-verification';
import { TabbedDialog } from '../../tabbed-dialog/tabbed-dialog.component';
import { ErrorContextKey } from '../../../../state/error';
import { ContextErrorBanner } from '../../context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-controller-service',
@ -73,7 +75,8 @@ import { TabbedDialog } from '../../tabbed-dialog/tabbed-dialog.component';
NifiSpinnerDirective,
ErrorBanner,
NifiTooltipDirective,
PropertyVerification
PropertyVerification,
ContextErrorBanner
],
styleUrls: ['./edit-controller-service.component.scss']
})
@ -214,4 +217,6 @@ export class EditControllerService extends TabbedDialog {
properties: this.getModifiedProperties()
});
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -17,7 +17,7 @@
<h2 mat-dialog-title>{{ isNew ? 'Add' : 'Edit' }} {{ isUser ? 'User' : 'User Group' }}</h2>
<form class="edit-tenant-form" [formGroup]="editTenantForm">
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.USERS"></context-error-banner>
<mat-dialog-content>
<div>
<mat-radio-group formControlName="tenantType" (change)="tenantTypeChanged()">

View File

@ -21,8 +21,8 @@ import { EditTenantDialog } from './edit-tenant-dialog.component';
import { EditTenantRequest } from '../../../state/shared';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { MockComponent } from 'ng-mocks';
import { ContextErrorBanner } from '../context-error-banner/context-error-banner.component';
describe('EditTenantDialog', () => {
let component: EditTenantDialog;
@ -786,7 +786,7 @@ describe('EditTenantDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditTenantDialog, MockComponent(ErrorBanner), NoopAnimationsModule],
imports: [EditTenantDialog, MockComponent(ContextErrorBanner), NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{ provide: MatDialogRef, useValue: null }

View File

@ -41,6 +41,8 @@ import { MatListModule } from '@angular/material/list';
import { Client } from '../../../service/client.service';
import { NiFiCommon, CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorBanner } from '../error-banner/error-banner.component';
import { ErrorContextKey } from '../../../state/error';
import { ContextErrorBanner } from '../context-error-banner/context-error-banner.component';
@Component({
selector: 'edit-tenant-dialog',
@ -57,7 +59,8 @@ import { ErrorBanner } from '../error-banner/error-banner.component';
NifiSpinnerDirective,
AsyncPipe,
MatListModule,
ErrorBanner
ErrorBanner,
ContextErrorBanner
],
templateUrl: './edit-tenant-dialog.component.html',
styleUrls: ['./edit-tenant-dialog.component.scss']
@ -258,4 +261,6 @@ export class EditTenantDialog extends CloseOnEscapeDialog {
override isDirty(): boolean {
return this.editTenantForm.dirty;
}
protected readonly ErrorContextKey = ErrorContextKey;
}

View File

@ -15,9 +15,16 @@
~ limitations under the License.
-->
@if ((messages$ | async)!; as messages) {
<div class="mb-4">
<div class="banner-container border-t border-b px-6 py-3 flex justify-between items-center">
@if (messages && messages.length > 0) {
<div
class="banner-container px-4 py-3 mb-5 flex flex-col justify-between text-sm"
[ngClass]="{ 'border-t': showBorder, 'border-b': showBorder }">
<div
class="flex gap-4"
[ngClass]="{ 'items-start': messages.length > 1, 'items-center': messages.length === 1 }">
@if (showErrorIcon) {
<i class="fa fa-exclamation-triangle error-color fa-2x"></i>
}
@if (messages.length === 1) {
<div>{{ messages[0] }}</div>
} @else {
@ -27,9 +34,9 @@
}
</ul>
}
<div class="flex flex-col mt-auto">
<button mat-stroked-button (click)="dismiss()">Dismiss</button>
</div>
</div>
<div class="flex flex-1 mt-auto justify-end w-full">
<button mat-flat-button class="error-button" (click)="dismissClicked()">Dismiss</button>
</div>
</div>
}

View File

@ -18,8 +18,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorBanner } from './error-banner.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/error/error.reducer';
describe('ErrorBanner', () => {
let component: ErrorBanner;
@ -27,8 +25,7 @@ describe('ErrorBanner', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ErrorBanner],
providers: [provideMockStore({ initialState })]
imports: [ErrorBanner]
});
fixture = TestBed.createComponent(ErrorBanner);
component = fixture.componentInstance;

View File

@ -15,27 +15,25 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AsyncPipe } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { NgClass } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { NiFiState } from '../../../state';
import { selectBannerErrors } from '../../../state/error/error.selectors';
import { clearBannerErrors } from '../../../state/error/error.actions';
@Component({
selector: 'error-banner',
standalone: true,
imports: [MatButtonModule, AsyncPipe],
imports: [MatButtonModule, NgClass],
templateUrl: './error-banner.component.html',
styleUrls: ['./error-banner.component.scss']
})
export class ErrorBanner {
messages$ = this.store.select(selectBannerErrors);
@Input() messages: string[] | null = null;
@Input() showErrorIcon = true;
@Input() showBorder = true;
constructor(private store: Store<NiFiState>) {}
@Output() dismiss: EventEmitter<void> = new EventEmitter<void>();
dismiss(): void {
this.store.dispatch(clearBannerErrors());
dismissClicked(): void {
this.dismiss.next();
}
}

View File

@ -42,7 +42,7 @@
}
}
</div>
<error-banner></error-banner>
<context-error-banner [context]="ErrorContextKey.STATUS_HISTORY"></context-error-banner>
<div class="status-history flex flex-col grow">
<mat-dialog-content class="grow flex flex-1">
<form [formGroup]="statusHistoryForm" class="flex flex-1 h-full">

Some files were not shown because too many files have changed in this diff Show More