mirror of https://github.com/apache/nifi.git
NIFI-12663: Error Handling in CS Listing (#8305)
* NIFI-12663: - Handling API error responses in the Management Controller Services page. * NIFI-12679: - Renaming components based on review feedback. - Using ng-content in page-content component. - Removing the problematic route when navigating to the error page. - Fixing logic when handling service loading errors. - Handling errors in the Property Table Helper service. * NIFI-12679: - Addressing review feedback.
This commit is contained in:
parent
c1a21ad078
commit
8e0c68e5cc
|
@ -24,6 +24,10 @@ const routes: Routes = [
|
|||
path: 'login',
|
||||
loadChildren: () => import('./pages/login/feature/login.module').then((m) => m.LoginModule)
|
||||
},
|
||||
{
|
||||
path: 'error',
|
||||
loadChildren: () => import('./pages/error/feature/error.module').then((m) => m.ErrorModule)
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
canMatch: [authenticationGuard],
|
||||
|
|
|
@ -42,6 +42,8 @@ import { ControllerServiceStateEffects } from './state/contoller-service-state/c
|
|||
import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diagnostics.effects';
|
||||
import { FlowConfigurationEffects } from './state/flow-configuration/flow-configuration.effects';
|
||||
import { ComponentStateEffects } from './state/component-state/component-state.effects';
|
||||
import { ErrorEffects } from './state/error/error.effects';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
|
@ -60,6 +62,7 @@ import { ComponentStateEffects } from './state/component-state/component-state.e
|
|||
navigationActionTiming: NavigationActionTiming.PostActivation
|
||||
}),
|
||||
EffectsModule.forRoot(
|
||||
ErrorEffects,
|
||||
CurrentUserEffects,
|
||||
ExtensionTypesEffects,
|
||||
AboutEffects,
|
||||
|
@ -76,7 +79,8 @@ import { ComponentStateEffects } from './state/component-state/component-state.e
|
|||
}),
|
||||
MatProgressSpinnerModule,
|
||||
MatNativeDateModule,
|
||||
MatDialogModule
|
||||
MatDialogModule,
|
||||
MatSnackBarModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { Error } from './error.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: Error }];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ErrorRoutingModule {}
|
|
@ -15,4 +15,15 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div *ngIf="message" [ngClass]="{ error: severity == 'alert' }" class="p-2 text-center text-sm">{{ message }}</div>
|
||||
<div class="error-background pt-24 pl-24 h-screen">
|
||||
<ng-container *ngIf="errorDetail$ | async; let errorDetail; else: noErrorDetails">
|
||||
<page-content [title]="errorDetail.title">
|
||||
<div class="text-sm">{{ errorDetail.message }}</div>
|
||||
</page-content>
|
||||
</ng-container>
|
||||
<ng-template #noErrorDetails>
|
||||
<page-content title="Error">
|
||||
<div class="text-sm">Please check the logs or navigate home and try again.</div>
|
||||
</page-content>
|
||||
</ng-template>
|
||||
</div>
|
|
@ -15,8 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
div {
|
||||
&.error {
|
||||
background-color: #ffcdd2;
|
||||
}
|
||||
.error-background {
|
||||
background: #fff url(../../../../assets/icons/bg-error.png) left top no-repeat;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { Error } from './error.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../state/current-user/current-user.reducer';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
describe('Error', () => {
|
||||
let component: Error;
|
||||
let fixture: ComponentFixture<Error>;
|
||||
|
||||
@Component({
|
||||
selector: 'page-content',
|
||||
standalone: true,
|
||||
template: ''
|
||||
})
|
||||
class MockPageContent {}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [Error],
|
||||
imports: [MockPageContent],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(Error);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectFullScreenError } from '../../../state/error/error.selectors';
|
||||
import { NiFiState } from '../../../state';
|
||||
|
||||
@Component({
|
||||
selector: 'error',
|
||||
templateUrl: './error.component.html',
|
||||
styleUrls: ['./error.component.scss']
|
||||
})
|
||||
export class Error {
|
||||
errorDetail$ = this.store.select(selectFullScreenError);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Error } from './error.component';
|
||||
import { ErrorRoutingModule } from './error-routing.module';
|
||||
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Error],
|
||||
exports: [Error],
|
||||
imports: [CommonModule, ErrorRoutingModule, PageContent]
|
||||
})
|
||||
export class ErrorModule {}
|
|
@ -18,7 +18,6 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CanvasUtils } from './canvas-utils.service';
|
||||
import {
|
||||
ComponentRunStatusRequest,
|
||||
CreateComponentRequest,
|
||||
|
@ -48,7 +47,6 @@ export class FlowService implements PropertyDescriptorRetriever {
|
|||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private canvasUtils: CanvasUtils,
|
||||
private client: Client,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
@ -198,12 +196,10 @@ export class FlowService implements PropertyDescriptorRetriever {
|
|||
}
|
||||
|
||||
updateComponent(updateComponent: UpdateComponentRequest): Observable<any> {
|
||||
// return throwError('API Error');
|
||||
return this.httpClient.put(this.nifiCommon.stripProtocol(updateComponent.uri), updateComponent.payload);
|
||||
}
|
||||
|
||||
deleteComponent(deleteComponent: DeleteComponentRequest): Observable<any> {
|
||||
// return throwError('API Error');
|
||||
const revision: any = this.client.getRevision(deleteComponent.entity);
|
||||
return this.httpClient.delete(this.nifiCommon.stripProtocol(deleteComponent.uri), { params: revision });
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<h2 mat-dialog-title>Create New {{ portTypeLabel }}</h2>
|
||||
<form class="create-port-form" [formGroup]="createPortForm">
|
||||
<banner></banner>
|
||||
<error-banner></error-banner>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field>
|
||||
<mat-label>{{ portTypeLabel }} Name</mat-label>
|
||||
|
|
|
@ -28,7 +28,7 @@ import { ComponentType, SelectOption, TextTipInput } from '../../../../../../../
|
|||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Banner } from '../../../../common/banner/banner.component';
|
||||
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
|
||||
|
@ -44,7 +44,7 @@ import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/ni
|
|||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatTooltipModule,
|
||||
Banner,
|
||||
ErrorBanner,
|
||||
NgIf,
|
||||
NgForOf,
|
||||
MatButtonModule,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<h2 mat-dialog-title>Edit {{ portTypeLabel }}</h2>
|
||||
<form class="edit-port-form" [formGroup]="editPortForm">
|
||||
<banner></banner>
|
||||
<error-banner></error-banner>
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
|
|
|
@ -24,7 +24,7 @@ import { updateComponent } from '../../../../../state/flow/flow.actions';
|
|||
import { Client } from '../../../../../../../service/client.service';
|
||||
import { EditComponentDialogRequest } from '../../../../../state/flow';
|
||||
import { ComponentType } from '../../../../../../../state/shared';
|
||||
import { Banner } from '../../../../common/banner/banner.component';
|
||||
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
@ -38,7 +38,7 @@ import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nif
|
|||
templateUrl: './edit-port.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
Banner,
|
||||
ErrorBanner,
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
|
@ -68,10 +68,6 @@ export class EditPort {
|
|||
this.portTypeLabel = 'Output Port';
|
||||
}
|
||||
|
||||
// TODO - consider updating the request to only provide the id of the port and selecting that item
|
||||
// from the store. this would also allow us to be informed when another client has submitted an
|
||||
// update to the same port which this editing is happening
|
||||
|
||||
// build the form
|
||||
this.editPortForm = this.formBuilder.group({
|
||||
name: new FormControl(request.entity.component.name, Validators.required),
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<h2 mat-dialog-title>Create Process Group</h2>
|
||||
<form class="create-process-group-form" [formGroup]="createProcessGroupForm">
|
||||
<error-banner></error-banner>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
|
|
|
@ -24,7 +24,7 @@ import { createProcessGroup, uploadProcessGroup } from '../../../../../state/flo
|
|||
import { SelectOption, TextTipInput } from '../../../../../../../state/shared';
|
||||
import { selectSaving } from '../../../../../state/flow/flow.selectors';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { Banner } from '../../../../common/banner/banner.component';
|
||||
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
@ -42,7 +42,7 @@ import { NiFiCommon } from '../../../../../../../service/nifi-common.service';
|
|||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
Banner,
|
||||
ErrorBanner,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
|
|
@ -24,7 +24,6 @@ import { groupComponents } from '../../../../../state/flow/flow.actions';
|
|||
import { ComponentType, SelectOption, TextTipInput } from '../../../../../../../state/shared';
|
||||
import { selectSaving } from '../../../../../state/flow/flow.selectors';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { Banner } from '../../../../common/banner/banner.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
@ -42,7 +41,6 @@ import { Client } from '../../../../../../../service/client.service';
|
|||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
Banner,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<h2 mat-dialog-title>Edit Processor</h2>
|
||||
<form class="processor-edit-form" [formGroup]="editProcessorForm">
|
||||
<error-banner></error-banner>
|
||||
<!-- TODO - Stop & Configure -->
|
||||
<mat-dialog-content>
|
||||
<mat-tab-group>
|
||||
|
|
|
@ -22,6 +22,9 @@ import { EditComponentDialogRequest } from '../../../../../state/flow';
|
|||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { ComponentType } from '../../../../../../../state/shared';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Component } from '@angular/core';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../../../../state/error/error.reducer';
|
||||
|
||||
describe('EditProcessor', () => {
|
||||
let component: EditProcessor;
|
||||
|
@ -719,10 +722,22 @@ describe('EditProcessor', () => {
|
|||
}
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'error-banner',
|
||||
standalone: true,
|
||||
template: ''
|
||||
})
|
||||
class MockErrorBanner {}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [EditProcessor, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
imports: [EditProcessor, MockErrorBanner, BrowserAnimationsModule],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(EditProcessor);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
RelationshipConfiguration,
|
||||
RelationshipSettings
|
||||
} from './relationship-settings/relationship-settings.component';
|
||||
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
|
||||
|
||||
@Component({
|
||||
selector: 'edit-processor',
|
||||
|
@ -68,7 +69,8 @@ import {
|
|||
NifiSpinnerDirective,
|
||||
NifiTooltipDirective,
|
||||
RunDurationSlider,
|
||||
RelationshipSettings
|
||||
RelationshipSettings,
|
||||
ErrorBanner
|
||||
],
|
||||
styleUrls: ['./edit-processor.component.scss']
|
||||
})
|
||||
|
|
|
@ -23,12 +23,16 @@
|
|||
|
||||
<ng-template #loaded>
|
||||
<ng-container *ngIf="access.error; else noErrors">
|
||||
<login-message [title]="access.error.title" [message]="access.error.message"></login-message>
|
||||
<page-content [title]="access.error.title">
|
||||
<div class="text-sm">{{ access.error.message }}</div>
|
||||
</page-content>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noErrors>
|
||||
<ng-container *ngIf="access.accessStatus.status === 'ACTIVE'; else needsLogin">
|
||||
<login-message [title]="'Success'" [message]="access.accessStatus.message"></login-message>
|
||||
<page-content [title]="'Success'">
|
||||
<div class="text-sm">{{ access.accessStatus.message }}</div>
|
||||
</page-content>
|
||||
</ng-container>
|
||||
<ng-template #needsLogin>
|
||||
<ng-container *ngIf="access.accessConfig.supportsLogin; else loginNotSupported">
|
||||
|
@ -36,11 +40,9 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-template #loginNotSupported>
|
||||
<login-message
|
||||
[title]="'Access Denied'"
|
||||
[message]="
|
||||
'This NiFi is not configured to support username/password logins.'
|
||||
"></login-message>
|
||||
<page-content [title]="'Access Denied'">
|
||||
<div class="text-sm">This NiFi is not configured to support username/password logins.</div>
|
||||
</page-content>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
|
|
@ -28,11 +28,11 @@ import { EffectsModule } from '@ngrx/effects';
|
|||
import { loginFeatureKey, reducers } from '../state';
|
||||
import { AccessEffects } from '../state/access/access.effects';
|
||||
import { LoginForm } from '../ui/login-form/login-form.component';
|
||||
import { LoginMessage } from '../ui/login-message/login-message.component';
|
||||
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Login, LoginForm, LoginMessage],
|
||||
declarations: [Login, LoginForm],
|
||||
exports: [Login],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -44,7 +44,8 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
NgxSkeletonLoaderModule
|
||||
NgxSkeletonLoaderModule,
|
||||
PageContent
|
||||
]
|
||||
})
|
||||
export class LoginModule {}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="login-form w-96 flex flex-col gap-y-3">
|
||||
<div class="login-form w-96 flex flex-col gap-y-5">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="login-title">Log In</div>
|
||||
<div class="flex gap-x-2">
|
||||
|
|
|
@ -57,6 +57,5 @@ export interface ManagementControllerServicesState {
|
|||
controllerServices: ControllerServiceEntity[];
|
||||
saving: boolean;
|
||||
loadedTimestamp: string;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
status: 'pending' | 'loading' | 'success';
|
||||
}
|
||||
|
|
|
@ -45,8 +45,8 @@ export const loadManagementControllerServicesSuccess = createAction(
|
|||
props<{ response: LoadManagementControllerServicesResponse }>()
|
||||
);
|
||||
|
||||
export const managementControllerServicesApiError = createAction(
|
||||
'[Management Controller Services] Load Management Controller Services Error',
|
||||
export const managementControllerServicesBannerApiError = createAction(
|
||||
'[Management Controller Services] Management Controller Services Banner Api Error',
|
||||
props<{ error: string }>()
|
||||
);
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as ManagementControllerServicesActions from './management-controller-services.actions';
|
||||
import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap } from 'rxjs';
|
||||
import * as ErrorActions from '../../../../state/error/error.actions';
|
||||
import { catchError, from, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
@ -31,17 +32,15 @@ import { EditControllerService } from '../../../../ui/common/controller-service/
|
|||
import {
|
||||
ComponentType,
|
||||
ControllerServiceReferencingComponent,
|
||||
InlineServiceCreationRequest,
|
||||
InlineServiceCreationResponse,
|
||||
PropertyDescriptor,
|
||||
UpdateControllerServiceRequest
|
||||
} from '../../../../state/shared';
|
||||
import { Router } from '@angular/router';
|
||||
import { ExtensionTypesService } from '../../../../service/extension-types.service';
|
||||
import { selectSaving } from './management-controller-services.selectors';
|
||||
import { selectSaving, selectStatus } from './management-controller-services.selectors';
|
||||
import { EnableControllerService } from '../../../../ui/common/controller-service/enable-controller-service/enable-controller-service.component';
|
||||
import { DisableControllerService } from '../../../../ui/common/controller-service/disable-controller-service/disable-controller-service.component';
|
||||
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ErrorHelper } from '../../../../service/error-helper.service';
|
||||
|
||||
@Injectable()
|
||||
export class ManagementControllerServicesEffects {
|
||||
|
@ -50,7 +49,7 @@ export class ManagementControllerServicesEffects {
|
|||
private store: Store<NiFiState>,
|
||||
private client: Client,
|
||||
private managementControllerServiceService: ManagementControllerServiceService,
|
||||
private extensionTypesService: ExtensionTypesService,
|
||||
private errorHelper: ErrorHelper,
|
||||
private dialog: MatDialog,
|
||||
private router: Router,
|
||||
private propertyTableHelperService: PropertyTableHelperService
|
||||
|
@ -59,7 +58,8 @@ export class ManagementControllerServicesEffects {
|
|||
loadManagementControllerServices$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ManagementControllerServicesActions.loadManagementControllerServices),
|
||||
switchMap(() =>
|
||||
concatLatestFrom(() => this.store.select(selectStatus)),
|
||||
switchMap(([action, status]) =>
|
||||
from(this.managementControllerServiceService.getControllerServices()).pipe(
|
||||
map((response) =>
|
||||
ManagementControllerServicesActions.loadManagementControllerServicesSuccess({
|
||||
|
@ -69,13 +69,17 @@ export class ManagementControllerServicesEffects {
|
|||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ManagementControllerServicesActions.managementControllerServicesApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
if (status === 'success') {
|
||||
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
|
||||
return of(ErrorActions.snackBarError({ error: errorResponse.error }));
|
||||
} else {
|
||||
return of(this.errorHelper.fullScreenError(errorResponse));
|
||||
}
|
||||
} else {
|
||||
return of(this.errorHelper.fullScreenError(errorResponse));
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -130,13 +134,10 @@ export class ManagementControllerServicesEffects {
|
|||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ManagementControllerServicesActions.managementControllerServicesApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
this.dialog.closeAll();
|
||||
return of(ErrorActions.snackBarError({ error: errorResponse.error }));
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -265,6 +266,8 @@ export class ManagementControllerServicesEffects {
|
|||
});
|
||||
|
||||
editDialogReference.afterClosed().subscribe((response) => {
|
||||
this.store.dispatch(ErrorActions.clearBannerErrors());
|
||||
|
||||
if (response != 'ROUTED') {
|
||||
this.store.dispatch(
|
||||
ManagementControllerServicesActions.selectControllerService({
|
||||
|
@ -295,18 +298,31 @@ export class ManagementControllerServicesEffects {
|
|||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ManagementControllerServicesActions.managementControllerServicesApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
if (this.errorHelper.showErrorInContext(errorResponse.status)) {
|
||||
return of(
|
||||
ManagementControllerServicesActions.managementControllerServicesBannerApiError({
|
||||
error: errorResponse.error
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.dialog.getDialogById(request.id)?.close('ROUTED');
|
||||
return of(this.errorHelper.fullScreenError(errorResponse));
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
managementControllerServicesBannerApiError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ManagementControllerServicesActions.managementControllerServicesBannerApiError),
|
||||
map((action) => action.error),
|
||||
switchMap((error) => of(ErrorActions.addBannerError({ error })))
|
||||
)
|
||||
);
|
||||
|
||||
configureControllerServiceSuccess$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
|
@ -425,13 +441,9 @@ export class ManagementControllerServicesEffects {
|
|||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ManagementControllerServicesActions.managementControllerServicesApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
return of(ErrorActions.snackBarError({ error: errorResponse.error }));
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
inlineCreateControllerServiceSuccess,
|
||||
loadManagementControllerServices,
|
||||
loadManagementControllerServicesSuccess,
|
||||
managementControllerServicesApiError,
|
||||
managementControllerServicesBannerApiError,
|
||||
resetManagementControllerServicesState
|
||||
} from './management-controller-services.actions';
|
||||
import { produce } from 'immer';
|
||||
|
@ -36,7 +36,6 @@ export const initialState: ManagementControllerServicesState = {
|
|||
controllerServices: [],
|
||||
saving: false,
|
||||
loadedTimestamp: '',
|
||||
error: null,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
|
@ -53,14 +52,11 @@ export const managementControllerServicesReducer = createReducer(
|
|||
...state,
|
||||
controllerServices: response.controllerServices,
|
||||
loadedTimestamp: response.loadedTimestamp,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(managementControllerServicesApiError, (state, { error }) => ({
|
||||
on(managementControllerServicesBannerApiError, (state, { error }) => ({
|
||||
...state,
|
||||
saving: false,
|
||||
error,
|
||||
status: 'error' as const
|
||||
saving: false
|
||||
})),
|
||||
on(createControllerService, configureControllerService, deleteControllerService, (state, { request }) => ({
|
||||
...state,
|
||||
|
|
|
@ -31,6 +31,11 @@ export const selectSaving = createSelector(
|
|||
(state: ManagementControllerServicesState) => state.saving
|
||||
);
|
||||
|
||||
export const selectStatus = createSelector(
|
||||
selectManagementControllerServicesState,
|
||||
(state: ManagementControllerServicesState) => state.status
|
||||
);
|
||||
|
||||
export const selectControllerServiceIdFromRoute = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route) {
|
||||
// always select the controller service from the route
|
||||
|
|
|
@ -20,10 +20,11 @@ import { CommonModule } from '@angular/common';
|
|||
import { ManagementControllerServices } from './management-controller-services.component';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { ControllerServiceTable } from '../../../../ui/common/controller-service/controller-service-table/controller-service-table.component';
|
||||
import { ErrorBanner } from '../../../../ui/common/error-banner/error-banner.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ManagementControllerServices],
|
||||
exports: [ManagementControllerServices],
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule, ControllerServiceTable]
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule, ControllerServiceTable, ErrorBanner]
|
||||
})
|
||||
export class ManagementControllerServicesModule {}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { Injectable } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import * as ErrorDetailActions from '../state/error/error.actions';
|
||||
import { Action } from '@ngrx/store';
|
||||
import { NiFiCommon } from './nifi-common.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ErrorHelper {
|
||||
constructor(private nifiCommon: NiFiCommon) {}
|
||||
|
||||
fullScreenError(errorResponse: HttpErrorResponse): Action {
|
||||
let title: string;
|
||||
let message: string;
|
||||
|
||||
switch (errorResponse.status) {
|
||||
case 401:
|
||||
title = 'Unauthorized';
|
||||
break;
|
||||
case 403:
|
||||
title = 'Insufficient Permissions';
|
||||
break;
|
||||
case 409:
|
||||
title = 'Invalid State';
|
||||
break;
|
||||
case 413:
|
||||
title = 'Payload Too Large';
|
||||
break;
|
||||
case 503:
|
||||
default:
|
||||
title = 'An unexpected error has occurred';
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.nifiCommon.isBlank(errorResponse.error)) {
|
||||
message =
|
||||
'An error occurred communicating with NiFi. Please check the logs and fix any configuration issues before restarting.';
|
||||
} else {
|
||||
message = errorResponse.error;
|
||||
}
|
||||
|
||||
return ErrorDetailActions.fullScreenError({
|
||||
errorDetail: {
|
||||
title,
|
||||
message
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showErrorInContext(status: number): boolean {
|
||||
return [400, 403, 404, 409, 413, 503].includes(status);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Bundle } from '../state/shared';
|
||||
|
||||
|
|
|
@ -80,7 +80,9 @@ export const authenticationGuard: CanMatchFn = (route, state) => {
|
|||
.select(selectCurrentUserState)
|
||||
.pipe(take(1))
|
||||
.subscribe((userState) => {
|
||||
if (userState.status == 'pending') {
|
||||
if (userState.status == 'success') {
|
||||
resolve(true);
|
||||
} else {
|
||||
userService
|
||||
.getUser()
|
||||
.pipe(take(1))
|
||||
|
@ -118,7 +120,7 @@ export const authenticationGuard: CanMatchFn = (route, state) => {
|
|||
}
|
||||
},
|
||||
error: (error) => {
|
||||
// there is no anonymous access and we don't know this user - open the login page which handles login/registration/etc
|
||||
// there is no anonymous access and we don't know this user - open the login page which handles login
|
||||
if (error.status === 401) {
|
||||
authStorage.removeToken();
|
||||
window.location.href = './login';
|
||||
|
@ -126,8 +128,6 @@ export const authenticationGuard: CanMatchFn = (route, state) => {
|
|||
resolve(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,12 +18,20 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthInterceptor } from './auth.interceptor';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/error/error.reducer';
|
||||
|
||||
describe('AuthInterceptor', () => {
|
||||
let service: AuthInterceptor;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(AuthInterceptor);
|
||||
});
|
||||
|
||||
|
|
|
@ -19,25 +19,54 @@ import { Injectable } from '@angular/core';
|
|||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { AuthStorage } from '../auth-storage.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../state';
|
||||
import { fullScreenError } from '../../state/error/error.actions';
|
||||
import { NiFiCommon } from '../nifi-common.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
constructor(private authStorage: AuthStorage) {}
|
||||
routedToFullScreenError: boolean = false;
|
||||
|
||||
constructor(
|
||||
private authStorage: AuthStorage,
|
||||
private store: Store<NiFiState>,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(
|
||||
tap({
|
||||
error: (error) => {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
if (error.status === 401) {
|
||||
this.authStorage.removeToken();
|
||||
error: (errorResponse) => {
|
||||
if (errorResponse instanceof HttpErrorResponse) {
|
||||
if (errorResponse.status === 401) {
|
||||
if (this.authStorage.hasToken()) {
|
||||
this.authStorage.removeToken();
|
||||
|
||||
// navigate to the root of the app which will handle redirection to the
|
||||
// login form if appropriate... TODO - replace with logout complete page?
|
||||
window.location.href = './login';
|
||||
} // TODO handle others (403)
|
||||
let message: string = errorResponse.error;
|
||||
if (this.nifiCommon.isBlank(message)) {
|
||||
message = 'Your session has expired. Please navigate home to log in again.';
|
||||
} else {
|
||||
message += '. Please navigate home to log in again.';
|
||||
}
|
||||
|
||||
this.routedToFullScreenError = true;
|
||||
|
||||
this.store.dispatch(
|
||||
fullScreenError({
|
||||
errorDetail: {
|
||||
title: 'Unauthorized',
|
||||
message
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (!this.routedToFullScreenError) {
|
||||
// the user has never logged in, redirect them to do so
|
||||
window.location.href = './login';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { catchError, map, NEVER, Observable, switchMap, take } from 'rxjs';
|
||||
import { catchError, EMPTY, map, Observable, switchMap, take, takeUntil, tap } from 'rxjs';
|
||||
import {
|
||||
ControllerServiceCreator,
|
||||
ControllerServiceEntity,
|
||||
|
@ -32,9 +32,12 @@ import {
|
|||
} from '../state/shared';
|
||||
import { NewPropertyDialog } from '../ui/common/new-property-dialog/new-property-dialog.component';
|
||||
import { CreateControllerService } from '../ui/common/controller-service/create-controller-service/create-controller-service.component';
|
||||
import { ManagementControllerServiceService } from '../pages/settings/service/management-controller-service.service';
|
||||
import { ExtensionTypesService } from './extension-types.service';
|
||||
import { Client } from './client.service';
|
||||
import { NiFiState } from '../state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { snackBarError } from '../state/error/error.actions';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -42,6 +45,7 @@ import { Client } from './client.service';
|
|||
export class PropertyTableHelperService {
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private store: Store<NiFiState>,
|
||||
private extensionTypesService: ExtensionTypesService,
|
||||
private client: Client
|
||||
) {}
|
||||
|
@ -63,12 +67,19 @@ export class PropertyTableHelperService {
|
|||
});
|
||||
|
||||
return newPropertyDialogReference.componentInstance.newProperty.pipe(
|
||||
take(1),
|
||||
takeUntil(newPropertyDialogReference.afterClosed()),
|
||||
switchMap((dialogResponse: NewPropertyDialogResponse) => {
|
||||
return propertyDescriptorService
|
||||
.getPropertyDescriptor(id, dialogResponse.name, dialogResponse.sensitive)
|
||||
.pipe(
|
||||
take(1),
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
this.store.dispatch(snackBarError({ error: errorResponse.error }));
|
||||
|
||||
// handle the error here to keep the observable alive so the
|
||||
// user can attempt to create the property again
|
||||
return EMPTY;
|
||||
}),
|
||||
map((response) => {
|
||||
newPropertyDialogReference.close();
|
||||
|
||||
|
@ -112,6 +123,11 @@ export class PropertyTableHelperService {
|
|||
)
|
||||
.pipe(
|
||||
take(1),
|
||||
tap({
|
||||
error: (errorResponse: HttpErrorResponse) => {
|
||||
this.store.dispatch(snackBarError({ error: errorResponse.error }));
|
||||
}
|
||||
}),
|
||||
switchMap((implementingTypesResponse) => {
|
||||
// show the create controller service dialog with the types that implemented the interface
|
||||
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
|
||||
|
@ -122,7 +138,7 @@ export class PropertyTableHelperService {
|
|||
});
|
||||
|
||||
return createServiceDialogReference.componentInstance.createControllerService.pipe(
|
||||
take(1),
|
||||
takeUntil(createServiceDialogReference.afterClosed()),
|
||||
switchMap((controllerServiceType) => {
|
||||
// typically this sequence would be implemented with ngrx actions, however we are
|
||||
// currently in an edit session, and we need to return both the value (new service id)
|
||||
|
@ -142,6 +158,17 @@ export class PropertyTableHelperService {
|
|||
|
||||
return controllerServiceCreator.createControllerService(payload).pipe(
|
||||
take(1),
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
this.store.dispatch(
|
||||
snackBarError({
|
||||
error: `Unable to create new Service: ${errorResponse.error}`
|
||||
})
|
||||
);
|
||||
|
||||
// handle the error here to keep the observable alive so the
|
||||
// user can attempt to create the service again
|
||||
return EMPTY;
|
||||
}),
|
||||
switchMap((createResponse) => {
|
||||
// if provided, call the callback function
|
||||
if (afterServiceCreated) {
|
||||
|
@ -153,6 +180,20 @@ export class PropertyTableHelperService {
|
|||
.getPropertyDescriptor(id, descriptor.name, false)
|
||||
.pipe(
|
||||
take(1),
|
||||
tap({
|
||||
error: (errorResponse: HttpErrorResponse) => {
|
||||
// we've errored getting the descriptor but since the service
|
||||
// was already created, we should close the create service dialog
|
||||
// so multiple service instances are not inadvertently created
|
||||
createServiceDialogReference.close();
|
||||
|
||||
this.store.dispatch(
|
||||
snackBarError({
|
||||
error: `Service created but unable to reload Property Descriptor: ${errorResponse.error}`
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
map((descriptorResponse) => {
|
||||
createServiceDialogReference.close();
|
||||
|
||||
|
@ -162,10 +203,6 @@ export class PropertyTableHelperService {
|
|||
};
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((error) => {
|
||||
// TODO - show error
|
||||
return NEVER;
|
||||
})
|
||||
);
|
||||
})
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { createAction, props } from '@ngrx/store';
|
||||
import { ErrorDetail } from './index';
|
||||
|
||||
export const fullScreenError = createAction('[Error] Full Screen Error', props<{ errorDetail: ErrorDetail }>());
|
||||
|
||||
export const snackBarError = createAction('[Error] Snackbar Error', props<{ error: string }>());
|
||||
|
||||
export const addBannerError = createAction('[Error] Add Banner Error', props<{ error: string }>());
|
||||
|
||||
export const clearBannerErrors = createAction('[Error] Clear Banner Errors');
|
||||
|
||||
export const resetErrorState = createAction('[Error] Reset Error State');
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as ErrorActions from './error.actions';
|
||||
import { map, tap } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Injectable()
|
||||
export class ErrorEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
fullScreenError$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ErrorActions.fullScreenError),
|
||||
tap(() => {
|
||||
this.router.navigate(['/error'], { replaceUrl: true });
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
snackBarError$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ErrorActions.snackBarError),
|
||||
map((action) => action.error),
|
||||
tap((error) => {
|
||||
this.snackBar.open(error, 'Dismiss', { duration: 30000 });
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { createReducer, on } from '@ngrx/store';
|
||||
import { ErrorState } from './index';
|
||||
import { resetErrorState, fullScreenError, addBannerError, clearBannerErrors } from './error.actions';
|
||||
import { produce } from 'immer';
|
||||
|
||||
export const initialState: ErrorState = {
|
||||
bannerErrors: null,
|
||||
fullScreenError: null
|
||||
};
|
||||
|
||||
export const errorReducer = createReducer(
|
||||
initialState,
|
||||
on(fullScreenError, (state, { errorDetail }) => ({
|
||||
...state,
|
||||
fullScreenError: errorDetail
|
||||
})),
|
||||
on(addBannerError, (state, { error }) => {
|
||||
return produce(state, (draftState) => {
|
||||
if (draftState.bannerErrors === null) {
|
||||
draftState.bannerErrors = [];
|
||||
}
|
||||
|
||||
draftState.bannerErrors.push(error);
|
||||
});
|
||||
}),
|
||||
on(clearBannerErrors, (state) => ({
|
||||
...state,
|
||||
bannerErrors: null
|
||||
})),
|
||||
on(resetErrorState, (state) => ({
|
||||
...initialState
|
||||
}))
|
||||
);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { 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);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const errorFeatureKey = 'error';
|
||||
|
||||
export interface ErrorDetail {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ErrorState {
|
||||
bannerErrors: string[] | null;
|
||||
fullScreenError: ErrorDetail | null;
|
||||
}
|
|
@ -33,9 +33,12 @@ import { flowConfigurationFeatureKey, FlowConfigurationState } from './flow-conf
|
|||
import { flowConfigurationReducer } from './flow-configuration/flow-configuration.reducer';
|
||||
import { componentStateFeatureKey, ComponentStateState } from './component-state';
|
||||
import { componentStateReducer } from './component-state/component-state.reducer';
|
||||
import { errorFeatureKey, ErrorState } from './error';
|
||||
import { errorReducer } from './error/error.reducer';
|
||||
|
||||
export interface NiFiState {
|
||||
router: RouterReducerState;
|
||||
[errorFeatureKey]: ErrorState;
|
||||
[currentUserFeatureKey]: CurrentUserState;
|
||||
[extensionTypesFeatureKey]: ExtensionTypesState;
|
||||
[aboutFeatureKey]: AboutState;
|
||||
|
@ -48,6 +51,7 @@ export interface NiFiState {
|
|||
|
||||
export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||
router: routerReducer,
|
||||
[errorFeatureKey]: errorReducer,
|
||||
[currentUserFeatureKey]: currentUserReducer,
|
||||
[extensionTypesFeatureKey]: extensionTypesReducer,
|
||||
[aboutFeatureKey]: aboutReducer,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<h2 mat-dialog-title>Edit Controller Service</h2>
|
||||
<form class="controller-service-edit-form" [formGroup]="editControllerServiceForm">
|
||||
<error-banner></error-banner>
|
||||
<mat-dialog-content>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Settings">
|
||||
|
|
|
@ -21,6 +21,9 @@ import { EditControllerService } from './edit-controller-service.component';
|
|||
import { EditControllerServiceDialogRequest } from '../../../../state/shared';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Component } from '@angular/core';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../state/error/error.reducer';
|
||||
|
||||
describe('EditControllerService', () => {
|
||||
let component: EditControllerService;
|
||||
|
@ -541,10 +544,22 @@ describe('EditControllerService', () => {
|
|||
}
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'error-banner',
|
||||
standalone: true,
|
||||
template: ''
|
||||
})
|
||||
class MockErrorBanner {}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [EditControllerService, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
imports: [EditControllerService, MockErrorBanner, BrowserAnimationsModule],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(EditControllerService);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -43,6 +43,7 @@ import { ControllerServiceApi } from '../controller-service-api/controller-servi
|
|||
import { Observable } from 'rxjs';
|
||||
import { ControllerServiceReferences } from '../controller-service-references/controller-service-references.component';
|
||||
import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive';
|
||||
import { ErrorBanner } from '../../error-banner/error-banner.component';
|
||||
|
||||
@Component({
|
||||
selector: 'edit-controller-service',
|
||||
|
@ -63,7 +64,8 @@ import { NifiSpinnerDirective } from '../../spinner/nifi-spinner.directive';
|
|||
ControllerServiceApi,
|
||||
ControllerServiceReferences,
|
||||
AsyncPipe,
|
||||
NifiSpinnerDirective
|
||||
NifiSpinnerDirective,
|
||||
ErrorBanner
|
||||
],
|
||||
styleUrls: ['./edit-controller-service.component.scss']
|
||||
})
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<ng-container *ngIf="(messages$ | async)!; let messages">
|
||||
<div class="banner-container border-t border-b px-6 py-3 flex justify-between items-center">
|
||||
<ng-container *ngIf="messages.length === 1; else multipleMessages">
|
||||
<div>{{ messages[0] }}</div>
|
||||
</ng-container>
|
||||
<ng-template #multipleMessages>
|
||||
<ul>
|
||||
<li *ngFor="let message of messages">{{ message }}</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
<div class="flex flex-col mt-auto">
|
||||
<button mat-stroked-button (click)="dismiss()">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.banner-container {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
}
|
||||
}
|
|
@ -17,20 +17,20 @@
|
|||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Banner } from './banner.component';
|
||||
import { ErrorBanner } from './error-banner.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../state/flow/flow.reducer';
|
||||
import { initialState } from '../../../state/error/error.reducer';
|
||||
|
||||
describe('Banner', () => {
|
||||
let component: Banner;
|
||||
let fixture: ComponentFixture<Banner>;
|
||||
describe('ErrorBanner', () => {
|
||||
let component: ErrorBanner;
|
||||
let fixture: ComponentFixture<ErrorBanner>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [Banner],
|
||||
imports: [ErrorBanner],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(Banner);
|
||||
fixture = TestBed.createComponent(ErrorBanner);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
|
@ -17,28 +17,25 @@
|
|||
|
||||
import { Component } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CanvasState } from '../../../state';
|
||||
import { selectApiError } from '../../../state/flow/flow.selectors';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { NgClass, NgIf } from '@angular/common';
|
||||
import { AsyncPipe, NgForOf, NgIf } 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: 'banner',
|
||||
selector: 'error-banner',
|
||||
standalone: true,
|
||||
imports: [NgClass, NgIf],
|
||||
templateUrl: './banner.component.html',
|
||||
styleUrls: ['./banner.component.scss']
|
||||
imports: [NgIf, NgForOf, MatButtonModule, AsyncPipe],
|
||||
templateUrl: './error-banner.component.html',
|
||||
styleUrls: ['./error-banner.component.scss']
|
||||
})
|
||||
export class Banner {
|
||||
message: string | null = '';
|
||||
severity: string = 'alert';
|
||||
export class ErrorBanner {
|
||||
messages$ = this.store.select(selectBannerErrors);
|
||||
|
||||
constructor(private store: Store<CanvasState>) {
|
||||
this.store
|
||||
.select(selectApiError)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((message) => {
|
||||
this.message = message;
|
||||
});
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
dismiss(): void {
|
||||
this.store.dispatch(clearBannerErrors());
|
||||
}
|
||||
}
|
|
@ -15,13 +15,13 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="login-message w-96 flex flex-col gap-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="login-title">{{ title }}</div>
|
||||
<div class="page-content w-1/2 flex flex-col gap-y-5">
|
||||
<div class="flex justify-between items-center gap-x-3">
|
||||
<div class="title whitespace-nowrap overflow-hidden text-ellipsis" [title]="title">{{ title }}</div>
|
||||
<div class="flex gap-x-3">
|
||||
<a (click)="logout()" *ngIf="hasToken()">log out</a>
|
||||
<a (click)="logout()" *ngIf="hasToken()" class="whitespace-nowrap">log out</a>
|
||||
<a [routerLink]="['/']">home</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">{{ message }}</div>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
|
@ -15,8 +15,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.login-message {
|
||||
.login-title {
|
||||
.page-content {
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: Roboto Slab;
|
|
@ -17,21 +17,20 @@
|
|||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginMessage } from './login-message.component';
|
||||
import { PageContent } from './page-content.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
describe('LoginMessage', () => {
|
||||
let component: LoginMessage;
|
||||
let fixture: ComponentFixture<LoginMessage>;
|
||||
describe('PageContent', () => {
|
||||
let component: PageContent;
|
||||
let fixture: ComponentFixture<PageContent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LoginMessage],
|
||||
imports: [HttpClientTestingModule, RouterModule, RouterTestingModule]
|
||||
imports: [PageContent, HttpClientTestingModule, RouterModule, RouterTestingModule]
|
||||
});
|
||||
fixture = TestBed.createComponent(LoginMessage);
|
||||
fixture = TestBed.createComponent(PageContent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
|
@ -16,17 +16,20 @@
|
|||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AuthStorage } from '../../../../service/auth-storage.service';
|
||||
import { AuthService } from '../../../../service/auth.service';
|
||||
import { AuthStorage } from '../../../service/auth-storage.service';
|
||||
import { AuthService } from '../../../service/auth.service';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'login-message',
|
||||
templateUrl: './login-message.component.html',
|
||||
styleUrls: ['./login-message.component.scss']
|
||||
selector: 'page-content',
|
||||
standalone: true,
|
||||
templateUrl: './page-content.component.html',
|
||||
imports: [RouterLink, NgIf],
|
||||
styleUrls: ['./page-content.component.scss']
|
||||
})
|
||||
export class LoginMessage {
|
||||
export class PageContent {
|
||||
@Input() title: string = '';
|
||||
@Input() message: string = '';
|
||||
|
||||
constructor(
|
||||
private authStorage: AuthStorage,
|
Loading…
Reference in New Issue