[NIFI-12437] - Summary (#8143)

* [NIFI-12437] - Summary
* Processors Status Snapshot Listing
  * initial processors status snapshot table
  * sorting
  * goto processor
  * multi-valued sort for processors status listing summary
  * add filtering to the processors status snapshot tab of the summary
  * created a re-usable summary-table-filter componennt
  * moved status history to common location
  * status history
  * status history chart
  * resize
  * display insufficient data message if there isn't enough data to render the history

* moved status history chart into its own component

* update missing licenses

* review feedback

* removing use of <label> for non-form elements in status-history component, also updated vertical spacing

* review feedback

* remove unused items from processor-status-listing.component.ts

* fixed tests. added  npm script

* fixed routing to processor after initial load of the processors summary table

* turn of debug route tracing

This closes #8143
This commit is contained in:
Rob Fellows 2023-12-15 16:06:44 -05:00 committed by GitHub
parent 231dbde4b3
commit 5e3239f8c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 4580 additions and 13 deletions

View File

@ -7,6 +7,7 @@
"build": "ng build --verbose",
"watch": "ng build --watch --configuration development",
"test": "ng test --karma-config=karma.conf.js --watch=false",
"test:dev": "ng test --karma-config=karma.conf.js --watch=true --browsers=Chrome",
"prettier": "prettier --config .prettierrc . --check",
"prettier-format": "prettier --config .prettierrc . --write",
"ci": "npm ci --ignore-scripts"

View File

@ -47,6 +47,11 @@ const routes: Routes = [
canMatch: [authenticationGuard],
loadChildren: () => import('./pages/counters/feature/counters.module').then((m) => m.CountersModule)
},
{
path: 'summary',
canMatch: [authenticationGuard],
loadChildren: () => import('./pages/summary/feature/summary.module').then((m) => m.SummaryModule)
},
{
path: '',
canMatch: [authenticationGuard],

View File

@ -36,6 +36,8 @@ import { PollingInterceptor } from './service/interceptors/polling.interceptor';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { MatNativeDateModule } from '@angular/material/core';
import { AboutEffects } from './state/about/about.effects';
import { StatusHistoryEffects } from './state/status-history/status-history.effects';
import { MatDialogModule } from '@angular/material/dialog';
// @ts-ignore
@NgModule({
@ -54,14 +56,15 @@ import { AboutEffects } from './state/about/about.effects';
routerState: RouterState.Minimal,
navigationActionTiming: NavigationActionTiming.PostActivation
}),
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects),
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects, StatusHistoryEffects),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
autoPause: true
}),
MatProgressSpinnerModule,
MatNativeDateModule
MatNativeDateModule,
MatDialogModule
],
providers: [
{

View File

@ -25,7 +25,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CounterTableComponent', () => {
describe('CounterTable', () => {
let component: CounterTable;
let fixture: ComponentFixture<CounterTable>;

View File

@ -28,6 +28,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import * as fromUser from '../../../../state/user/user.reducer';
describe('EditableBehaviorService', () => {
let service: EditableBehavior;
@ -51,6 +53,10 @@ describe('EditableBehaviorService', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -26,6 +26,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import * as fromUser from '../../../../state/user/user.reducer';
describe('SelectableBehavior', () => {
let service: SelectableBehavior;
@ -45,6 +47,10 @@ describe('SelectableBehavior', () => {
{
selector: selectFlowState,
value: initialState[flowFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -27,6 +27,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { selectFlowState } from '../state/flow/flow.selectors';
import { controllerServicesFeatureKey } from '../state/controller-services';
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../state/user/user.selectors';
import * as fromUser from '../../../state/user/user.reducer';
describe('CanvasUtils', () => {
let service: CanvasUtils;
@ -46,6 +48,10 @@ describe('CanvasUtils', () => {
{
selector: selectFlowState,
value: initialState[flowFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import * as fromUser from '../../../../state/user/user.reducer';
describe('LabelManager', () => {
let service: LabelManager;
@ -51,6 +53,10 @@ describe('LabelManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import * as fromUser from '../../../../state/user/user.reducer';
describe('PortManager', () => {
let service: PortManager;
@ -51,6 +53,10 @@ describe('PortManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import * as fromUser from '../../../../state/user/user.reducer';
describe('ProcessGroupManager', () => {
let service: ProcessGroupManager;
@ -51,6 +53,10 @@ describe('ProcessGroupManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
import * as fromUser from '../../../../state/user/user.reducer';
describe('ProcessorManager', () => {
let service: ProcessorManager;
@ -51,6 +53,10 @@ describe('ProcessorManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
},
{
selector: selectUser,
value: fromUser.initialState.user
}
]
})

View File

@ -64,7 +64,7 @@
<i class="fa fa-navicon"></i>
</button>
<mat-menu #globalMenu="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item">
<button mat-menu-item class="global-menu-item" [routerLink]="['/summary']">
<i class="fa fa-fw fa-table mr-2"></i>
Summary
</button>

View File

@ -0,0 +1,78 @@
/*
* 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 { RouterModule, Routes } from '@angular/router';
import { Summary } from './summary.component';
import { NgModule } from '@angular/core';
import { ProcessorStatusListing } from '../ui/processor-status-listing/processor-status-listing.component';
import { InputPortStatusListing } from '../ui/input-port-status-listing/input-port-status-listing.component';
import { OutputPortStatusListing } from '../ui/output-port-status-listing/output-port-status-listing.component';
import { RemoteProcessGroupStatusListing } from '../ui/remote-process-group-status-listing/remote-process-group-status-listing.component';
import { ConnectionStatusListing } from '../ui/connection-status-listing/connection-status-listing.component';
import { ProcessGroupStatusListing } from '../ui/process-group-status-listing/process-group-status-listing.component';
const routes: Routes = [
{
path: '',
component: Summary,
children: [
{ path: '', pathMatch: 'full', redirectTo: 'processors' },
{
path: 'processors',
component: ProcessorStatusListing,
children: [
{
path: ':id',
component: ProcessorStatusListing,
children: [
{
path: 'history',
component: ProcessorStatusListing
}
]
}
]
},
{
path: 'input-ports',
component: InputPortStatusListing
},
{
path: 'output-ports',
component: OutputPortStatusListing
},
{
path: 'remote-process-groups',
component: RemoteProcessGroupStatusListing
},
{
path: 'connections',
component: ConnectionStatusListing
},
{
path: 'process-groups',
component: ProcessGroupStatusListing
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SummaryRoutingModule {}

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.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<h3 class="text-xl bold summary-header">NiFi Summary</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1 flex flex-col">
<div class="summary-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
<a
mat-tab-link
*ngFor="let tab of tabLinks"
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
</nav>
</div>
<div class="pt-4 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
/*!
* 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.
*/
.summary-header {
color: #728e9b;
}
.summary-tabs {
border-bottom-width: 1px;
}

View File

@ -0,0 +1,48 @@
/*
* 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 { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { Summary } from './summary.component';
import { initialState } from '../state/summary-listing/summary-listing.reducer';
describe('Summary', () => {
let component: Summary;
let fixture: ComponentFixture<Summary>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Summary],
imports: [MatTabsModule, RouterModule, RouterTestingModule],
providers: [
provideMockStore({
initialState
})
]
});
fixture = TestBed.createComponent(Summary);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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 { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../state';
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
import { loadSummaryListing } from '../state/summary-listing/summary-listing.actions';
interface TabLink {
label: string;
link: string;
}
@Component({
selector: 'summary',
templateUrl: './summary.component.html',
styleUrls: ['./summary.component.scss']
})
export class Summary implements OnInit, OnDestroy {
tabLinks: TabLink[] = [
{ label: 'Processors', link: 'processors' },
{ label: 'Input Ports', link: 'input-ports' },
{ label: 'Output Ports', link: 'output-ports' },
{ label: 'Remote Process Groups', link: 'remote-process-groups' },
{ label: 'Connections', link: 'connections' },
{ label: 'Process Groups', link: 'process-groups' }
];
constructor(private store: Store<NiFiState>) {}
ngOnInit(): void {
this.store.dispatch(startUserPolling());
this.store.dispatch(loadSummaryListing({ recursive: true }));
}
ngOnDestroy(): void {
this.store.dispatch(stopUserPolling());
}
}

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 { NgModule } from '@angular/core';
import { Summary } from './summary.component';
import { CommonModule } from '@angular/common';
import { SummaryRoutingModule } from './summary-routing.module';
import { MatTabsModule } from '@angular/material/tabs';
import { StoreModule } from '@ngrx/store';
import { reducers, summaryFeatureKey } from '../state';
import { EffectsModule } from '@ngrx/effects';
import { ProcessorStatusListingModule } from '../ui/processor-status-listing/processor-status-listing.module';
import { ProcessGroupStatusListingModule } from '../ui/process-group-status-listing/process-group-status-listing.module';
import { ConnectionStatusListingModule } from '../ui/connection-status-listing/connection-status-listing.module';
import { RemoteProcessGroupStatusListingModule } from '../ui/remote-process-group-status-listing/remote-process-group-status-listing.module';
import { OutputPortStatusListingModule } from '../ui/output-port-status-listing/output-port-status-listing.module';
import { InputPortStatusListingModule } from '../ui/input-port-status-listing/input-port-status-listing.module';
import { SummaryListingEffects } from '../state/summary-listing/summary-listing.effects';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@NgModule({
declarations: [Summary],
exports: [Summary],
imports: [
CommonModule,
SummaryRoutingModule,
MatTabsModule,
ProcessorStatusListingModule,
ProcessGroupStatusListingModule,
ConnectionStatusListingModule,
RemoteProcessGroupStatusListingModule,
ConnectionStatusListingModule,
OutputPortStatusListingModule,
InputPortStatusListingModule,
StoreModule.forFeature(summaryFeatureKey, reducers),
EffectsModule.forFeature(SummaryListingEffects),
NgxSkeletonLoaderModule
]
})
export class SummaryModule {}

View File

@ -0,0 +1,35 @@
/*
* 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 '../../../service/client.service';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ClusterSummaryService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
getClusterSummary(): Observable<any> {
return this.httpClient.get(`${ClusterSummaryService.API}/flow/cluster/summary`);
}
}

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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ProcessGroupStatusService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
getProcessGroupsStatus(recursive?: boolean): Observable<any> {
if (recursive) {
const params = {
recursive: true
};
return this.httpClient.get(`${ProcessGroupStatusService.API}/flow/process-groups/root/status`, { params });
}
return this.httpClient.get(`${ProcessGroupStatusService.API}/flow/process-groups/root/status`);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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 { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
import { summaryListingFeatureKey, SummaryListingState } from './summary-listing';
import { summaryListingReducer } from './summary-listing/summary-listing.reducer';
export const summaryFeatureKey = 'summary';
export interface SummaryState {
[summaryListingFeatureKey]: SummaryListingState;
}
export function reducers(state: SummaryState | undefined, action: Action) {
return combineReducers({
[summaryListingFeatureKey]: summaryListingReducer
})(state, action);
}
export const selectSummaryState = createFeatureSelector<SummaryState>(summaryFeatureKey);

View File

@ -0,0 +1,145 @@
/*
* 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 summaryListingFeatureKey = 'summary-listing';
export interface ClusterSummaryEntity {
clustered: boolean;
connectedNodeCount: number;
connectedToCluster: boolean;
totalNodeCount: number;
}
interface BaseSnapshot {
bytesIn: number;
bytesOut: number;
flowFilesIn: number;
flowFilesOut: number;
id: string;
input: string;
name: string;
output: string;
}
export interface BaseSnapshotEntity {
canRead: boolean;
id: string;
}
export interface ConnectionStatusSnapshot extends BaseSnapshot {
bytesQueued: number;
destinationName: string;
flowFileAvailability: string;
flowFilesQueued: number;
groupId: string;
percentUseCount: number;
percentUseBytes: number;
queued: string;
queuedCount: string;
queuedSize: string;
sourceName: string;
}
export interface ConnectionStatusSnapshotEntity extends BaseSnapshotEntity {
connectionStatusSnapshot: ConnectionStatusSnapshot;
}
export interface ProcessorStatusSnapshot extends BaseSnapshot {
activeThreadCount: number;
bytesRead: number;
bytesWritten: number;
executionNode: string;
groupId: string;
read: string;
runStatus: string;
taskCount: number;
tasks: string;
tasksDuration: string;
tasksDurationNanos: number;
terminatedThreadCount: number;
type: string;
written: string;
parentProcessGroupName: string;
processGroupNamePath: string;
}
export interface ProcessorStatusSnapshotEntity extends BaseSnapshotEntity {
processorStatusSnapshot: ProcessorStatusSnapshot;
}
export interface ProcessGroupStatusSnapshotEntity extends BaseSnapshotEntity {
processGroupStatusSnapshot: ProcessGroupStatusSnapshot;
}
export interface ProcessGroupStatusSnapshot extends BaseSnapshot {
connectionStatusSnapshots: ConnectionStatusSnapshotEntity[];
processorStatusSnapshots: ProcessorStatusSnapshotEntity[];
processGroupStatusSnapshots: ProcessGroupStatusSnapshotEntity[];
remoteProcessGroupStatusSnapshots: any[];
inputPortStatusSnapshots: any[];
outputPortStatusSnapshots: any[];
bytesRead: number;
bytesReceived: number;
bytesSent: number;
bytesTransferred: number;
bytesWritten: number;
read: string;
received: string;
sent: string;
transferred: string;
written: string;
flowFilesReceived: number;
flowFilesTransferred: number;
flowFilesSent: number;
activeThreadCount: number;
processingNanos: number;
statelessActiveThreadCount: number;
terminatedThreadCount: number;
}
export interface AggregateSnapshot extends ProcessGroupStatusSnapshot {}
export interface ProcessGroupStatusEntity {
canRead: boolean;
processGroupStatus: {
aggregateSnapshot: AggregateSnapshot;
id: string;
name: string;
statsLastRefreshed: string;
};
}
export interface SummaryListingResponse {
clusterSummary: ClusterSummaryEntity;
status: ProcessGroupStatusEntity;
}
export interface SelectProcessorStatusRequest {
id: string;
}
export interface SummaryListingState {
clusterSummary: ClusterSummaryEntity | null;
processGroupStatus: ProcessGroupStatusEntity | null;
processorStatusSnapshots: ProcessorStatusSnapshotEntity[];
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -0,0 +1,46 @@
/*
* 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 { SelectProcessorStatusRequest, SummaryListingResponse } from './index';
const SUMMARY_LISTING_PREFIX: string = '[Summary Listing]';
export const loadSummaryListing = createAction(
`${SUMMARY_LISTING_PREFIX} Load Summary Listing`,
props<{ recursive: boolean }>()
);
export const loadSummaryListingSuccess = createAction(
`${SUMMARY_LISTING_PREFIX} Load Summary Listing Success`,
props<{ response: SummaryListingResponse }>()
);
export const summaryListingApiError = createAction(
`${SUMMARY_LISTING_PREFIX} Load Summary Listing error`,
props<{ error: string }>()
);
export const selectProcessorStatus = createAction(
`${SUMMARY_LISTING_PREFIX} Select Processor Status`,
props<{ request: SelectProcessorStatusRequest }>()
);
export const navigateToViewProcessorStatusHistory = createAction(
`${SUMMARY_LISTING_PREFIX} Navigate To Processor Status History`,
props<{ id: string }>()
);

View File

@ -0,0 +1,106 @@
/*
* 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 { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { ClusterSummaryService } from '../../service/cluster-summary.service';
import { ProcessGroupStatusService } from '../../service/process-group-status.service';
import * as SummaryListingActions from './summary-listing.actions';
import * as StatusHistoryActions from '../../../../state/status-history/status-history.actions';
import { catchError, combineLatest, filter, map, of, switchMap, tap } from 'rxjs';
import { Router } from '@angular/router';
import { ComponentType } from '../../../../state/shared';
@Injectable()
export class SummaryListingEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private clusterSummaryService: ClusterSummaryService,
private pgStatusService: ProcessGroupStatusService,
private router: Router
) {}
loadSummaryListing$ = createEffect(() =>
this.actions$.pipe(
ofType(SummaryListingActions.loadSummaryListing),
map((action) => action.recursive),
switchMap((recursive) =>
combineLatest([
this.clusterSummaryService.getClusterSummary(),
this.pgStatusService.getProcessGroupsStatus(recursive)
]).pipe(
map(([clusterSummary, status]) =>
SummaryListingActions.loadSummaryListingSuccess({
response: {
clusterSummary,
status
}
})
),
catchError((error) => of(SummaryListingActions.summaryListingApiError({ error: error.error })))
)
)
)
);
selectProcessorStatus$ = createEffect(
() =>
this.actions$.pipe(
ofType(SummaryListingActions.selectProcessorStatus),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/summary', 'processors', request.id]);
})
),
{ dispatch: false }
);
navigateToProcessorStatusHistory$ = createEffect(
() =>
this.actions$.pipe(
ofType(SummaryListingActions.navigateToViewProcessorStatusHistory),
map((action) => action.id),
tap((id) => {
this.router.navigate(['/summary', 'processors', id, 'history']);
})
),
{ dispatch: false }
);
completeProcessorStatusHistory$ = createEffect(
() =>
this.actions$.pipe(
ofType(StatusHistoryActions.viewStatusHistoryComplete),
map((action) => action.request),
filter((request) => request.source === 'summary' && request.componentType === ComponentType.Processor),
tap((request) => {
this.store.dispatch(
SummaryListingActions.selectProcessorStatus({
request: {
id: request.componentId
}
})
);
})
),
{ dispatch: false }
);
}

View File

@ -0,0 +1,92 @@
/*
* 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 {
AggregateSnapshot,
ProcessGroupStatusSnapshot,
ProcessorStatusSnapshotEntity,
SummaryListingState
} from './index';
import { loadSummaryListing, loadSummaryListingSuccess, summaryListingApiError } from './summary-listing.actions';
export const initialState: SummaryListingState = {
clusterSummary: null,
processGroupStatus: null,
processorStatusSnapshots: [],
status: 'pending',
error: null,
loadedTimestamp: ''
};
export const summaryListingReducer = createReducer(
initialState,
on(loadSummaryListing, (state) => ({
...state,
status: 'loading' as const
})),
on(loadSummaryListingSuccess, (state, { response }) => {
const processors: ProcessorStatusSnapshotEntity[] = flattenProcessorStatusSnapshots(
response.status.processGroupStatus.aggregateSnapshot
);
return {
...state,
error: null,
status: 'success' as const,
loadedTimestamp: response.status.processGroupStatus.statsLastRefreshed,
processGroupStatus: response.status,
clusterSummary: response.clusterSummary,
processorStatusSnapshots: processors
};
}),
on(summaryListingApiError, (state, { error }) => ({
...state,
error,
status: 'error' as const
}))
);
function flattenProcessorStatusSnapshots(
snapshot: ProcessGroupStatusSnapshot,
parentPath: string = ''
): ProcessorStatusSnapshotEntity[] {
const path: string = `${parentPath}/${snapshot.name}`;
// supplement the processors with the parent process group name
const processors = snapshot.processorStatusSnapshots.map((p) => {
return {
...p,
processorStatusSnapshot: {
...p.processorStatusSnapshot,
parentProcessGroupName: snapshot.name,
processGroupNamePath: path
}
};
});
if (snapshot.processGroupStatusSnapshots?.length > 0) {
const children = snapshot.processGroupStatusSnapshots
.map((pg) => pg.processGroupStatusSnapshot)
.flatMap((pg) => flattenProcessorStatusSnapshots(pg, path));
return [...processors, ...children];
} else {
return processors;
}
}

View File

@ -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 { createSelector } from '@ngrx/store';
import { selectSummaryState, SummaryState } from '../index';
import { ProcessorStatusSnapshotEntity, summaryListingFeatureKey, SummaryListingState } from './index';
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
export const selectSummaryListing = createSelector(
selectSummaryState,
(state: SummaryState) => state[summaryListingFeatureKey]
);
export const selectSummaryListingLoadedTimestamp = createSelector(
selectSummaryListing,
(state: SummaryListingState) => state.loadedTimestamp
);
export const selectSummaryListingStatus = createSelector(
selectSummaryListing,
(state: SummaryListingState) => state.status
);
export const selectClusterSummary = createSelector(
selectSummaryListing,
(state: SummaryListingState) => state.clusterSummary
);
export const selectProcessGroupStatus = createSelector(
selectSummaryListing,
(state: SummaryListingState) => state.processGroupStatus
);
export const selectProcessorStatusSnapshots = createSelector(
selectSummaryListing,
(state: SummaryListingState) => state.processorStatusSnapshots
);
export const selectProcessorStatus = (id: string) =>
createSelector(selectProcessorStatusSnapshots, (processors: ProcessorStatusSnapshotEntity[]) =>
processors.find((processor) => id === processor.id)
);
export const selectProcessorIdFromRoute = createSelector(selectCurrentRoute, (route) => {
if (route) {
return route.params.id;
}
return null;
});
export const selectViewStatusHistory = createSelector(selectCurrentRoute, (route) => {
if (route?.routeConfig?.path === 'history') {
return route.params.id;
}
});

View File

@ -0,0 +1,57 @@
<!--
~ 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.
-->
<div class="summary-table-filter-container">
<div class="value">Displaying {{ filteredCount }} of {{ totalCount }}</div>
<form [formGroup]="filterForm">
<div class="flex pt-1 gap-1 items-baseline">
<div>
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput type="text" class="small" formControlName="filterTerm" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Filter By</mat-label>
<mat-select formControlName="filterColumn">
<ng-container *ngFor="let option of filterableColumns">
<mat-option [value]="option"> {{ option }} </mat-option>
</ng-container>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="includeStatusFilter">
<mat-form-field>
<mat-label>Status</mat-label>
<mat-select formControlName="filterStatus">
<mat-option value="All"> All Statuses </mat-option>
<mat-option value="Running"> Running </mat-option>
<mat-option value="Stopped"> Stopped </mat-option>
<mat-option value="Validating"> Validating </mat-option>
<mat-option value="Disabled"> Disabled </mat-option>
<mat-option value="Invalid"> Invalid </mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="includePrimaryNodeOnlyFilter">
<mat-checkbox formControlName="primaryOnly"></mat-checkbox>
<mat-label>Primary Node</mat-label>
</div>
</div>
</form>
</div>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,42 @@
/*
* 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 { SummaryTableFilter } from './summary-table-filter.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { SummaryTableFilterModule } from './summary-table-filter.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SummaryTableFilter', () => {
let component: SummaryTableFilter;
let fixture: ComponentFixture<SummaryTableFilter>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SummaryTableFilter],
imports: [SummaryTableFilterModule, NoopAnimationsModule]
});
fixture = TestBed.createComponent(SummaryTableFilter);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,131 @@
/*
* 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 { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounceTime } from 'rxjs';
export interface SummaryTableFilterArgs {
filterTerm: string;
filterColumn: string;
filterStatus?: string;
primaryOnly?: boolean;
}
@Component({
selector: 'summary-table-filter',
templateUrl: './summary-table-filter.component.html',
styleUrls: ['./summary-table-filter.component.scss']
})
export class SummaryTableFilter implements AfterViewInit {
filterForm: FormGroup;
private _filteredCount: number = 0;
private _totalCount: number = 0;
@Input() filterableColumns: string[] = [];
@Input() includeStatusFilter: boolean = false;
@Input() includePrimaryNodeOnlyFilter: boolean = false;
@Output() filterChanged: EventEmitter<SummaryTableFilterArgs> = new EventEmitter<SummaryTableFilterArgs>();
@Input() set filterTerm(term: string) {
this.filterForm.get('filterTerm')?.value(term);
}
@Input() set filterColumn(column: string) {
if (this.filterableColumns?.length > 0) {
if (this.filterableColumns.indexOf(column) >= 0) {
this.filterForm.get('filterColumn')?.value(column);
} else {
this.filterForm.get('filterColumn')?.value(this.filterableColumns[0]);
}
} else {
this.filterForm.get('filterColumn')?.value(this.filterableColumns[0]);
}
}
@Input() set filterStatus(status: string) {
if (this.includeStatusFilter) {
this.filterForm.get('filterStatus')?.value(status);
}
}
@Input() set filteredCount(count: number) {
this._filteredCount = count;
}
get filteredCount(): number {
return this._filteredCount;
}
@Input() set totalCount(total: number) {
this._totalCount = total;
}
get totalCount(): number {
return this._totalCount;
}
constructor(private formBuilder: FormBuilder) {
this.filterForm = this.formBuilder.group({
filterTerm: '',
filterColumn: 'name',
filterStatus: 'All',
primaryOnly: false
});
}
ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm.get('filterStatus')?.valueChanges.subscribe((filterStatus: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterColumn = this.filterForm.get('filterColumn')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm.get('primaryOnly')?.valueChanges.subscribe((primaryOnly: boolean) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterColumn = this.filterForm.get('filterColumn')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
}
applyFilter(filterTerm: string, filterColumn: string, filterStatus: string, primaryOnly: boolean) {
this.filterChanged.next({
filterColumn,
filterStatus,
filterTerm,
primaryOnly
});
}
}

View File

@ -0,0 +1,43 @@
/*
* 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 { SummaryTableFilter } from './summary-table-filter.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { ReactiveFormsModule } from '@angular/forms';
import { NgForOf, NgIf } from '@angular/common';
import { MatCheckboxModule } from '@angular/material/checkbox';
@NgModule({
declarations: [SummaryTableFilter],
imports: [
MatFormFieldModule,
MatInputModule,
MatOptionModule,
MatSelectModule,
ReactiveFormsModule,
NgForOf,
NgIf,
MatCheckboxModule
],
exports: [SummaryTableFilter],
providers: []
})
export class SummaryTableFilterModule {}

View File

@ -0,0 +1,18 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<p>connection-status-listing works!</p>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,38 @@
/*
* 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 { ConnectionStatusListing } from './connection-status-listing.component';
describe('ConnectionStatusListing', () => {
let component: ConnectionStatusListing;
let fixture: ComponentFixture<ConnectionStatusListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ConnectionStatusListing]
});
fixture = TestBed.createComponent(ConnectionStatusListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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 { Component } from '@angular/core';
@Component({
selector: 'connection-status-listing',
templateUrl: './connection-status-listing.component.html',
styleUrls: ['./connection-status-listing.component.scss']
})
export class ConnectionStatusListing {}

View File

@ -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.
*/
import { NgModule } from '@angular/core';
import { ConnectionStatusListing } from './connection-status-listing.component';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [ConnectionStatusListing],
exports: [ConnectionStatusListing],
imports: [CommonModule]
})
export class ConnectionStatusListingModule {}

View File

@ -0,0 +1,18 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<p>input-port-status-listing works!</p>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,38 @@
/*
* 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 { InputPortStatusListing } from './input-port-status-listing.component';
describe('InputPortStatusListing', () => {
let component: InputPortStatusListing;
let fixture: ComponentFixture<InputPortStatusListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InputPortStatusListing]
});
fixture = TestBed.createComponent(InputPortStatusListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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 { Component } from '@angular/core';
@Component({
selector: 'input-port-status-listing',
templateUrl: './input-port-status-listing.component.html',
styleUrls: ['./input-port-status-listing.component.scss']
})
export class InputPortStatusListing {}

View File

@ -0,0 +1,26 @@
/*
* 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 { InputPortStatusListing } from './input-port-status-listing.component';
@NgModule({
declarations: [InputPortStatusListing],
imports: [CommonModule]
})
export class InputPortStatusListingModule {}

View File

@ -0,0 +1,18 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<p>output-port-status-listing works!</p>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,38 @@
/*
* 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 { OutputPortStatusListing } from './output-port-status-listing.component';
describe('OutputPortStatusListing', () => {
let component: OutputPortStatusListing;
let fixture: ComponentFixture<OutputPortStatusListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [OutputPortStatusListing]
});
fixture = TestBed.createComponent(OutputPortStatusListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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 { Component } from '@angular/core';
@Component({
selector: 'output-port-status-listing',
templateUrl: './output-port-status-listing.component.html',
styleUrls: ['./output-port-status-listing.component.scss']
})
export class OutputPortStatusListing {}

View File

@ -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.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OutputPortStatusListing } from './output-port-status-listing.component';
@NgModule({
declarations: [OutputPortStatusListing],
exports: [OutputPortStatusListing],
imports: [CommonModule]
})
export class OutputPortStatusListingModule {}

View File

@ -0,0 +1,18 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<p>process-group-status-listing works!</p>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,38 @@
/*
* 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 { ProcessGroupStatusListing } from './process-group-status-listing.component';
describe('ProcessGroupStatusListing', () => {
let component: ProcessGroupStatusListing;
let fixture: ComponentFixture<ProcessGroupStatusListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ProcessGroupStatusListing]
});
fixture = TestBed.createComponent(ProcessGroupStatusListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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 { Component } from '@angular/core';
@Component({
selector: 'process-group-status-listing',
templateUrl: './process-group-status-listing.component.html',
styleUrls: ['./process-group-status-listing.component.scss']
})
export class ProcessGroupStatusListing {}

View File

@ -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.
*/
import { NgModule } from '@angular/core';
import { ProcessGroupStatusListing } from './process-group-status-listing.component';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [ProcessGroupStatusListing],
exports: [ProcessGroupStatusListing],
imports: [CommonModule]
})
export class ProcessGroupStatusListingModule {}

View File

@ -0,0 +1,46 @@
<!--
~ 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>
<div *ngIf="isInitialLoading((loadedTimestamp$ | async)!); else loaded">
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
</div>
<ng-template #loaded>
<div class="flex flex-col h-full gap-y-2">
<div class="flex-1" *ngIf="currentUser$ | async as user">
<ng-container>
<processor-status-table
[processors]="(processorStatusSnapshots$ | async)!"
[selectedProcessorId]="selectedProcessorId$ | async"
(viewStatusHistory)="viewStatusHistory($event)"
(selectProcessor)="selectProcessor($event)"
initialSortColumn="name"
initialSortDirection="asc"></processor-status-table>
</ng-container>
</div>
<div class="flex justify-between">
<div class="refresh-container flex items-center gap-x-2">
<button class="nifi-button" (click)="refreshSummaryListing()">
<i class="fa fa-refresh" [class.fa-spin]="(summaryListingStatus$ | async) === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
</div>
</div>
</div>
</ng-template>
</ng-container>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,47 @@
/*
* 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 { ProcessorStatusListing } from './processor-status-listing.component';
import { SummaryTableFilter } from '../common/summary-table-filter/summary-table-filter.component';
import { SummaryTableFilterModule } from '../common/summary-table-filter/summary-table-filter.module';
import { ProcessorStatusListingModule } from './processor-status-listing.module';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../counters/state/counter-listing/counter-listing.reducer';
import { ProcessorStatusTable } from './processor-status-table/processor-status-table.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ProcessorStatusListing', () => {
let component: ProcessorStatusListing;
let fixture: ComponentFixture<ProcessorStatusListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ProcessorStatusListing],
imports: [SummaryTableFilterModule, ProcessorStatusTable, NoopAnimationsModule],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(ProcessorStatusListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,103 @@
/*
* 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 {
selectProcessorIdFromRoute,
selectProcessorStatus,
selectProcessorStatusSnapshots,
selectSummaryListingLoadedTimestamp,
selectSummaryListingStatus,
selectViewStatusHistory
} from '../../state/summary-listing/summary-listing.selectors';
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 { 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';
@Component({
selector: 'processor-status-listing',
templateUrl: './processor-status-listing.component.html',
styleUrls: ['./processor-status-listing.component.scss']
})
export class ProcessorStatusListing {
processorStatusSnapshots$ = this.store.select(selectProcessorStatusSnapshots);
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
selectedProcessorId$ = this.store.select(selectProcessorIdFromRoute);
currentUser$ = this.store.select(selectUser);
constructor(private store: Store<SummaryListingState>) {
this.store
.select(selectViewStatusHistory)
.pipe(
filter((id: string) => !!id),
switchMap((id: string) =>
this.store.select(selectProcessorStatus(id)).pipe(
filter((processor) => !!processor),
take(1)
)
),
takeUntilDestroyed()
)
.subscribe((processor) => {
if (processor) {
this.store.dispatch(
openStatusHistoryDialog({
request: {
source: 'summary',
componentType: ComponentType.Processor,
componentId: processor.id
}
})
);
}
});
}
isInitialLoading(loadedTimestamp: string): boolean {
return loadedTimestamp == initialState.loadedTimestamp;
}
refreshSummaryListing() {
this.store.dispatch(SummaryListingActions.loadSummaryListing({ recursive: true }));
}
viewStatusHistory(processor: ProcessorStatusSnapshotEntity): void {
this.store.dispatch(
SummaryListingActions.navigateToViewProcessorStatusHistory({
id: processor.id
})
);
}
selectProcessor(processor: ProcessorStatusSnapshotEntity): void {
this.store.dispatch(
SummaryListingActions.selectProcessorStatus({
request: {
id: processor.id
}
})
);
}
}

View File

@ -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 { NgModule } from '@angular/core';
import { ProcessorStatusListing } from './processor-status-listing.component';
import { CommonModule } from '@angular/common';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ReactiveFormsModule } from '@angular/forms';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterLink } from '@angular/router';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
import { SummaryTableFilterModule } from '../common/summary-table-filter/summary-table-filter.module';
import { ProcessorStatusTable } from './processor-status-table/processor-status-table.component';
@NgModule({
declarations: [ProcessorStatusListing],
exports: [ProcessorStatusListing],
imports: [
CommonModule,
NgxSkeletonLoaderModule,
MatFormFieldModule,
MatInputModule,
MatOptionModule,
MatSelectModule,
MatSortModule,
MatTableModule,
ReactiveFormsModule,
MatDialogModule,
RouterLink,
NifiTooltipDirective,
SummaryTableFilterModule,
ProcessorStatusTable
]
})
export class ProcessorStatusListingModule {}

View File

@ -0,0 +1,231 @@
<!--
~ 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.
-->
<div class="processor-status-table h-full flex flex-col">
<!-- allow filtering of the table -->
<summary-table-filter
[filteredCount]="filteredCount"
[totalCount]="totalCount"
[filterableColumns]="filterableColumns"
[includeStatusFilter]="true"
[includePrimaryNodeOnlyFilter]="true"
(filterChanged)="applyFilter($event)"></summary-table-filter>
<div class="flex-1 relative">
<div class="listing-table overflow-y-auto border absolute inset-0">
<table
mat-table
[dataSource]="dataSource"
matSort
matSortDisableClear
(matSortChange)="sortData($event)"
[matSortActive]="initialSortColumn"
[matSortDirection]="initialSortDirection">
<!-- More Details Column -->
<ng-container matColumnDef="moreDetails">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<ng-container *ngIf="canRead(item)">
<div class="flex items-center gap-x-3">
<!-- TODO - handle read only in configure component? -->
<div
class="pointer fa fa-info-circle"
*ngIf="canRead(item)"
title="View Processor Details"></div>
</div>
</ng-container>
</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
<td mat-cell *matCellDef="let item">
{{ formatName(item) }}
</td>
</ng-container>
<!-- Type column -->
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
<td mat-cell *matCellDef="let item">
{{ formatType(item) }}
</td>
</ng-container>
<!-- Process Group column -->
<ng-container matColumnDef="processGroup">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Process Group</th>
<td mat-cell *matCellDef="let item">
{{ formatProcessGroup(item) }}
</td>
</ng-container>
<!-- Run Status column -->
<ng-container matColumnDef="runStatus">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Run Status</th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-2">
<div [ngClass]="getRunStatusIcon(item)"></div>
<div>{{ formatRunStatus(item) }}</div>
<ng-container *ngIf="item.processorStatusSnapshot as pg">
<div
*ngIf="pg.terminatedThreadCount > 0; else activeThreads"
title="Threads: (Active / Terminated)">
({{ pg.activeThreadCount }}/{{ pg.terminatedThreadCount }})
</div>
<ng-template #activeThreads>
<div *ngIf="pg.activeThreadCount > 0" title="Active Threads">
({{ pg.activeThreadCount }})
</div>
</ng-template>
</ng-container>
</div>
</td>
</ng-container>
<!-- Input column -->
<ng-container matColumnDef="in">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
title="Count / data size in the last 5 minutes">
<div class="flex items-center gap-x-1">
<div [ngClass]="{ underline: multiSort.active === 'in' && multiSort.sortValueIndex === 0 }">
In
</div>
<div [ngClass]="{ underline: multiSort.active === 'in' && multiSort.sortValueIndex === 1 }">
(Size)
</div>
<div class="font-light">5 min</div>
</div>
</th>
<td mat-cell *matCellDef="let item">
{{ formatIn(item) }}
</td>
</ng-container>
<!-- Read Write column -->
<ng-container matColumnDef="readWrite">
<th mat-header-cell *matHeaderCellDef mat-sort-header title="Data size in the last 5 minutes">
<div class="flex items-center gap-x-1">
<div
[ngClass]="{
underline: multiSort.active === 'readWrite' && multiSort.sortValueIndex === 0
}">
Read
</div>
<div>|</div>
<div
[ngClass]="{
underline: multiSort.active === 'readWrite' && multiSort.sortValueIndex === 1
}">
Write
</div>
<div class="font-light">5 min</div>
</div>
</th>
<td mat-cell *matCellDef="let item">
{{ formatReadWrite(item) }}
</td>
</ng-container>
<!-- Output column -->
<ng-container matColumnDef="out">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
title="Count / data size in the last 5 minutes">
<div class="flex items-center gap-x-1">
<div
[ngClass]="{ underline: multiSort.active === 'out' && multiSort.sortValueIndex === 0 }">
Out
</div>
<div
[ngClass]="{ underline: multiSort.active === 'out' && multiSort.sortValueIndex === 1 }">
(Size)
</div>
<div class="font-light">5 min</div>
</div>
</th>
<td mat-cell *matCellDef="let item">
{{ formatOut(item) }}
</td>
</ng-container>
<!-- Tasks column -->
<ng-container matColumnDef="tasks">
<th
mat-header-cell
*matHeaderCellDef
mat-sort-header
title="Count / duration in the last 5 minutes">
<div class="flex items-center gap-x-1">
<div
[ngClass]="{
underline: multiSort.active === 'tasks' && multiSort.sortValueIndex === 0
}">
Tasks
</div>
<div>|</div>
<div
[ngClass]="{
underline: multiSort.active === 'tasks' && multiSort.sortValueIndex === 1
}">
Time
</div>
<div class="font-light">5 min</div>
</div>
</th>
<td mat-cell *matCellDef="let item">
{{ formatTasks(item) }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3">
<div
class="pointer fa fa-long-arrow-right"
[routerLink]="getProcessorLink(item)"
(click)="$event.stopPropagation()"
title="Go to Processor in {{
item?.processorStatusSnapshot?.processGroupNamePath
}}"></div>
<div
class="pointer fa fa-area-chart"
title="View Status History"
(click)="viewStatusHistoryClicked($event, item)"></div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; let even = even; columns: displayedColumns"
[class.even]="even"
(click)="select(row)"
[class.selected]="isSelected(row)"></tr>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
/*!
* 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.
*/
.processor-status-table {
.listing-table {
.mat-column-actions {
width: 72px;
min-width: 72px;
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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 { ProcessorStatusTable } from './processor-status-table.component';
import { SummaryTableFilterModule } from '../../common/summary-table-filter/summary-table-filter.module';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ProcessorStatusTable', () => {
let component: ProcessorStatusTable;
let fixture: ComponentFixture<ProcessorStatusTable>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SummaryTableFilterModule,
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule,
NoopAnimationsModule
]
});
fixture = TestBed.createComponent(ProcessorStatusTable);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,339 @@
/*
* 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, EventEmitter, Input, Output } from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ProcessorStatusSnapshot, ProcessorStatusSnapshotEntity } from '../../../state/summary-listing';
import { MatSortModule, Sort, SortDirection } from '@angular/material/sort';
import { SummaryTableFilterArgs } from '../../common/summary-table-filter/summary-table-filter.component';
import { RouterLink } from '@angular/router';
import { SummaryTableFilterModule } from '../../common/summary-table-filter/summary-table-filter.module';
import { NgClass, NgIf } from '@angular/common';
import { ComponentType } from '../../../../../state/shared';
export type SupportedColumns = 'name' | 'type' | 'processGroup' | 'runStatus' | 'in' | 'out' | 'readWrite' | 'tasks';
export interface MultiSort extends Sort {
sortValueIndex: number;
totalValues: number;
}
@Component({
selector: 'processor-status-table',
templateUrl: './processor-status-table.component.html',
styleUrls: ['./processor-status-table.component.scss', '../../../../../../assets/styles/listing-table.scss'],
standalone: true,
imports: [RouterLink, SummaryTableFilterModule, MatTableModule, MatSortModule, NgClass, NgIf]
})
export class ProcessorStatusTable {
private _initialSortColumn: SupportedColumns = 'name';
private _initialSortDirection: SortDirection = 'asc';
filterableColumns: string[] = ['name', 'type'];
totalCount: number = 0;
filteredCount: number = 0;
multiSort: MultiSort = {
active: this._initialSortColumn,
direction: this._initialSortDirection,
sortValueIndex: 0,
totalValues: 2
};
displayedColumns: string[] = [
'moreDetails',
'name',
'type',
'processGroup',
'runStatus',
'in',
'out',
'readWrite',
'tasks',
'actions'
];
dataSource: MatTableDataSource<ProcessorStatusSnapshotEntity> =
new MatTableDataSource<ProcessorStatusSnapshotEntity>();
constructor() {}
applyFilter(filter: SummaryTableFilterArgs) {
this.dataSource.filter = `${filter.filterTerm}|${filter.filterColumn}|${filter.filterStatus}|${filter.primaryOnly}`;
this.filteredCount = this.dataSource.filteredData.length;
}
@Input() selectedProcessorId!: string;
@Input() set initialSortColumn(initialSortColumn: SupportedColumns) {
this._initialSortColumn = initialSortColumn;
this.multiSort = { ...this.multiSort, active: initialSortColumn };
}
get initialSortColumn() {
return this._initialSortColumn;
}
@Input() set initialSortDirection(initialSortDirection: SortDirection) {
this._initialSortDirection = initialSortDirection;
this.multiSort = { ...this.multiSort, direction: initialSortDirection };
}
get initialSortDirection() {
return this._initialSortDirection;
}
@Input() set processors(processors: ProcessorStatusSnapshotEntity[]) {
if (processors) {
this.dataSource.data = this.sortEntities(processors, this.multiSort);
this.dataSource.filterPredicate = (data: ProcessorStatusSnapshotEntity, filter: string): boolean => {
const filterArray: string[] = filter.split('|');
const filterTerm: string = filterArray[0] || '';
const filterColumn: string = filterArray[1];
const filterStatus: string = filterArray[2];
const primaryOnly: boolean = filterArray[3] === 'true';
const matchOnStatus: boolean = filterStatus !== 'All';
if (primaryOnly) {
if (data.processorStatusSnapshot.executionNode !== 'PRIMARY') {
return false;
}
}
if (matchOnStatus) {
if (data.processorStatusSnapshot.runStatus !== filterStatus) {
return false;
}
}
if (filterTerm === '') {
return true;
}
try {
const filterExpression: RegExp = new RegExp(filterTerm, 'i');
const field: string = data.processorStatusSnapshot[
filterColumn as keyof ProcessorStatusSnapshot
] as string;
return field.search(filterExpression) >= 0;
} catch (e) {
// invalid regex;
return false;
}
};
this.totalCount = processors.length;
this.filteredCount = processors.length;
}
}
@Output() viewStatusHistory: EventEmitter<ProcessorStatusSnapshotEntity> =
new EventEmitter<ProcessorStatusSnapshotEntity>();
@Output() selectProcessor: EventEmitter<ProcessorStatusSnapshotEntity> =
new EventEmitter<ProcessorStatusSnapshotEntity>();
formatName(processor: ProcessorStatusSnapshotEntity): string {
return processor.processorStatusSnapshot.name;
}
formatType(processor: ProcessorStatusSnapshotEntity): string {
return processor.processorStatusSnapshot.type;
}
formatProcessGroup(processor: ProcessorStatusSnapshotEntity): string {
return processor.processorStatusSnapshot.parentProcessGroupName;
}
formatRunStatus(processor: ProcessorStatusSnapshotEntity): string {
return processor.processorStatusSnapshot.runStatus;
}
formatIn(processor: ProcessorStatusSnapshotEntity): string {
return processor.processorStatusSnapshot.input;
}
formatOut(processor: ProcessorStatusSnapshotEntity): string {
return processor.processorStatusSnapshot.output;
}
formatReadWrite(processor: ProcessorStatusSnapshotEntity): string {
return `${processor.processorStatusSnapshot.read} | ${processor.processorStatusSnapshot.written}`;
}
formatTasks(processor: ProcessorStatusSnapshotEntity): string {
return `${processor.processorStatusSnapshot.tasks} | ${processor.processorStatusSnapshot.tasksDuration}`;
}
canRead(processor: ProcessorStatusSnapshotEntity): boolean {
return processor.canRead;
}
getProcessorLink(processor: ProcessorStatusSnapshotEntity): string[] {
return ['/process-groups', processor.processorStatusSnapshot.groupId, ComponentType.Processor, processor.id];
}
getRunStatusIcon(processor: ProcessorStatusSnapshotEntity): string {
switch (processor.processorStatusSnapshot.runStatus.toLowerCase()) {
case 'running':
return 'fa fa-play running';
case 'stopped':
return 'fa fa-stop stopped';
case 'enabled':
return 'fa fa-flash enabled';
case 'disabled':
return 'icon icon-enable-false disabled';
case 'validating':
return 'fa fa-spin fa-circle-notch validating';
case 'invalid':
return 'fa fa-warning invalid';
default:
return '';
}
}
sortData(sort: Sort) {
this.setMultiSort(sort);
this.dataSource.data = this.sortEntities(this.dataSource.data, sort);
}
private sortEntities(data: ProcessorStatusSnapshotEntity[], sort: Sort): ProcessorStatusSnapshotEntity[] {
if (!data) {
return [];
}
return data.slice().sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name':
return this.compare(this.formatName(a), this.formatName(b), isAsc);
case 'type':
return this.compare(this.formatType(a), this.formatType(b), isAsc);
case 'processGroup':
return this.compare(this.formatProcessGroup(a), this.formatProcessGroup(b), isAsc);
case 'runStatus':
return this.compare(this.formatRunStatus(a), this.formatRunStatus(b), isAsc);
case 'in':
if (this.multiSort.sortValueIndex === 0) {
return this.compare(
a.processorStatusSnapshot.flowFilesIn,
b.processorStatusSnapshot.flowFilesIn,
isAsc
);
} else {
return this.compare(
a.processorStatusSnapshot.bytesIn,
b.processorStatusSnapshot.bytesIn,
isAsc
);
}
case 'out':
if (this.multiSort.sortValueIndex === 0) {
return this.compare(
a.processorStatusSnapshot.flowFilesOut,
b.processorStatusSnapshot.flowFilesOut,
isAsc
);
} else {
return this.compare(
a.processorStatusSnapshot.bytesOut,
b.processorStatusSnapshot.bytesOut,
isAsc
);
}
case 'readWrite':
if (this.multiSort.sortValueIndex === 0) {
return this.compare(
a.processorStatusSnapshot.bytesRead,
b.processorStatusSnapshot.bytesRead,
isAsc
);
} else {
return this.compare(
a.processorStatusSnapshot.bytesWritten,
b.processorStatusSnapshot.bytesWritten,
isAsc
);
}
case 'tasks':
if (this.multiSort.sortValueIndex === 0) {
return this.compare(
a.processorStatusSnapshot.taskCount,
b.processorStatusSnapshot.taskCount,
isAsc
);
} else {
return this.compare(
a.processorStatusSnapshot.tasksDurationNanos,
b.processorStatusSnapshot.tasksDurationNanos,
isAsc
);
}
default:
return 0;
}
});
}
private compare(a: number | string, b: number | string, isAsc: boolean) {
return (a < b ? -1 : a > b ? 1 : 0) * (isAsc ? 1 : -1);
}
private supportsMultiValuedSort(sort: Sort): boolean {
switch (sort.active) {
case 'in':
case 'out':
case 'readWrite':
case 'tasks':
return true;
default:
return false;
}
}
private setMultiSort(sort: Sort) {
const { active, direction, sortValueIndex, totalValues } = this.multiSort;
if (this.supportsMultiValuedSort(sort)) {
if (active === sort.active) {
// previous sort was of the same column
if (direction === 'desc' && sort.direction === 'asc') {
// change from previous index to the next
const newIndex = sortValueIndex + 1 >= totalValues ? 0 : sortValueIndex + 1;
this.multiSort = { ...sort, sortValueIndex: newIndex, totalValues };
} else {
this.multiSort = { ...sort, sortValueIndex, totalValues };
}
} else {
// sorting a different column, just reset
this.multiSort = { ...sort, sortValueIndex: 0, totalValues };
}
} else {
this.multiSort = { ...sort, sortValueIndex: 0, totalValues };
}
}
select(processor: ProcessorStatusSnapshotEntity): void {
this.selectProcessor.next(processor);
}
isSelected(processor: ProcessorStatusSnapshotEntity): boolean {
if (this.selectedProcessorId) {
return processor.id === this.selectedProcessorId;
}
return false;
}
viewStatusHistoryClicked(event: MouseEvent, processor: ProcessorStatusSnapshotEntity): void {
event.stopPropagation();
this.viewStatusHistory.next(processor);
}
}

View File

@ -0,0 +1,18 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<p>remote-process-group-status-listing works!</p>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,38 @@
/*
* 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 { RemoteProcessGroupStatusListing } from './remote-process-group-status-listing.component';
describe('RemoteProcessGroupStatusListing', () => {
let component: RemoteProcessGroupStatusListing;
let fixture: ComponentFixture<RemoteProcessGroupStatusListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [RemoteProcessGroupStatusListing]
});
fixture = TestBed.createComponent(RemoteProcessGroupStatusListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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 { Component } from '@angular/core';
@Component({
selector: 'remote-process-group-status-listing',
templateUrl: './remote-process-group-status-listing.component.html',
styleUrls: ['./remote-process-group-status-listing.component.scss']
})
export class RemoteProcessGroupStatusListing {}

View File

@ -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.
*/
import { NgModule } from '@angular/core';
import { RemoteProcessGroupStatusListing } from './remote-process-group-status-listing.component';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [RemoteProcessGroupStatusListing],
exports: [RemoteProcessGroupStatusListing],
imports: [CommonModule]
})
export class RemoteProcessGroupStatusListingModule {}

View File

@ -21,10 +21,21 @@ import { Injectable } from '@angular/core';
providedIn: 'root'
})
export class NiFiCommon {
private static readonly MILLIS_PER_DAY: number = 86400000;
private static readonly MILLIS_PER_HOUR: number = 3600000;
private static readonly MILLIS_PER_MINUTE: number = 60000;
private static readonly MILLIS_PER_SECOND: number = 1000;
/**
* Constants for time duration formatting.
*/
public static readonly MILLIS_PER_DAY: number = 86400000;
public static readonly MILLIS_PER_HOUR: number = 3600000;
public static readonly MILLIS_PER_MINUTE: number = 60000;
public static readonly MILLIS_PER_SECOND: number = 1000;
/**
* Constants for formatting data size.
*/
public static readonly BYTES_IN_KILOBYTE: number = 1024;
public static readonly BYTES_IN_MEGABYTE: number = 1048576;
public static readonly BYTES_IN_GIGABYTE: number = 1073741824;
public static readonly BYTES_IN_TERABYTE: number = 1099511627776;
constructor() {}
@ -340,4 +351,62 @@ export class NiFiCommon {
return time;
}
}
/**
* Formats the specified number of bytes into a human readable string.
*
* @param {number} dataSize
* @returns {string}
*/
public formatDataSize(dataSize: number): string {
let dataSizeToFormat: number = parseFloat(`${dataSize / NiFiCommon.BYTES_IN_TERABYTE}`);
if (dataSizeToFormat > 1) {
return dataSizeToFormat.toFixed(2) + ' TB';
}
// check gigabytes
dataSizeToFormat = parseFloat(`${dataSize / NiFiCommon.BYTES_IN_GIGABYTE}`);
if (dataSizeToFormat > 1) {
return dataSizeToFormat.toFixed(2) + ' GB';
}
// check megabytes
dataSizeToFormat = parseFloat(`${dataSize / NiFiCommon.BYTES_IN_MEGABYTE}`);
if (dataSizeToFormat > 1) {
return dataSizeToFormat.toFixed(2) + ' MB';
}
// check kilobytes
dataSizeToFormat = parseFloat(`${dataSize / NiFiCommon.BYTES_IN_KILOBYTE}`);
if (dataSizeToFormat > 1) {
return dataSizeToFormat.toFixed(2) + ' KB';
}
// default to bytes
return parseFloat(`${dataSize}`).toFixed(2) + ' bytes';
}
/**
* Formats the specified integer as a string (adding commas). At this
* point this does not take into account any locales.
*
* @param {integer} integer
*/
public formatInteger(integer: number): string {
const locale: string = (navigator && navigator.language) || 'en';
return integer.toLocaleString(locale, { maximumFractionDigits: 0 });
}
/**
* Formats the specified float using two decimal places.
*
* @param {float} f
*/
public formatFloat(f: number): string {
if (!f) {
return '0.0';
}
const locale: string = (navigator && navigator.language) || 'en';
return f.toLocaleString(locale, { maximumFractionDigits: 2, minimumFractionDigits: 2 });
}
}

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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Client } from './client.service';
import { ComponentType } from '../state/shared';
@Injectable({ providedIn: 'root' })
export class StatusHistoryService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
getProcessorStatusHistory(componentType: ComponentType, componentId: string) {
let componentPath: string;
switch (componentType) {
case ComponentType.Processor:
componentPath = 'processors';
break;
case ComponentType.ProcessGroup:
componentPath = 'process-groups';
break;
case ComponentType.Connection:
componentPath = 'connections';
break;
case ComponentType.RemoteProcessGroup:
componentPath = 'remote-process-groups';
break;
default:
componentPath = 'processors';
}
return this.httpClient.get(
`${StatusHistoryService.API}/flow/${componentPath}/${encodeURIComponent(componentId)}/status/history`
);
}
}

View File

@ -23,17 +23,21 @@ import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types
import { extensionTypesReducer } from './extension-types/extension-types.reducer';
import { aboutFeatureKey, AboutState } from './about';
import { aboutReducer } from './about/about.reducer';
import { statusHistoryFeatureKey, StatusHistoryState } from './status-history';
import { statusHistoryReducer } from './status-history/status-history.reducer';
export interface NiFiState {
router: RouterReducerState;
[userFeatureKey]: UserState;
[extensionTypesFeatureKey]: ExtensionTypesState;
[aboutFeatureKey]: AboutState;
[statusHistoryFeatureKey]: StatusHistoryState;
}
export const rootReducers: ActionReducerMap<NiFiState> = {
router: routerReducer,
[userFeatureKey]: userReducer,
[extensionTypesFeatureKey]: extensionTypesReducer,
[aboutFeatureKey]: aboutReducer
[aboutFeatureKey]: aboutReducer,
[statusHistoryFeatureKey]: statusHistoryReducer
};

View File

@ -0,0 +1,76 @@
/*
* 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 { ComponentType } from '../shared';
export const statusHistoryFeatureKey = 'status-history';
export interface StatusHistoryRequest {
source: string;
componentId: string;
componentType: ComponentType;
}
export interface FieldDescriptor {
description: string;
field: string;
formatter: string;
label: string;
}
export interface ComponentDetails {
'Group Id': string;
Id: string;
Name: string;
Type: ComponentType;
}
export interface StatusHistoryAggregateSnapshot {
timestamp: number;
statusMetrics: any[];
}
export interface NodeSnapshot {
nodeId: string;
address: string;
apiPort: string;
statusSnapshots: any[];
}
export interface StatusHistory {
aggregateSnapshots: StatusHistoryAggregateSnapshot[];
componentDetails: ComponentDetails;
fieldDescriptors: FieldDescriptor[];
generated: string;
nodeSnapshots?: NodeSnapshot[];
}
export interface StatusHistoryEntity {
canRead: boolean;
statusHistory: StatusHistory;
}
export interface StatusHistoryResponse {
statusHistory: StatusHistoryEntity;
}
export interface StatusHistoryState {
statusHistory: StatusHistoryEntity;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -0,0 +1,48 @@
/*
* 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 { StatusHistoryRequest, StatusHistoryResponse } from './index';
const STATUS_HISTORY_PREFIX: string = '[Status History]';
export const loadStatusHistory = createAction(
`${STATUS_HISTORY_PREFIX} Load Status History`,
props<{ request: StatusHistoryRequest }>()
);
export const loadStatusHistorySuccess = createAction(
`${STATUS_HISTORY_PREFIX} Load Status History Success`,
props<{ response: StatusHistoryResponse }>()
);
export const openStatusHistoryDialog = createAction(
`${STATUS_HISTORY_PREFIX} Open Status History Dialog`,
props<{ request: StatusHistoryRequest }>()
);
export const statusHistoryApiError = createAction(
`${STATUS_HISTORY_PREFIX} Load Status History error`,
props<{ error: string }>()
);
export const clearStatusHistory = createAction(`${STATUS_HISTORY_PREFIX} Clear Status History`);
export const viewStatusHistoryComplete = createAction(
`${STATUS_HISTORY_PREFIX} View Status History Complete`,
props<{ request: StatusHistoryRequest }>()
);

View File

@ -0,0 +1,99 @@
/*
* 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 { Store } from '@ngrx/store';
import { NiFiState } from '../index';
import { StatusHistoryService } from '../../service/status-history.service';
import * as StatusHistoryActions from './status-history.actions';
import { StatusHistoryRequest } from './index';
import { catchError, from, map, of, switchMap, tap } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { StatusHistory } from '../../ui/common/status-history/status-history.component';
import { ComponentType } from '../shared';
@Injectable()
export class StatusHistoryEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private statusHistoryService: StatusHistoryService,
private dialog: MatDialog
) {}
loadStatusHistory$ = createEffect(() =>
this.actions$.pipe(
ofType(StatusHistoryActions.loadStatusHistory),
map((action) => action.request),
switchMap((request: StatusHistoryRequest) =>
from(
this.statusHistoryService
.getProcessorStatusHistory(request.componentType, request.componentId)
.pipe(
map((response: any) =>
StatusHistoryActions.loadStatusHistorySuccess({
response: {
statusHistory: {
canRead: response.canRead,
statusHistory: response.statusHistory
}
}
})
),
catchError((error) =>
of(
StatusHistoryActions.statusHistoryApiError({
error: error.error
})
)
)
)
)
)
)
);
openStatusHistoryDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(StatusHistoryActions.openStatusHistoryDialog),
map((action) => action.request),
tap((request) => {
const dialogReference = this.dialog.open(StatusHistory, {
data: request,
panelClass: 'large-dialog'
});
dialogReference.afterClosed().subscribe((response) => {
if (response !== 'ROUTED') {
this.store.dispatch(
StatusHistoryActions.viewStatusHistoryComplete({
request: {
source: request.source,
componentType: request.componentType,
componentId: request.componentId
}
})
);
}
});
})
),
{ dispatch: false }
);
}

View File

@ -0,0 +1,65 @@
/*
* 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 { StatusHistoryEntity, StatusHistoryState } from './index';
import { createReducer, on } from '@ngrx/store';
import {
clearStatusHistory,
loadStatusHistory,
loadStatusHistorySuccess,
statusHistoryApiError,
viewStatusHistoryComplete
} from './status-history.actions';
import { produce } from 'immer';
export const initialState: StatusHistoryState = {
statusHistory: {} as StatusHistoryEntity,
status: 'pending',
error: null,
loadedTimestamp: ''
};
export const statusHistoryReducer = createReducer(
initialState,
on(loadStatusHistory, (state) => ({
...state,
status: 'loading' as const
})),
on(loadStatusHistorySuccess, (state, { response }) => ({
...state,
error: null,
status: 'success' as const,
loadedTimestamp: response.statusHistory.statusHistory.generated,
statusHistory: response.statusHistory
})),
on(statusHistoryApiError, (state, { error }) => ({
...state,
error,
status: 'error' as const
})),
on(clearStatusHistory, (state) => ({
...initialState
})),
on(viewStatusHistoryComplete, (state) => ({
...initialState
}))
);

View File

@ -0,0 +1,36 @@
/*
* 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 { StatusHistoryEntity, statusHistoryFeatureKey, StatusHistoryState } from './index';
export const selectStatusHistoryState = createFeatureSelector<StatusHistoryState>(statusHistoryFeatureKey);
export const selectStatusHistory = createSelector(
selectStatusHistoryState,
(state: StatusHistoryState) => state.statusHistory
);
export const selectStatusHistoryComponentDetails = createSelector(
selectStatusHistory,
(state: StatusHistoryEntity) => state.statusHistory?.componentDetails
);
export const selectStatusHistoryFieldDescriptors = createSelector(
selectStatusHistory,
(state: StatusHistoryEntity) => state.statusHistory?.fieldDescriptors
);

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, ElementRef, EventEmitter, Output } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { CdkDrag, CdkDragEnd, CdkDragMove } from '@angular/cdk/drag-drop';
@ -31,6 +31,8 @@ import { AsyncPipe, NgIf } from '@angular/common';
})
export class Resizable {
@Output() resized = new EventEmitter<DOMRect>();
@Input() minHeight: number = 0;
@Input() minWidth: number = 0;
private startSize$ = new Subject<DOMRect>();
private dragMove$ = new Subject<CdkDragMove>();
@ -38,9 +40,18 @@ export class Resizable {
withLatestFrom(this.startSize$),
auditTime(25),
tap(([{ distance }, rect]) => {
this.el.nativeElement.style.width = `${rect.width + distance.x}px`;
this.el.nativeElement.style.height = `${rect.height + distance.y}px`;
this.resized.emit(this.el.nativeElement.getBoundingClientRect());
let resized: boolean = false;
if (rect.width + distance.x >= this.minWidth) {
this.el.nativeElement.style.width = `${rect.width + distance.x}px`;
resized = true;
}
if (rect.height + distance.y >= this.minHeight) {
this.el.nativeElement.style.height = `${rect.height + distance.y}px`;
resized = true;
}
if (resized) {
this.resized.emit(this.el.nativeElement.getBoundingClientRect());
}
})
);

View File

@ -0,0 +1,37 @@
/*
* 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 interface Instance {
id: string;
label: string;
snapshots: any[];
}
export interface VisibleInstances {
[key: string]: boolean;
}
export const NIFI_NODE_CONFIG = {
nifiInstanceId: 'nifi-instance-id',
nifiInstanceLabel: 'NiFi'
};
export interface Stats {
min: string;
max: string;
mean: string;
}

View File

@ -0,0 +1,21 @@
<!--
~ 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.
-->
<div id="status-history-container" class="flex-1 flex flex-col">
<div id="status-history-chart-container" class="grow"></div>
<div id="status-history-chart-control-container"></div>
</div>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,38 @@
/*
* 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 { StatusHistoryChart } from './status-history-chart.component';
describe('StatusHistoryChart', () => {
let component: StatusHistoryChart;
let fixture: ComponentFixture<StatusHistoryChart>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [StatusHistoryChart]
});
fixture = TestBed.createComponent(StatusHistoryChart);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,675 @@
/*
* 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, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FieldDescriptor } from '../../../../state/status-history';
import * as d3 from 'd3';
import { NiFiCommon } from '../../../../service/nifi-common.service';
import { Instance, NIFI_NODE_CONFIG, Stats, VisibleInstances } from '../index';
import { debounceTime, Subject } from 'rxjs';
@Component({
selector: 'status-history-chart',
standalone: true,
imports: [CommonModule],
templateUrl: './status-history-chart.component.html',
styleUrls: ['./status-history-chart.component.scss']
})
export class StatusHistoryChart {
private _instances!: Instance[];
private _selectedDescriptor: FieldDescriptor | null = null;
private _visibleInstances: VisibleInstances = {};
@Input() set instances(nodeInstances: Instance[]) {
this._instances = nodeInstances;
if (this._selectedDescriptor) {
this.updateChart(this._selectedDescriptor);
}
}
get instances(): Instance[] {
return this._instances;
}
@Input() set selectedFieldDescriptor(selected: FieldDescriptor | null) {
if (this._selectedDescriptor !== selected) {
// clear the brush selection when the data is changed to view a different descriptor
this.brushSelection = null;
this._selectedDescriptor = selected;
if (selected) {
this.updateChart(selected);
}
}
}
get selectedFieldDescriptor(): FieldDescriptor | null {
return this._selectedDescriptor;
}
@Input() set visibleInstances(visibleInstances: VisibleInstances) {
this._visibleInstances = visibleInstances;
if (this._selectedDescriptor) {
this.updateChart(this._selectedDescriptor);
}
}
get visibleInstances(): VisibleInstances {
return this._visibleInstances;
}
@Output() nodeStats: EventEmitter<Stats> = new EventEmitter<Stats>();
@Output() clusterStats: EventEmitter<Stats> = new EventEmitter<Stats>();
private nodeStats$: Subject<Stats> = new Subject<Stats>();
private clusterStats$: Subject<Stats> = new Subject<Stats>();
nodes: any[] = [];
constructor(private nifiCommon: NiFiCommon) {
// don't need constantly fire the stats changing as a result of brush drag/move
this.nodeStats$.pipe(debounceTime(20)).subscribe((stats: Stats) => {
this.nodeStats.next(stats);
});
this.clusterStats$.pipe(debounceTime(20)).subscribe((stats: Stats) => {
this.clusterStats.next(stats);
});
}
private formatters: any = {
DURATION: (d: number) => {
return this.nifiCommon.formatDuration(d);
},
COUNT: (d: number) => {
// need to handle floating point number since this formatter
// will also be used for average values
if (d % 1 === 0) {
return this.nifiCommon.formatInteger(d);
} else {
return this.nifiCommon.formatFloat(d);
}
},
DATA_SIZE: (d: number) => {
return this.nifiCommon.formatDataSize(d);
},
FRACTION: (d: number) => {
return this.nifiCommon.formatFloat(d / 1000000);
}
};
private brushSelection: any = null;
// private selectedDescriptor: FieldDescriptor | null = null;
private updateChart(selectedDescriptor: FieldDescriptor) {
const margin = {
top: 15,
right: 20,
bottom: 25,
left: 80
};
// -------------
// prep the data
// -------------
// available colors
const color = d3.scaleOrdinal(d3.schemeCategory10);
// determine the available instances
const instanceLabels = this.instances.map((instance) => instance.label);
// specify the domain based on the detected instances
color.domain(instanceLabels);
// data for the chart
const statusData = this.instances.map((instance) => {
// convert the model
return {
id: instance.id,
label: instance.label,
values: instance.snapshots.map((snapshot) => {
return {
timestamp: snapshot.timestamp,
value: snapshot.statusMetrics[selectedDescriptor.field]
};
}),
visible: this._visibleInstances[instance.id]
};
});
const customTimeFormat = (d: any) => {
if (d.getMilliseconds()) {
return d3.timeFormat(':%S.%L')(d);
} else if (d.getSeconds()) {
return d3.timeFormat(':%S')(d);
} else if (d.getMinutes() || d.getHours()) {
return d3.timeFormat('%H:%M')(d);
} else if (d.getDay() && d.getDate() !== 1) {
return d3.timeFormat('%a %d')(d);
} else if (d.getDate() !== 1) {
return d3.timeFormat('%b %d')(d);
} else if (d.getMonth()) {
return d3.timeFormat('%B')(d);
} else {
return d3.timeFormat('%Y')(d);
}
};
// ----------
// main chart
// ----------
// the container for the main chart
const chartContainer = document.getElementById('status-history-chart-container')!;
// clear out the dom for the chart
chartContainer.replaceChildren();
// calculate the dimensions
chartContainer.setAttribute('height', this.getChartMinHeight() + 'px');
// determine the new width/height
let width = chartContainer.clientWidth - margin.left - margin.right;
let height = chartContainer.clientHeight - margin.top - margin.bottom;
let maxWidth = Math.min(chartContainer.clientWidth, this.getChartMaxWidth());
if (width > maxWidth) {
width = maxWidth;
}
let maxHeight = this.getChartMaxHeight();
if (height > maxHeight) {
height = maxHeight;
}
// define the x axis for the main chart
const x = d3.scaleTime().range([0, width]);
const xAxis: any = d3.axisBottom(x).ticks(5).tickFormat(customTimeFormat);
// define the y axis
const y = d3.scaleLinear().range([height, 0]);
const yAxis: any = d3.axisLeft(y).tickFormat(this.formatters[selectedDescriptor.formatter]);
// status line
const line = d3
.line()
.curve(d3.curveMonotoneX)
.x((d: any) => x(d.timestamp))
.y((d: any) => y(d.value));
// build the chart svg
const chartSvg = d3
.select('#status-history-chart-container')
.append('svg')
.attr('style', 'pointer-events: none;')
.attr('width', chartContainer.clientWidth)
.attr('height', chartContainer.clientHeight);
// define a clip the path
const clipPath = chartSvg
.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width)
.attr('height', height);
// build the chart
const chart = chartSvg.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
// determine the min/max date
const minDate = d3.min(statusData, (d) => {
return d3.min(d.values, (s) => {
return s.timestamp;
});
});
const maxDate = d3.max(statusData, (d) => {
return d3.max(d.values, (s) => {
return s.timestamp;
});
});
// determine the x axis range
x.domain([minDate, maxDate]);
// determine the y axis range
y.domain([this.getMinValue(statusData), this.getMaxValue(statusData)]);
// build the x axis
chart
.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
// build the y axis
chart
.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.attr('text-anchor', 'end')
.text(selectedDescriptor.label);
// build the chart
const status = chart
.selectAll('.status')
.data(statusData)
.enter()
.append('g')
.attr('clip-path', 'url(#clip)')
.attr('class', 'status');
status
.append('path')
.attr('class', (d: any) => {
return 'chart-line chart-line-' + d.id;
})
.attr('d', (d: any) => {
return line(d.values);
})
.attr('stroke', (d: any) => {
return color(d.label);
})
.classed('hidden', (d: any) => {
return d.visible === false;
})
.append('title')
.text((d: any) => {
return d.label;
});
const me = this;
// draw the control points for each line
status.each(function (d) {
// create a group for the control points
const markGroup = d3
.select(this)
.append('g')
.attr('class', function () {
return 'mark-group mark-group-' + d.id;
})
.classed('hidden', function (d: any) {
return d.visible === false;
});
// draw the control points
markGroup
.selectAll('circle.mark')
.data(d.values)
.enter()
.append('circle')
.attr('style', 'pointer-events: all;')
.attr('class', 'mark')
.attr('cx', (v) => {
return x(v.timestamp);
})
.attr('cy', (v) => {
return y(v.value);
})
.attr('fill', () => {
return color(d.label);
})
.attr('r', 1.5)
.append('title')
.text((v) => {
return d.label + ' -- ' + me.formatters[selectedDescriptor.formatter](v.value);
});
});
// update the size of the chart
chartSvg.attr('width', chartContainer.parentElement?.clientWidth!).attr('height', chartContainer.clientHeight);
// update the size of the clipper
clipPath.attr('width', width).attr('height', height);
// update the position of the x axis
chart.select('.x.axis').attr('transform', 'translate(0, ' + height + ')');
// -------------
// control chart
// -------------
// the container for the main chart control
const chartControlContainer = document.getElementById('status-history-chart-control-container')!;
chartControlContainer.replaceChildren();
const controlHeight = chartControlContainer.clientHeight - margin.top - margin.bottom;
const xControl = d3.scaleTime().range([0, width]);
const xControlAxis = d3.axisBottom(xControl).ticks(5).tickFormat(customTimeFormat);
const yControl = d3.scaleLinear().range([controlHeight, 0]);
const yControlAxis = d3
.axisLeft(yControl)
.tickValues(y.domain())
.tickFormat(this.formatters[selectedDescriptor.formatter]);
// status line
const controlLine = d3
.line()
.curve(d3.curveMonotoneX)
.x(function (d: any) {
return xControl(d.timestamp);
})
.y(function (d: any) {
return yControl(d.value);
});
// build the svg
const controlChartSvg = d3
.select('#status-history-chart-control-container')
.append('svg')
.attr('width', chartContainer.clientWidth)
.attr('height', chartControlContainer.clientHeight);
// build the control chart
const control = controlChartSvg
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
// define the domain for the control chart
xControl.domain(x.domain());
yControl.domain(y.domain());
// build the control x axis
control
.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + controlHeight + ')')
.call(xControlAxis);
// build the control y axis
control.append('g').attr('class', 'y axis').call(yControlAxis);
// build the control chart
const controlStatus = control.selectAll('.status').data(statusData).enter().append('g').attr('class', 'status');
// draw the lines
controlStatus
.append('path')
.attr('class', (d: any) => {
return 'chart-line chart-line-' + d.id;
})
.attr('d', (d: any) => {
return controlLine(d.values);
})
.attr('stroke', (d: any) => {
return color(d.label);
})
.classed('hidden', (d: any) => {
return this.visibleInstances[d.id] === false;
})
.append('title')
.text(function (d) {
return d.label;
});
const updateAggregateStatistics = () => {
const xDomain = x.domain();
const yDomain = y.domain();
// locate the instances that have data points within the current brush
const withinBrush = statusData.map((d: any) => {
// update to only include values within the brush
const values = d.values.filter((s: any) => {
return (
s.timestamp >= xDomain[0].getTime() &&
s.timestamp <= xDomain[1] &&
s.value >= yDomain[0] &&
s.value <= yDomain[1]
);
});
return {
...d,
values
};
});
// consider visible nodes with data in the brush
const nodes: any[] = withinBrush.filter((d: any) => {
return d.id !== NIFI_NODE_CONFIG.nifiInstanceId && d.visible && d.values.length > 0;
});
const nodeMinValue =
nodes.length === 0 ? 'NA' : this.formatters[selectedDescriptor.formatter](this.getMinValue(nodes));
const nodeMeanValue =
nodes.length === 0 ? 'NA' : this.formatters[selectedDescriptor.formatter](this.getMeanValue(nodes));
const nodeMaxValue =
nodes.length === 0 ? 'NA' : this.formatters[selectedDescriptor.formatter](this.getMaxValue(nodes));
// update the currently displayed min/max/mean
this.nodeStats$.next({
min: nodeMinValue,
max: nodeMaxValue,
mean: nodeMeanValue
});
// only consider the cluster with data in the brush
const cluster = withinBrush.filter((d) => {
return d.id === NIFI_NODE_CONFIG.nifiInstanceId && d.visible && d.values.length > 0;
});
// determine the cluster values
const clusterMinValue =
cluster.length === 0 ? 'NA' : this.formatters[selectedDescriptor.formatter](this.getMinValue(cluster));
const clusterMeanValue =
cluster.length === 0 ? 'NA' : this.formatters[selectedDescriptor.formatter](this.getMeanValue(cluster));
const clusterMaxValue =
cluster.length === 0 ? 'NA' : this.formatters[selectedDescriptor.formatter](this.getMaxValue(cluster));
// update the cluster min/max/mean
this.clusterStats$.next({
min: clusterMinValue,
max: clusterMaxValue,
mean: clusterMeanValue
});
};
// -------------------
// configure the brush
// -------------------
/**
* Updates the axis for the main chart.
*
* @param {array} xDomain The new domain for the x axis
* @param {array} yDomain The new domain for the y axis
*/
const updateAxes = (xDomain: any[], yDomain: any[]) => {
x.domain(xDomain);
y.domain(yDomain);
// update the chart lines
status.selectAll('.chart-line').attr('d', (d: any) => {
return line(d.values);
});
status
.selectAll('circle.mark')
.attr('cx', (v: any) => {
return x(v.timestamp);
})
.attr('cy', (v: any) => {
return y(v.value);
})
.attr('r', function () {
return d3.brushSelection(brushNode.node()) === null ? 1.5 : 4;
});
// update the x axis
chart.select('.x.axis').call(xAxis);
chart.select('.y.axis').call(yAxis);
};
/**
* Handles brush events by updating the main chart according to the context window
* or the control domain if there is no context window.
*/
const brushed = () => {
this.brushSelection = d3.brushSelection(brushNode.node());
let xContextDomain: any[];
let yContextDomain: any[];
// determine the new x and y domains
if (this.brushSelection === null) {
// get the all visible instances
const visibleInstances: any[] = statusData.filter((d: any) => d.visible);
if (visibleInstances.length === 0) {
yContextDomain = yControl.domain();
} else {
yContextDomain = [
d3.min(visibleInstances, (d) => {
return d3.min(d.values, (s: any) => {
return s.value;
});
}),
d3.max(visibleInstances, (d) => {
return d3.max(d.values, (s: any) => {
return s.value;
});
})
];
}
xContextDomain = xControl.domain();
} else {
xContextDomain = [this.brushSelection[0][0], this.brushSelection[1][0]].map(xControl.invert, xControl);
yContextDomain = [this.brushSelection[1][1], this.brushSelection[0][1]].map(yControl.invert, yControl);
}
// update the axes accordingly
updateAxes(xContextDomain, yContextDomain);
// update the aggregate statistics according to the new domain
updateAggregateStatistics();
};
// build the brush
let brush: any = d3
.brush()
.extent([
[xControl.range()[0], yControl.range()[1]],
[xControl.range()[1], yControl.range()[0]]
])
.on('brush', brushed);
// context area
const brushNode: any = control.append('g').attr('class', 'brush').on('click', brushed).call(brush);
if (!!this.brushSelection) {
brush = brush.move(brushNode, this.brushSelection);
}
// add expansion to the extent
control
.select('rect.selection')
.attr('style', 'pointer-events: all;')
.on('dblclick', () => {
if (this.brushSelection !== null) {
// get the y range (this value does not change from the original y domain)
const yRange = yControl.range();
// expand the extent vertically
brush.move(brushNode, [
[this.brushSelection[0][0], yRange[1]],
[this.brushSelection[1][0], yRange[0]]
]);
}
});
// identify all nodes and sort
this.nodes = statusData
.filter((status) => {
return status.id !== NIFI_NODE_CONFIG.nifiInstanceId;
})
.sort((a: any, b: any) => {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
});
brushed();
}
private getChartMinHeight() {
const chartContainer = document.getElementById('status-history-chart-container')!;
const controlContainer = document.getElementById('status-history-chart-control-container')!;
const marginTop: any = controlContainer.computedStyleMap().get('margin-top');
return (
chartContainer.parentElement!.clientHeight - controlContainer.clientHeight - parseInt(marginTop.value, 10)
);
}
private getChartMaxHeight() {
const controlContainer = document.getElementById('status-history-chart-control-container')!;
const marginTop: any = controlContainer.computedStyleMap().get('margin-top');
const statusHistory = document.getElementsByClassName('status-history')![0];
const dialogContent = statusHistory.getElementsByClassName('dialog-content')![0];
const dialogStyles: any = dialogContent.computedStyleMap();
const bodyHeight = document.body.getBoundingClientRect().height;
return (
bodyHeight -
controlContainer.clientHeight -
50 -
parseInt(marginTop.value, 10) -
parseInt(dialogStyles.get('top')?.value) -
parseInt(dialogStyles.get('bottom')?.value)
);
}
private getChartMaxWidth() {
const statusHistory = document.getElementsByClassName('status-history')![0];
const dialogContent = statusHistory.getElementsByClassName('dialog-content')![0];
const dialogContentStyles: any = dialogContent.computedStyleMap();
const fullDialogStyles: any = statusHistory.computedStyleMap();
const bodyWidth = document.body.getBoundingClientRect().width;
return (
bodyWidth -
statusHistory.clientWidth -
parseInt(fullDialogStyles.get('left')?.value, 10) -
parseInt(dialogContentStyles.get('left')?.value) -
parseInt(dialogContentStyles.get('right')?.value)
);
}
private getMinValue(nodeInstances: any): any {
return d3.min(nodeInstances, (d: any) => {
return d3.min(d.values, (s: any) => s.value);
});
}
private getMaxValue(nodeInstances: any): any {
return d3.max(nodeInstances, (d: any) => {
return d3.max(d.values, (s: any) => s.value);
});
}
private getMeanValue(nodeInstances: any[]): any {
let snapshotCount = 0;
const totalValue = d3.sum(nodeInstances, (d: any) => {
snapshotCount += d.values.length;
return d3.sum(d.values, (s: any) => {
return s.value;
});
});
return totalValue / snapshotCount;
}
}

View File

@ -0,0 +1,157 @@
<!--
~ 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.
-->
<div
resizable
(resized)="resized($event)"
[minHeight]="630"
[minWidth]="830"
class="flex flex-col status-history-container">
<ng-container *ngIf="statusHistoryState$ | async; let statusHistoryState">
<h2 mat-dialog-title>Status History</h2>
<div class="status-history flex flex-col grow">
<mat-dialog-content class="grow flex flex-1">
<form [formGroup]="statusHistoryForm" class="flex flex-1 h-full">
<div class="dialog-content flex w-full flex-1">
<div *ngIf="isInitialLoading(statusHistoryState); else loaded" class="flex-1">
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
</div>
<ng-template #loaded>
<ng-container *ngIf="instances.length > 0; else insufficientHistory">
<div
*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>
<div class="flex flex-col">
<div>Start</div>
<div class="value">{{ minDate }}</div>
</div>
<div class="flex flex-col">
<div>End</div>
<div class="value">{{ maxDate }}</div>
</div>
<div class="flex flex-col">
<div class="font-bold">NiFi</div>
<div class="detail-item">
<div>Min / Max / Mean</div>
<div class="value">
{{ clusterStats.min }} / {{ clusterStats.max }} /
{{ clusterStats.mean }}
</div>
</div>
<div class="legend-entry">
<mat-checkbox
value="nifi-instance-id"
(change)="selectNode($event)"
[checked]="!!instanceVisibility['nifi-instance-id']"></mat-checkbox>
<mat-label>NiFi</mat-label>
</div>
</div>
<div class="flex flex-col" *ngIf="!!nodes && nodes.length > 0">
<div>Nodes</div>
<div>
<div>Min / Max / Mean</div>
<div class="value">
{{ nodeStats.min }} / {{ nodeStats.max }} / {{ nodeStats.mean }}
</div>
</div>
<!-- TODO display nodes to select from-->
<div class="legend-entry">
<ng-container *ngFor="let node of nodes">
<mat-checkbox
[value]="node.id"
(change)="selectNode($event)"
[checked]="
!!instanceVisibility['nifi-instance-id']
"></mat-checkbox>
<mat-label>{{ node.label }}</mat-label>
</ng-container>
</div>
</div>
</div>
<div class="chart-panel grow flex flex-col">
<div *ngIf="fieldDescriptors$ | async">
<mat-form-field>
<mat-select formControlName="fieldDescriptor">
<ng-container *ngFor="let descriptor of fieldDescriptors">
<ng-container
*ngIf="descriptor.description; else noDescription">
<mat-option
[value]="descriptor"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(descriptor)"
[delayClose]="false">
<span class="option-text">{{ descriptor.label }}</span>
</mat-option>
</ng-container>
<ng-template #noDescription>
<mat-option [value]="descriptor">
<span class="option-text">{{ descriptor.label }}</span>
</mat-option>
</ng-template>
</ng-container>
</mat-select>
</mat-form-field>
</div>
<status-history-chart
[instances]="instances"
[visibleInstances]="instanceVisibility"
[selectedFieldDescriptor]="selectedDescriptor"
(nodeStats)="nodeStatsChanged($event)"
(clusterStats)="clusterStatsChanged($event)"
class="flex flex-1 flex-col">
</status-history-chart>
</div>
</div>
</ng-container>
<ng-template #insufficientHistory>
<div class="grow flex items-center justify-center">
Insufficient history, please try again later.
</div>
</ng-template>
</ng-template>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions align="start">
<div class="flex w-full">
<div class="flex-1">
<div class="refresh-container flex items-center gap-x-2" *ngIf="instances.length > 0">
<button class="nifi-button" (click)="refresh()">
<i class="fa fa-refresh" [class.fa-spin]="statusHistoryState.status === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="refresh-timestamp">{{ statusHistoryState.loadedTimestamp }}</div>
</div>
</div>
<div>
<button color="primary" mat-raised-button mat-dialog-close>Close</button>
</div>
</div>
</mat-dialog-actions>
</div>
</ng-container>
</div>

View File

@ -0,0 +1,121 @@
/*!
* 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;
.status-history-container {
position: relative;
resize: both;
max-width: 80vw;
max-height: 84vh;
.status-history {
@include mat.button-density(-1);
overflow-y: auto;
.mdc-dialog__content {
padding: 0 16px;
font-size: 14px;
.dialog-content {
min-height: 523px;
overflow-y: auto;
}
}
.mdc-dialog__actions {
// give a little room for the grab/resize handle away from the button.
padding: 10px;
}
.mat-mdc-form-field {
width: 100%;
}
mat-dialog-actions {
margin-top: auto;
}
.status-history-svg {
pointer-events: none;
}
.component-details {
min-width: 285px;
}
.chart-panel {
min-width: 495px;
}
.mat-mdc-dialog-content {
max-height: unset;
}
}
}
:host ::ng-deep #status-history-chart-container,
:host ::ng-deep #status-history-chart-control-container {
background-color: #fff;
overflow: hidden;
cursor: default;
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.chart-area {
clip-path: url(#clip);
}
.chart-line {
fill: none;
stroke-width: 1.5px;
}
.chart-line.over {
stroke-width: 2.5px;
}
.brush .selection {
stroke: #666;
fill-opacity: 0.125;
shape-rendering: crispEdges;
}
}
:host ::ng-deep #status-history-chart-control-container {
height: 125px;
background-color: #fff;
margin-top: 5px;
cursor: default;
overflow: hidden;
}
:host ::ng-deep #status-history-chart-container text,
#status-history-chart-control-container text {
fill: #527991;
font-family: Arial, sans-serif;
font-size: 10px;
}
:host ::ng-deep .mat-mdc-dialog-surface.mdc-dialog__surface {
overflow: hidden !important;
}

View File

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

View File

@ -0,0 +1,253 @@
/*
* 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 { AfterViewInit, Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { StatusHistoryService } from '../../../service/status-history.service';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import {
FieldDescriptor,
NodeSnapshot,
StatusHistoryEntity,
StatusHistoryRequest,
StatusHistoryState
} from '../../../state/status-history';
import { Store } from '@ngrx/store';
import { loadStatusHistory } from '../../../state/status-history/status-history.actions';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import {
selectStatusHistory,
selectStatusHistoryComponentDetails,
selectStatusHistoryFieldDescriptors,
selectStatusHistoryState
} from '../../../state/status-history/status-history.selectors';
import { initialState } from '../../../state/status-history/status-history.reducer';
import { filter, take } from 'rxjs';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import * as d3 from 'd3';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { TextTip } from '../tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
import { TextTipInput } from '../../../state/shared';
import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox';
import { Resizable } from '../resizable/resizable.component';
import { Instance, NIFI_NODE_CONFIG, Stats } from './index';
import { StatusHistoryChart } from './status-history-chart/status-history-chart.component';
@Component({
selector: 'status-history',
templateUrl: './status-history.component.html',
styleUrls: ['./status-history.component.scss'],
standalone: true,
imports: [
MatDialogModule,
AsyncPipe,
MatButtonModule,
NgIf,
NgxSkeletonLoaderModule,
NgForOf,
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
NifiTooltipDirective,
MatCheckboxModule,
Resizable,
StatusHistoryChart
]
})
export class StatusHistory implements OnInit, AfterViewInit {
request: StatusHistoryRequest;
statusHistoryState$ = this.store.select(selectStatusHistoryState);
componentDetails$ = this.store.select(selectStatusHistoryComponentDetails);
statusHistory$ = this.store.select(selectStatusHistory);
fieldDescriptors$ = this.store.select(selectStatusHistoryFieldDescriptors);
fieldDescriptors: FieldDescriptor[] = [];
minDate: string = '';
maxDate: string = '';
statusHistoryForm: FormGroup;
nodeStats: Stats = {
max: 'NA',
min: 'NA',
mean: 'NA'
};
clusterStats: Stats = {
max: 'NA',
min: 'NA',
mean: 'NA'
};
nodes: any[] = [];
instances: Instance[] = [];
instanceVisibility: any = {};
selectedDescriptor: FieldDescriptor | null = null;
constructor(
private statusHistoryService: StatusHistoryService,
private store: Store<StatusHistoryState>,
private formBuilder: FormBuilder,
private nifiCommon: NiFiCommon,
@Inject(MAT_DIALOG_DATA) private dialogRequest: StatusHistoryRequest
) {
this.request = dialogRequest;
this.statusHistoryForm = this.formBuilder.group({
fieldDescriptor: ''
});
}
ngOnInit(): void {
this.refresh();
this.statusHistory$.pipe(filter((entity) => !!entity)).subscribe((entity: StatusHistoryEntity) => {
if (entity) {
this.instances = [];
if (entity.statusHistory?.aggregateSnapshots?.length > 1) {
this.instances.push({
id: NIFI_NODE_CONFIG.nifiInstanceId,
label: NIFI_NODE_CONFIG.nifiInstanceLabel,
snapshots: entity.statusHistory.aggregateSnapshots
});
// if this is the first time this instance is being rendered, make it visible
if (this.instanceVisibility[NIFI_NODE_CONFIG.nifiInstanceId] === undefined) {
this.instanceVisibility[NIFI_NODE_CONFIG.nifiInstanceId] = true;
}
}
// get the status for each node in the cluster if applicable
if (entity.statusHistory?.nodeSnapshots && entity.statusHistory?.nodeSnapshots.length > 1) {
entity.statusHistory.nodeSnapshots.forEach((nodeSnapshot: NodeSnapshot) => {
this.instances.push({
id: nodeSnapshot.nodeId,
label: `${nodeSnapshot.address}:${nodeSnapshot.apiPort}`,
snapshots: nodeSnapshot.statusSnapshots
});
// if this is the first time this instance is being rendered, make it visible
if (this.instanceVisibility[nodeSnapshot.nodeId] === undefined) {
this.instanceVisibility[nodeSnapshot.nodeId] = true;
}
});
}
// identify all nodes and sort
this.nodes = this.instances
.filter((status) => {
return status.id !== NIFI_NODE_CONFIG.nifiInstanceId;
})
.sort((a: any, b: any) => {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
});
// determine the min/max date
const minDate: any = d3.min(this.instances, (d) => {
return d3.min(d.snapshots, (s) => {
return s.timestamp;
});
});
const maxDate: any = d3.max(this.instances, (d) => {
return d3.max(d.snapshots, (s) => {
return s.timestamp;
});
});
this.minDate = this.nifiCommon.formatDateTime(new Date(minDate));
this.maxDate = this.nifiCommon.formatDateTime(new Date(maxDate));
}
});
}
ngAfterViewInit(): void {
this.fieldDescriptors$
.pipe(
filter((descriptors) => !!descriptors),
take(1) // only need to get the descriptors once
)
.subscribe((descriptors) => {
this.fieldDescriptors = descriptors;
// select the first field description by default
this.statusHistoryForm.get('fieldDescriptor')?.setValue(descriptors[0]);
});
// when the selected descriptor changes, update the chart
this.statusHistoryForm.get('fieldDescriptor')?.valueChanges.subscribe((descriptor: FieldDescriptor) => {
if (this.instances.length > 0) {
this.selectedDescriptor = descriptor;
}
});
}
isInitialLoading(state: StatusHistoryState) {
return state.loadedTimestamp === initialState.loadedTimestamp;
}
refresh() {
this.store.dispatch(loadStatusHistory({ request: this.request }));
}
getSelectOptionTipData(descriptor: FieldDescriptor): TextTipInput {
return {
text: descriptor.description
};
}
clusterStatsChanged(stats: Stats) {
this.clusterStats = stats;
}
nodeStatsChanged(stats: Stats) {
this.nodeStats = stats;
}
protected readonly Object = Object;
private resizeChart() {}
protected readonly TextTip = TextTip;
selectNode(event: MatCheckboxChange) {
const instanceId: string = event.source.value;
const checked: boolean = event.checked;
// get the line and the control points for this instance (select all for the line to update control and main charts)
const chartLine = d3.selectAll('path.chart-line-' + instanceId);
const markGroup = d3.select('g.mark-group-' + instanceId);
// determine if it was hidden
const isHidden = markGroup.classed('hidden');
// toggle the visibility
chartLine.classed('hidden', () => !isHidden);
markGroup.classed('hidden', () => !isHidden);
// record the current status so it persists across refreshes
this.instanceVisibility = {
...this.instanceVisibility,
[instanceId]: checked
};
}
resized(event: DOMRect) {
if (this.selectedDescriptor) {
// trigger the chart to re-render by changing the selection
this.selectedDescriptor = { ...this.selectedDescriptor };
}
}
}