Nifi 12505 system diagnostics (#8190)

* [NIFI-12505] System Diagnostics

* Refactor Status History dialog to use the load and open action pattern.

* address review feedback

This closes #8190
This commit is contained in:
Rob Fellows 2023-12-28 15:20:50 -05:00 committed by GitHub
parent a73d812c23
commit e15aecce67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1092 additions and 35 deletions

View File

@ -39,6 +39,7 @@ import { AboutEffects } from './state/about/about.effects';
import { StatusHistoryEffects } from './state/status-history/status-history.effects';
import { MatDialogModule } from '@angular/material/dialog';
import { ControllerServiceStateEffects } from './state/contoller-service-state/controller-service-state.effects';
import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diagnostics.effects';
// @ts-ignore
@NgModule({
@ -62,7 +63,8 @@ import { ControllerServiceStateEffects } from './state/contoller-service-state/c
ExtensionTypesEffects,
AboutEffects,
StatusHistoryEffects,
ControllerServiceStateEffects
ControllerServiceStateEffects,
SystemDiagnosticsEffects
),
StoreDevtoolsModule.instrument({
maxAge: 25,

View File

@ -41,6 +41,9 @@
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div *ngIf="(currentUser$ | async)?.systemPermissions?.canRead">
<a (click)="openSystemDiagnostics()">System Diagnostics</a>
</div>
</div>
</div>
</ng-template>

View File

@ -36,8 +36,12 @@ import {
import { selectUser } from '../../../../state/user/user.selectors';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { openStatusHistoryDialog } from '../../../../state/status-history/status-history.actions';
import {
getStatusHistoryAndOpenDialog,
openStatusHistoryDialog
} from '../../../../state/status-history/status-history.actions';
import { ComponentType } from '../../../../state/shared';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@Component({
selector: 'connection-status-listing',
@ -67,7 +71,7 @@ export class ConnectionStatusListing {
.subscribe((connection) => {
if (connection) {
this.store.dispatch(
openStatusHistoryDialog({
getStatusHistoryAndOpenDialog({
request: {
source: 'summary',
componentType: ComponentType.Connection,
@ -104,4 +108,14 @@ export class ConnectionStatusListing {
})
);
}
openSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -41,6 +41,9 @@
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div *ngIf="(currentUser$ | async)?.systemPermissions?.canRead">
<a (click)="openSystemDiagnostics()">System Diagnostics</a>
</div>
</div>
</div>
</ng-template>

View File

@ -27,6 +27,7 @@ import { PortStatusSnapshotEntity, SummaryListingState } from '../../state/summa
import { Store } from '@ngrx/store';
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
import * as SummaryListingActions from '../../state/summary-listing/summary-listing.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@Component({
selector: 'input-port-status-listing',
@ -59,4 +60,14 @@ export class InputPortStatusListing {
})
);
}
openSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -41,6 +41,9 @@
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div *ngIf="(currentUser$ | async)?.systemPermissions?.canRead">
<a (click)="openSystemDiagnostics()">System Diagnostics</a>
</div>
</div>
</div>
</ng-template>

View File

@ -29,6 +29,7 @@ import { Store } from '@ngrx/store';
import { PortStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
import * as SummaryListingActions from '../../state/summary-listing/summary-listing.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@Component({
selector: 'output-port-status-listing',
@ -61,4 +62,14 @@ export class OutputPortStatusListing {
})
);
}
openSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -42,6 +42,9 @@
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div *ngIf="(currentUser$ | async)?.systemPermissions?.canRead">
<a (click)="openSystemDiagnostics()">System Diagnostics</a>
</div>
</div>
</div>
</ng-template>

View File

@ -37,9 +37,13 @@ import {
} from '../../state/summary-listing/summary-listing.selectors';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { openStatusHistoryDialog } from '../../../../state/status-history/status-history.actions';
import {
getStatusHistoryAndOpenDialog,
openStatusHistoryDialog
} from '../../../../state/status-history/status-history.actions';
import { ComponentType } from '../../../../state/shared';
import { selectUser } from '../../../../state/user/user.selectors';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@Component({
selector: 'process-group-status-listing',
@ -70,7 +74,7 @@ export class ProcessGroupStatusListing {
.subscribe((pg) => {
if (pg) {
this.store.dispatch(
openStatusHistoryDialog({
getStatusHistoryAndOpenDialog({
request: {
source: 'summary',
componentType: ComponentType.ProcessGroup,
@ -107,4 +111,14 @@ export class ProcessGroupStatusListing {
})
);
}
openSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -40,6 +40,9 @@
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div *ngIf="(currentUser$ | async)?.systemPermissions?.canRead">
<a (click)="openSystemDiagnostics()">System Diagnostics</a>
</div>
</div>
</div>
</ng-template>

View File

@ -28,11 +28,15 @@ import {
import { ProcessorStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
import { selectUser } from '../../../../state/user/user.selectors';
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
import { openStatusHistoryDialog } from '../../../../state/status-history/status-history.actions';
import {
getStatusHistoryAndOpenDialog,
openStatusHistoryDialog
} from '../../../../state/status-history/status-history.actions';
import { ComponentType } from '../../../../state/shared';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import * as SummaryListingActions from '../../state/summary-listing/summary-listing.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@Component({
selector: 'processor-status-listing',
@ -63,7 +67,7 @@ export class ProcessorStatusListing {
.subscribe((processor) => {
if (processor) {
this.store.dispatch(
openStatusHistoryDialog({
getStatusHistoryAndOpenDialog({
request: {
source: 'summary',
componentType: ComponentType.Processor,
@ -100,4 +104,14 @@ export class ProcessorStatusListing {
})
);
}
openSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -41,6 +41,9 @@
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div *ngIf="(currentUser$ | async)?.systemPermissions?.canRead">
<a (click)="openSystemDiagnostics()">System Diagnostics</a>
</div>
</div>
</div>
</ng-template>

View File

@ -29,10 +29,14 @@ import { Store } from '@ngrx/store';
import { RemoteProcessGroupStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
import { filter, switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { openStatusHistoryDialog } from '../../../../state/status-history/status-history.actions';
import {
getStatusHistoryAndOpenDialog,
openStatusHistoryDialog
} from '../../../../state/status-history/status-history.actions';
import { ComponentType } from '../../../../state/shared';
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
import * as SummaryListingActions from '../../state/summary-listing/summary-listing.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
@Component({
selector: 'remote-process-group-status-listing',
@ -62,7 +66,7 @@ export class RemoteProcessGroupStatusListing {
.subscribe((rpg) => {
if (rpg) {
this.store.dispatch(
openStatusHistoryDialog({
getStatusHistoryAndOpenDialog({
request: {
source: 'summary',
componentType: ComponentType.RemoteProcessGroup,
@ -99,4 +103,14 @@ export class RemoteProcessGroupStatusListing {
})
);
}
openSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -0,0 +1,40 @@
/*
* 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 { HttpClient } from '@angular/common/http';
import { Client } from './client.service';
@Injectable({ providedIn: 'root' })
export class SystemDiagnosticsService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
getSystemDiagnostics(nodewise?: boolean) {
if (nodewise) {
const params = {
nodewise: true
};
return this.httpClient.get(`${SystemDiagnosticsService.API}/system-diagnostics`, { params });
}
return this.httpClient.get(`${SystemDiagnosticsService.API}/system-diagnostics`);
}
}

View File

@ -27,6 +27,8 @@ import { statusHistoryFeatureKey, StatusHistoryState } from './status-history';
import { statusHistoryReducer } from './status-history/status-history.reducer';
import { controllerServiceStateFeatureKey, ControllerServiceState } from './contoller-service-state';
import { controllerServiceStateReducer } from './contoller-service-state/controller-service-state.reducer';
import { systemDiagnosticsFeatureKey, SystemDiagnosticsState } from './system-diagnostics';
import { systemDiagnosticsReducer } from './system-diagnostics/system-diagnostics.reducer';
export interface NiFiState {
router: RouterReducerState;
@ -35,6 +37,7 @@ export interface NiFiState {
[aboutFeatureKey]: AboutState;
[statusHistoryFeatureKey]: StatusHistoryState;
[controllerServiceStateFeatureKey]: ControllerServiceState;
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
}
export const rootReducers: ActionReducerMap<NiFiState> = {
@ -43,5 +46,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[extensionTypesFeatureKey]: extensionTypesReducer,
[aboutFeatureKey]: aboutReducer,
[statusHistoryFeatureKey]: statusHistoryReducer,
[controllerServiceStateFeatureKey]: controllerServiceStateReducer
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer
};

View File

@ -20,14 +20,24 @@ import { StatusHistoryRequest, StatusHistoryResponse } from './index';
const STATUS_HISTORY_PREFIX: string = '[Status History]';
export const loadStatusHistory = createAction(
`${STATUS_HISTORY_PREFIX} Load Status History`,
export const reloadStatusHistory = createAction(
`${STATUS_HISTORY_PREFIX} Reload Status History`,
props<{ request: StatusHistoryRequest }>()
);
export const getStatusHistoryAndOpenDialog = createAction(
`${STATUS_HISTORY_PREFIX} Get Status History and Open Dialog`,
props<{ request: StatusHistoryRequest }>()
);
export const reloadStatusHistorySuccess = createAction(
`${STATUS_HISTORY_PREFIX} Reload Status History Success`,
props<{ response: StatusHistoryResponse }>()
);
export const loadStatusHistorySuccess = createAction(
`${STATUS_HISTORY_PREFIX} Load Status History Success`,
props<{ response: StatusHistoryResponse }>()
props<{ request: StatusHistoryRequest; response: StatusHistoryResponse }>()
);
export const openStatusHistoryDialog = createAction(

View File

@ -36,9 +36,9 @@ export class StatusHistoryEffects {
private dialog: MatDialog
) {}
loadStatusHistory$ = createEffect(() =>
reloadStatusHistory$ = createEffect(() =>
this.actions$.pipe(
ofType(StatusHistoryActions.loadStatusHistory),
ofType(StatusHistoryActions.reloadStatusHistory),
map((action) => action.request),
switchMap((request: StatusHistoryRequest) =>
from(
@ -46,7 +46,7 @@ export class StatusHistoryEffects {
.getProcessorStatusHistory(request.componentType, request.componentId)
.pipe(
map((response: any) =>
StatusHistoryActions.loadStatusHistorySuccess({
StatusHistoryActions.reloadStatusHistorySuccess({
response: {
statusHistory: {
canRead: response.canRead,
@ -68,6 +68,47 @@ export class StatusHistoryEffects {
)
);
getStatusHistoryAndOpenDialog$ = createEffect(() =>
this.actions$.pipe(
ofType(StatusHistoryActions.getStatusHistoryAndOpenDialog),
map((action) => action.request),
switchMap((request) =>
from(
this.statusHistoryService
.getProcessorStatusHistory(request.componentType, request.componentId)
.pipe(
map((response: any) =>
StatusHistoryActions.loadStatusHistorySuccess({
request,
response: {
statusHistory: {
canRead: response.canRead,
statusHistory: response.statusHistory
}
}
})
),
catchError((error) =>
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
)
)
)
)
)
);
loadStatusHistorySuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(StatusHistoryActions.loadStatusHistorySuccess),
map((action) => action.request),
switchMap((request) => of(StatusHistoryActions.openStatusHistoryDialog({ request })))
)
);
openStatusHistoryDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -19,10 +19,12 @@ import { StatusHistoryEntity, StatusHistoryState } from './index';
import { createReducer, on } from '@ngrx/store';
import {
clearStatusHistory,
loadStatusHistory,
reloadStatusHistory,
loadStatusHistorySuccess,
statusHistoryApiError,
viewStatusHistoryComplete
viewStatusHistoryComplete,
reloadStatusHistorySuccess,
getStatusHistoryAndOpenDialog
} from './status-history.actions';
import { produce } from 'immer';
@ -36,12 +38,12 @@ export const initialState: StatusHistoryState = {
export const statusHistoryReducer = createReducer(
initialState,
on(loadStatusHistory, (state) => ({
on(reloadStatusHistory, getStatusHistoryAndOpenDialog, (state) => ({
...state,
status: 'loading' as const
})),
on(loadStatusHistorySuccess, (state, { response }) => ({
on(loadStatusHistorySuccess, reloadStatusHistorySuccess, (state, { response }) => ({
...state,
error: null,
status: 'success' as const,

View File

@ -0,0 +1,107 @@
/*
* 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 systemDiagnosticsFeatureKey = 'systemDiagnostics';
export interface SystemDiagnostics {
aggregateSnapshot: SystemDiagnosticSnapshot;
nodeSnapshots?: NodeSnapshot[];
}
export interface RepositoryStorageUsage {
freeSpace: string;
freeSpaceBytes: number;
totalSpace: string;
totalSpaceBytes: number;
usedSpace: string;
usedSpaceBytes: number;
utilization: string;
identifier?: string;
}
export interface GarbageCollection {
collectionCount: number;
collectionMillis: number;
collectionTime: string;
name: string;
}
export interface VersionInfo {
buildBranch: string;
buildRevision: string;
buildTag: string;
buildTimestamp: string;
javaVendor: string;
javaVersion: string;
niFiVersion: string;
osArchitecture: string;
osName: string;
osVersion: string;
}
export interface NodeSnapshot {
address: string;
apiPort: number;
nodeId: string;
snapshot: SystemDiagnosticSnapshot;
}
export interface SystemDiagnosticSnapshot {
availableProcessors: number;
contentRepositoryStorageUsage: RepositoryStorageUsage[];
daemonThreads: number;
flowFileRepositoryStorageUsage: RepositoryStorageUsage;
freeHeap: string;
freeHeapBytes: number;
freeNonHeap: string;
freeNonHeapBytes: number;
garbageCollection: GarbageCollection[];
heapUtilization: string;
maxHeap: string;
maxHeapBytes: number;
maxNonHeap: string;
maxNonHeapBytes: number;
processorLoadAverage: number;
provenanceRepositoryStorageUsage: RepositoryStorageUsage[];
statsLastRefreshed: string;
totalHeap: string;
totalHeapBytes: number;
totalNonHeap: string;
totalNonHeapBytes: string;
totalThreads: number;
uptime: string;
usedHeap: string;
usedHeapBytes: number;
usedNonHeap: string;
usedNonHeapBytes: number;
versionInfo: VersionInfo;
}
export interface SystemDiagnosticsRequest {
nodewise: boolean;
}
export interface OpenSystemDiagnosticsDialogRequest {}
export interface SystemDiagnosticsResponse {
systemDiagnostics: SystemDiagnostics;
}
export interface SystemDiagnosticsState {
systemDiagnostics: SystemDiagnostics | null;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -0,0 +1,54 @@
/*
* 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 { OpenSystemDiagnosticsDialogRequest, SystemDiagnosticsRequest, SystemDiagnosticsResponse } from './index';
const SYSTEM_DIAGNOSTICS_PREFIX: string = '[System Diagnostics]';
export const reloadSystemDiagnostics = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics`,
props<{ request: SystemDiagnosticsRequest }>()
);
export const loadSystemDiagnosticsSuccess = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics Success`,
props<{ response: SystemDiagnosticsResponse }>()
);
export const reloadSystemDiagnosticsSuccess = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Reload System Diagnostics Success`,
props<{ response: SystemDiagnosticsResponse }>()
);
export const getSystemDiagnosticsAndOpenDialog = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Get System Diagnostics and Open Dialog`,
props<{ request: SystemDiagnosticsRequest }>()
);
export const openSystemDiagnosticsDialog = createAction(`${SYSTEM_DIAGNOSTICS_PREFIX} Open System Diagnostics Dialog`);
export const systemDiagnosticsApiError = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} Load System Diagnostics Error`,
props<{ error: string }>()
);
export const resetSystemDiagnostics = createAction(`${SYSTEM_DIAGNOSTICS_PREFIX} Clear System Diagnostics`);
export const viewSystemDiagnosticsComplete = createAction(
`${SYSTEM_DIAGNOSTICS_PREFIX} View System Diagnostics Complete`
);

View File

@ -0,0 +1,110 @@
/*
* 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 { act, Actions, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { NiFiState } from '../index';
import { MatDialog } from '@angular/material/dialog';
import { SystemDiagnosticsService } from '../../service/system-diagnostics.service';
import * as SystemDiagnosticsActions from './system-diagnostics.actions';
import { catchError, from, map, of, switchMap, tap } from 'rxjs';
import { SystemDiagnosticsRequest } from './index';
import { SystemDiagnosticsDialog } from '../../ui/common/system-diagnostics-dialog/system-diagnostics-dialog.component';
@Injectable()
export class SystemDiagnosticsEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private systemDiagnosticsService: SystemDiagnosticsService,
private dialog: MatDialog
) {}
reloadSystemDiagnostics$ = createEffect(() =>
this.actions$.pipe(
ofType(SystemDiagnosticsActions.reloadSystemDiagnostics),
map((action) => action.request),
switchMap((request: SystemDiagnosticsRequest) =>
from(this.systemDiagnosticsService.getSystemDiagnostics(request.nodewise)).pipe(
map((response: any) =>
SystemDiagnosticsActions.reloadSystemDiagnosticsSuccess({
response: {
systemDiagnostics: response.systemDiagnostics
}
})
),
catchError((error) =>
of(
SystemDiagnosticsActions.systemDiagnosticsApiError({
error: error.error
})
)
)
)
)
)
);
getSystemDiagnosticsAndOpenDialog$ = createEffect(() =>
this.actions$.pipe(
ofType(SystemDiagnosticsActions.getSystemDiagnosticsAndOpenDialog),
map((action) => action.request),
switchMap((request) =>
from(this.systemDiagnosticsService.getSystemDiagnostics(request.nodewise)).pipe(
map((response: any) =>
SystemDiagnosticsActions.loadSystemDiagnosticsSuccess({
response: {
systemDiagnostics: response.systemDiagnostics
}
})
),
catchError((error) =>
of(
SystemDiagnosticsActions.systemDiagnosticsApiError({
error: error.error
})
)
)
)
)
)
);
loadSystemDiagnosticsSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(SystemDiagnosticsActions.loadSystemDiagnosticsSuccess),
switchMap(() => of(SystemDiagnosticsActions.openSystemDiagnosticsDialog()))
)
);
openSystemDiagnosticsDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(SystemDiagnosticsActions.openSystemDiagnosticsDialog),
tap(() => {
this.dialog
.open(SystemDiagnosticsDialog, { panelClass: 'large-dialog' })
.afterClosed()
.subscribe(() => {
this.store.dispatch(SystemDiagnosticsActions.viewSystemDiagnosticsComplete());
});
})
),
{ dispatch: false }
);
}

View File

@ -0,0 +1,66 @@
/*
* 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 { SystemDiagnosticsState } from './index';
import { createReducer, on } from '@ngrx/store';
import {
reloadSystemDiagnostics,
loadSystemDiagnosticsSuccess,
resetSystemDiagnostics,
systemDiagnosticsApiError,
viewSystemDiagnosticsComplete,
getSystemDiagnosticsAndOpenDialog,
reloadSystemDiagnosticsSuccess
} from './system-diagnostics.actions';
export const initialSystemDiagnosticsState: SystemDiagnosticsState = {
systemDiagnostics: null,
status: 'pending',
error: null,
loadedTimestamp: ''
};
export const systemDiagnosticsReducer = createReducer(
initialSystemDiagnosticsState,
on(reloadSystemDiagnostics, getSystemDiagnosticsAndOpenDialog, (state) => ({
...state,
status: 'loading' as const
})),
on(loadSystemDiagnosticsSuccess, reloadSystemDiagnosticsSuccess, (state, { response }) => ({
...state,
error: null,
status: 'success' as const,
loadedTimestamp: response.systemDiagnostics.aggregateSnapshot.statsLastRefreshed,
systemDiagnostics: response.systemDiagnostics
})),
on(systemDiagnosticsApiError, (state, { error }) => ({
...state,
error,
status: 'error' as const
})),
on(resetSystemDiagnostics, (state) => ({
...initialSystemDiagnosticsState
})),
on(viewSystemDiagnosticsComplete, (state) => ({
...initialSystemDiagnosticsState
}))
);

View File

@ -0,0 +1,41 @@
/*
* 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 { systemDiagnosticsFeatureKey, SystemDiagnosticsState } from './index';
export const selectSystemDiagnosticsState = createFeatureSelector<SystemDiagnosticsState>(systemDiagnosticsFeatureKey);
export const selectSystemDiagnostics = createSelector(
selectSystemDiagnosticsState,
(state: SystemDiagnosticsState) => state.systemDiagnostics
);
export const selectSystemDiagnosticsLoadedTimestamp = createSelector(
selectSystemDiagnosticsState,
(state: SystemDiagnosticsState) => state.loadedTimestamp
);
export const selectSystemDiagnosticsError = createSelector(
selectSystemDiagnosticsState,
(state: SystemDiagnosticsState) => state.error
);
export const selectSystemDiagnosticsStatus = createSelector(
selectSystemDiagnosticsState,
(state: SystemDiagnosticsState) => state.status
);

View File

@ -621,13 +621,14 @@ export class StatusHistoryChart {
const marginTop: any = controlContainer.computedStyleMap().get('margin-top');
const statusHistory = document.getElementsByClassName('status-history')![0];
const dialogContent = statusHistory.getElementsByClassName('dialog-content')![0];
const descriptorContainer = document.getElementsByClassName('selected-descriptor-container')![0];
const dialogStyles: any = dialogContent.computedStyleMap();
const bodyHeight = document.body.getBoundingClientRect().height;
return (
bodyHeight -
controlContainer.clientHeight -
50 -
descriptorContainer.clientHeight -
parseInt(marginTop.value, 10) -
parseInt(dialogStyles.get('top')?.value) -
parseInt(dialogStyles.get('bottom')?.value)

View File

@ -35,12 +35,12 @@
*ngIf="componentDetails$ | async; let componentDetails"
class="flex flex-1 w-full gap-x-4">
<div class="component-details flex flex-col gap-y-3">
<div
*ngFor="let entry of Object.entries(componentDetails)"
class="flex flex-col">
<div>{{ entry[0] }}</div>
<div class="value">{{ entry[1] }}</div>
</div>
<ng-container *ngFor="let entry of Object.entries(componentDetails)">
<div *ngIf="entry[0] && entry[1]" class="flex flex-col">
<div>{{ entry[0] }}</div>
<div class="value">{{ entry[1] }}</div>
</div>
</ng-container>
<div class="flex flex-col">
<div>Start</div>
<div class="value">{{ minDate }}</div>
@ -89,7 +89,7 @@
</div>
</div>
<div class="chart-panel grow flex flex-col">
<div *ngIf="fieldDescriptors$ | async">
<div class="selected-descriptor-container" *ngIf="fieldDescriptors$ | async">
<mat-form-field>
<mat-select formControlName="fieldDescriptor">
<ng-container *ngFor="let descriptor of fieldDescriptors">

View File

@ -65,6 +65,10 @@
.mat-mdc-dialog-content {
max-height: unset;
}
.selected-descriptor-container {
height: 68px;
}
}
}

View File

@ -28,7 +28,7 @@ import {
StatusHistoryState
} from '../../../state/status-history';
import { Store } from '@ngrx/store';
import { loadStatusHistory } from '../../../state/status-history/status-history.actions';
import { reloadStatusHistory } from '../../../state/status-history/status-history.actions';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import {
selectStatusHistory,
@ -115,8 +115,6 @@ export class StatusHistory implements OnInit, AfterViewInit {
}
ngOnInit(): void {
this.refresh();
this.statusHistory$.pipe(filter((entity) => !!entity)).subscribe((entity: StatusHistoryEntity) => {
if (entity) {
this.instances = [];
@ -171,9 +169,6 @@ export class StatusHistory implements OnInit, AfterViewInit {
this.maxDate = this.nifiCommon.formatDateTime(new Date(maxDate));
}
});
}
ngAfterViewInit(): void {
this.fieldDescriptors$
.pipe(
filter((descriptors) => !!descriptors),
@ -184,8 +179,11 @@ export class StatusHistory implements OnInit, AfterViewInit {
// select the first field description by default
this.statusHistoryForm.get('fieldDescriptor')?.setValue(descriptors[0]);
this.selectedDescriptor = descriptors[0];
});
}
ngAfterViewInit(): void {
// when the selected descriptor changes, update the chart
this.statusHistoryForm.get('fieldDescriptor')?.valueChanges.subscribe((descriptor: FieldDescriptor) => {
if (this.instances.length > 0) {
@ -199,7 +197,7 @@ export class StatusHistory implements OnInit, AfterViewInit {
}
refresh() {
this.store.dispatch(loadStatusHistory({ request: this.request }));
this.store.dispatch(reloadStatusHistory({ request: this.request }));
}
getSelectOptionTipData(descriptor: FieldDescriptor): TextTipInput {

View File

@ -0,0 +1,264 @@
<!--
~ 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="(systemDiagnostics$ | async)?.aggregateSnapshot; let systemDiagnostics">
<h2 mat-dialog-title>System Diagnostics</h2>
<div class="system-diagnostics">
<mat-dialog-content>
<div class="dialog-content">
<mat-tab-group>
<mat-tab label="JVM">
<div class="tab-content py-4 h-full w-full">
<div class="inset-0 flex gap-y-4">
<div class="flex flex-col flex-1 gap-y-4">
<section>
<div class="section-header">Heap ({{ systemDiagnostics.heapUtilization }})</div>
<div class="flex flex-col gap-y-3">
<div class="flex flex-col">
<div>Max</div>
<div class="value">{{ systemDiagnostics.maxHeap }}</div>
</div>
<div class="flex flex-col">
<div>Total</div>
<div class="value">{{ systemDiagnostics.totalHeap }}</div>
</div>
<div class="flex flex-col">
<div>Used</div>
<div class="value">{{ systemDiagnostics.usedHeap }}</div>
</div>
<div class="flex flex-col">
<div>Free</div>
<div class="value">{{ systemDiagnostics.freeHeap }}</div>
</div>
</div>
</section>
<section>
<div class="section-header">Garbage Collection</div>
<div class="flex flex-col gap-y-3" *ngIf="sortedGarbageCollections">
<div class="flex flex-col" *ngFor="let gc of sortedGarbageCollections">
<div>{{ gc.name }}</div>
<div class="value">
{{ gc.collectionCount }} times ({{ gc.collectionTime }})
</div>
</div>
</div>
</section>
</div>
<div class="flex flex-col flex-1 gap-y-4">
<section>
<div class="section-header">Non Heap</div>
<div class="flex flex-col gap-y-3">
<div class="flex flex-col">
<div>Max</div>
<div class="value">{{ systemDiagnostics.maxNonHeap }}</div>
</div>
<div class="flex flex-col">
<div>Total</div>
<div class="value">{{ systemDiagnostics.totalNonHeap }}</div>
</div>
<div class="flex flex-col">
<div>Used</div>
<div class="value">{{ systemDiagnostics.usedNonHeap }}</div>
</div>
<div class="flex flex-col">
<div>Free</div>
<div class="value">{{ systemDiagnostics.freeNonHeap }}</div>
</div>
</div>
</section>
<section>
<div class="section-header">Runtime</div>
<div class="flex flex-col gap-y-3">
<div class="flex flex-col">
<div>Uptime</div>
<div class="value">{{ systemDiagnostics.uptime }}</div>
</div>
</div>
</section>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="System">
<div class="tab-content py-4 gap-y-6 h-full w-full flex flex-col">
<div class="flex">
<div class="flex flex-col flex-1 gap-y-4">
<div class="flex flex-col gap-y-3">
<div class="flex flex-col">
<div>Available Cores</div>
<div class="value">{{ systemDiagnostics.availableProcessors }}</div>
</div>
</div>
</div>
<div class="flex flex-col flex-1 gap-y-4">
<div class="flex flex-col gap-y-3">
<div class="flex flex-col">
<div class="flex gap-x-3 items-center">
<div>Core Load Average</div>
<div
class="fa fa-question-circle"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getCoreLoadTooltip()"></div>
</div>
<div class="value">
{{ formatFloat(systemDiagnostics.processorLoadAverage) }}
</div>
</div>
</div>
</div>
</div>
<section class="flex flex-col pr-4">
<div class="section-header">FlowFile Repository Usage</div>
<div>
<div class="capitalize">Usage:</div>
<mat-progress-bar
mode="determinate"
[value]="
getRepositoryStorageUsagePercent(
systemDiagnostics.flowFileRepositoryStorageUsage
)
">
</mat-progress-bar>
<div class="value">
{{ systemDiagnostics.flowFileRepositoryStorageUsage.utilization }}
({{ systemDiagnostics.flowFileRepositoryStorageUsage.usedSpace }}
of
{{ systemDiagnostics.flowFileRepositoryStorageUsage.totalSpace }})
</div>
</div>
</section>
<section class="flex flex-col pr-4">
<div class="section-header">Content Repository Usage</div>
<div class="repository-storage-container flex flex-col gap-y-2">
<div *ngFor="let repo of systemDiagnostics.contentRepositoryStorageUsage">
<div class="capitalize">Usage for {{ repo.identifier }}:</div>
<mat-progress-bar
mode="determinate"
[value]="getRepositoryStorageUsagePercent(repo)">
</mat-progress-bar>
<div class="value">
{{ repo.utilization }} ({{ repo.usedSpace }} of {{ repo.totalSpace }})
</div>
</div>
</div>
</section>
<section class="flex flex-col pr-4">
<div class="section-header">Provenance Repository Usage</div>
<div class="repository-storage-container flex flex-col gap-y-2">
<div *ngFor="let repo of systemDiagnostics.provenanceRepositoryStorageUsage">
<div class="capitalize">Usage for {{ repo.identifier }}:</div>
<mat-progress-bar
mode="determinate"
[value]="getRepositoryStorageUsagePercent(repo)">
</mat-progress-bar>
<div class="value">
{{ repo.utilization }} ({{ repo.usedSpace }} of {{ repo.totalSpace }})
</div>
</div>
</div>
</section>
</div>
</mat-tab>
<mat-tab label="Version">
<div class="tab-content py-4 h-full w-full">
<div class="inset-0 flex flex-col gap-y-4">
<section>
<div class="section-header">NiFi</div>
<dl class="setting-attributes-list">
<dt>NiFi Version</dt>
<dd>{{ systemDiagnostics.versionInfo.niFiVersion }}</dd>
<dt>Tag</dt>
<dd>{{ systemDiagnostics.versionInfo.buildTag }}</dd>
<dt>Build Date/Time</dt>
<dd>{{ systemDiagnostics.versionInfo.buildTimestamp }}</dd>
<dt>Branch</dt>
<dd>{{ systemDiagnostics.versionInfo.buildBranch }}</dd>
<dt>Revision</dt>
<dd>{{ systemDiagnostics.versionInfo.buildRevision }}</dd>
</dl>
</section>
<section>
<div class="section-header">Java</div>
<dl class="setting-attributes-list">
<dt>Version</dt>
<dd>{{ systemDiagnostics.versionInfo.javaVersion }}</dd>
<dt>Vendor</dt>
<dd>{{ systemDiagnostics.versionInfo.javaVendor }}</dd>
</dl>
</section>
<section>
<div class="section-header">Operating System</div>
<dl class="setting-attributes-list">
<dt>Name</dt>
<dd>{{ systemDiagnostics.versionInfo.osName }}</dd>
<dt>Version</dt>
<dd>{{ systemDiagnostics.versionInfo.osVersion }}</dd>
<dt>Architecture</dt>
<dd>{{ systemDiagnostics.versionInfo.osArchitecture }}</dd>
</dl>
</section>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<div class="flex flex-1 justify-between">
<div class="refresh-container flex items-center gap-x-2">
<button class="nifi-button" (click)="refreshSystemDiagnostics()">
<i class="fa fa-refresh" [class.fa-spin]="(status$ | async) === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
<div>
<button color="primary" mat-raised-button mat-dialog-close>Close</button>
</div>
</div>
</mat-dialog-actions>
</div>
</ng-container>

View File

@ -0,0 +1,67 @@
/*!
* 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;
.system-diagnostics {
@include mat.button-density(-1);
overflow-y: auto;
.mdc-dialog__content {
padding: 0 16px;
font-size: 14px;
.dialog-content {
min-height: 500px;
overflow-y: auto;
}
}
.tab-content {
position: relative;
height: 480px;
.section-header {
color: #728e9b;
font-size: 15px;
font-family: 'Roboto Slab';
font-style: normal;
font-weight: bold;
}
}
.setting-attributes-list {
dt {
float: left;
clear: left;
padding: 0 0.5em 0.2em 0;
font-weight: bold;
}
dd {
margin-left: 9em;
padding-bottom: 0.2em;
}
}
.mat-mdc-form-field {
width: 100%;
}
mat-dialog-actions {
margin-top: auto;
}
}

View File

@ -0,0 +1,45 @@
/*
* 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 { SystemDiagnosticsDialog } from './system-diagnostics-dialog.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialSystemDiagnosticsState } from '../../../state/system-diagnostics/system-diagnostics.reducer';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
describe('SystemDiagnosticsDialog', () => {
let component: SystemDiagnosticsDialog;
let fixture: ComponentFixture<SystemDiagnosticsDialog>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SystemDiagnosticsDialog],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: {} },
provideMockStore({ initialState: initialSystemDiagnosticsState })
]
});
fixture = TestBed.createComponent(SystemDiagnosticsDialog);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,105 @@
/*
* 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, Inject, OnInit, signal } from '@angular/core';
import { CommonModule, NgForOf } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import {
GarbageCollection,
OpenSystemDiagnosticsDialogRequest,
RepositoryStorageUsage,
SystemDiagnosticsState
} from '../../../state/system-diagnostics';
import { Store } from '@ngrx/store';
import {
selectSystemDiagnostics,
selectSystemDiagnosticsLoadedTimestamp,
selectSystemDiagnosticsStatus
} from '../../../state/system-diagnostics/system-diagnostics.selectors';
import { MatButtonModule } from '@angular/material/button';
import { reloadSystemDiagnostics } from '../../../state/system-diagnostics/system-diagnostics.actions';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { filter } from 'rxjs';
import { TextTip } from '../tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
import { TextTipInput } from '../../../state/shared';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@Component({
selector: 'system-diagnostics-dialog',
standalone: true,
imports: [
CommonModule,
MatTabsModule,
MatDialogModule,
MatButtonModule,
NgForOf,
NifiTooltipDirective,
MatProgressBarModule
],
templateUrl: './system-diagnostics-dialog.component.html',
styleUrls: ['./system-diagnostics-dialog.component.scss']
})
export class SystemDiagnosticsDialog implements OnInit {
systemDiagnostics$ = this.store.select(selectSystemDiagnostics);
loadedTimestamp$ = this.store.select(selectSystemDiagnosticsLoadedTimestamp);
status$ = this.store.select(selectSystemDiagnosticsStatus);
sortedGarbageCollections: GarbageCollection[] | null = null;
constructor(
private store: Store<SystemDiagnosticsState>,
private nifiCommon: NiFiCommon,
@Inject(MAT_DIALOG_DATA) public request: OpenSystemDiagnosticsDialogRequest
) {}
ngOnInit(): void {
this.systemDiagnostics$.pipe(filter((diagnostics) => !!diagnostics)).subscribe((diagnostics) => {
const sorted = diagnostics!.aggregateSnapshot.garbageCollection.slice();
sorted.sort((a, b) => {
return this.nifiCommon.compareString(a.name, b.name);
});
this.sortedGarbageCollections = sorted;
});
}
refreshSystemDiagnostics() {
this.store.dispatch(
reloadSystemDiagnostics({
request: {
nodewise: false
}
})
);
}
formatFloat(value: number): string {
return this.nifiCommon.formatFloat(value);
}
getCoreLoadTooltip(): TextTipInput {
return {
text: 'Core load average for the last minute. Not available on all platforms.'
};
}
getRepositoryStorageUsagePercent(repoStorage: RepositoryStorageUsage): number {
return (repoStorage.usedSpaceBytes / repoStorage.totalSpaceBytes) * 100;
}
protected readonly TextTip = TextTip;
}