mirror of
https://github.com/apache/nifi.git
synced 2025-02-06 01:58:32 +00:00
[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:
parent
231dbde4b3
commit
5e3239f8c1
@ -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"
|
||||
|
@ -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],
|
||||
|
@ -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: [
|
||||
{
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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 {}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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());
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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`);
|
||||
}
|
||||
}
|
@ -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`);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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';
|
||||
}
|
@ -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 }>()
|
||||
);
|
@ -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 }
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {}
|
@ -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 {}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 {}
|
@ -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 {}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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`
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
};
|
||||
|
@ -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';
|
||||
}
|
@ -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 }>()
|
||||
);
|
@ -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 }
|
||||
);
|
||||
}
|
@ -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
|
||||
}))
|
||||
);
|
@ -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
|
||||
);
|
@ -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());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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.
|
||||
*/
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user