13 changed files with 954 additions and 281 deletions
@ -0,0 +1,77 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed 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. |
||||
|
|
||||
|
--> |
||||
|
<table mat-table [dataSource]="dataSource" matSort [matSortActive]="pageLink.sortOrder?.property" |
||||
|
[matSortDirection]="pageLink.sortDirection()" matSortDisableClear> |
||||
|
<ng-container matColumnDef="serviceId"> |
||||
|
<mat-header-cell *matHeaderCellDef style="width: 35%;" mat-sort-header> |
||||
|
Service id |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let serviceData"> |
||||
|
{{ serviceData.serviceId }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="serviceType"> |
||||
|
<mat-header-cell *matHeaderCellDef style="width: 35%;" mat-sort-header> |
||||
|
Service type |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let serviceData"> |
||||
|
{{ serviceData.serviceType }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="cpuUsage"> |
||||
|
<mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header> |
||||
|
CPU |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let serviceData" |
||||
|
[matTooltip]="infoTooltip(serviceData, 'cpu')" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-resource-usage" |
||||
|
[ngClass]="statusClass(serviceData.cpuUsage)"> |
||||
|
{{ serviceData.cpuUsage }}% |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="memoryUsage"> |
||||
|
<mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header> |
||||
|
RAM |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let serviceData" |
||||
|
[matTooltip]="infoTooltip(serviceData, 'ram')" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-resource-usage" |
||||
|
[ngClass]="statusClass(serviceData.memoryUsage)"> |
||||
|
{{ serviceData.memoryUsage }}% |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="discUsage"> |
||||
|
<mat-header-cell *matHeaderCellDef style="width: 10%;" mat-sort-header> |
||||
|
Disk |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let serviceData" |
||||
|
[matTooltip]="infoTooltip(serviceData, 'disc')" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-resource-usage" |
||||
|
[ngClass]="statusClass(serviceData.discUsage)"> |
||||
|
{{ serviceData.discUsage }}% |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row *matRowDef="let serviceData; columns: displayedColumns;"></mat-row> |
||||
|
</table> |
||||
|
<span [fxShow]="dataSource.isEmpty() | async" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found">No data</span> |
||||
@ -0,0 +1,71 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed 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. |
||||
|
*/ |
||||
|
:host { |
||||
|
span.no-data-found { |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
|
||||
|
.tb-resource-usage { |
||||
|
cursor: default; |
||||
|
&.tb-status-warn, &.tb-status-critical { |
||||
|
position: relative; |
||||
|
overflow: visible; |
||||
|
&:before { |
||||
|
position: absolute; |
||||
|
top: 15px; |
||||
|
left: -24px; |
||||
|
font-family: 'Material Icons Round'; |
||||
|
font-size: 20px; |
||||
|
line-height: 1; |
||||
|
} |
||||
|
} |
||||
|
&.tb-status-warn { |
||||
|
color: #FAA405; |
||||
|
&:before { |
||||
|
content: "warning"; |
||||
|
color: #FAA405; |
||||
|
} |
||||
|
} |
||||
|
&.tb-status-critical { |
||||
|
color: #D12730; |
||||
|
&:before { |
||||
|
content: "warning"; |
||||
|
color: #D12730; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mdc-data-table__header-cell { |
||||
|
font-weight: 500; |
||||
|
font-size: 11px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
.mdc-data-table__cell { |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,236 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed 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, ChangeDetectionStrategy, Component, NgZone, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
||||
|
import { BehaviorSubject, Observable, of } from 'rxjs'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; |
||||
|
import { catchError, map, publishReplay, refCount, share } from 'rxjs/operators'; |
||||
|
import { MatSort } from '@angular/material/sort'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; |
||||
|
import { EntityDataUpdate, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { AliasFilterType } from '@shared/models/alias.models'; |
||||
|
import { EntityKeyType } from '@shared/models/query/query.models'; |
||||
|
|
||||
|
export interface SystemInfoData { |
||||
|
serviceId: string; |
||||
|
serviceType: string; |
||||
|
cpuUsage: number; |
||||
|
cpuCount: number; |
||||
|
memoryUsage: number; |
||||
|
totalMemory: number; |
||||
|
discUsage: number; |
||||
|
totalDiscSpace: number; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-cluster-info-table', |
||||
|
templateUrl: './cluster-info-table.component.html', |
||||
|
styleUrls: ['./cluster-info-table.component.scss'], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class ClusterInfoTableComponent extends PageComponent implements OnInit, AfterViewInit { |
||||
|
|
||||
|
@ViewChild(MatSort) sort: MatSort; |
||||
|
|
||||
|
displayedColumns = ['serviceId', 'serviceType', 'cpuUsage', 'memoryUsage', 'discUsage']; |
||||
|
|
||||
|
dataSource: SystemInfoDataSource; |
||||
|
|
||||
|
pageLink: PageLink; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private telemetryWsService: TelemetryWebsocketService, |
||||
|
private zone: NgZone) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.dataSource = new SystemInfoDataSource(this.telemetryWsService, this.zone); |
||||
|
const sortOrder: SortOrder = { |
||||
|
property: 'serviceId', |
||||
|
direction: Direction.ASC |
||||
|
}; |
||||
|
this.pageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); |
||||
|
this.dataSource.loadData(this.pageLink); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.sort.sortChange.subscribe(() => this.updateData()); |
||||
|
} |
||||
|
|
||||
|
updateData() { |
||||
|
this.pageLink.sortOrder.property = this.sort.active; |
||||
|
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
||||
|
this.dataSource.loadData(this.pageLink); |
||||
|
} |
||||
|
|
||||
|
statusClass(value: number): string { |
||||
|
let status = ''; |
||||
|
if (value > 85) { |
||||
|
status = 'tb-status-critical'; |
||||
|
} else if (value > 75) { |
||||
|
status = 'tb-status-warn'; |
||||
|
} |
||||
|
return status; |
||||
|
} |
||||
|
|
||||
|
infoTooltip(serviceData: SystemInfoData, type: 'cpu' | 'ram' | 'disc'): string { |
||||
|
let tooltip = ''; |
||||
|
let value: number; |
||||
|
switch (type) { |
||||
|
case 'cpu': |
||||
|
value = serviceData.cpuUsage; |
||||
|
tooltip += value + ' / 100 %'; |
||||
|
break; |
||||
|
case 'ram': |
||||
|
value = serviceData.memoryUsage; |
||||
|
const memoryUsage = serviceData.memoryUsage; |
||||
|
const totalMemory = serviceData.totalMemory / 1073741824; |
||||
|
const usedMemory = (totalMemory * (memoryUsage / 100)).toFixed(0); |
||||
|
tooltip += usedMemory + ' / ' + totalMemory.toFixed(0) + ' Gb'; |
||||
|
break; |
||||
|
case 'disc': |
||||
|
value = serviceData.discUsage; |
||||
|
const discUsage = serviceData.discUsage; |
||||
|
const totalDiscSpace = serviceData.totalDiscSpace / 1073741824; |
||||
|
const usedDisc = (totalDiscSpace * (discUsage / 100)).toFixed(0); |
||||
|
tooltip += usedDisc + ' / ' + totalDiscSpace.toFixed(0) + ' Gb'; |
||||
|
break; |
||||
|
} |
||||
|
if (value > 85) { |
||||
|
switch (type) { |
||||
|
case 'cpu': |
||||
|
tooltip += '\n\nCritically high CPU usage. To avoid system failure, optimize system performance.'; |
||||
|
break; |
||||
|
case 'ram': |
||||
|
tooltip += '\n\nCritically low reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.'; |
||||
|
break; |
||||
|
case 'disc': |
||||
|
tooltip += '\n\nCritically low disk space. To avoid data loss, free up or expand the disk space.'; |
||||
|
break; |
||||
|
} |
||||
|
} else if (value > 75) { |
||||
|
switch (type) { |
||||
|
case 'cpu': |
||||
|
tooltip += '\n\nRunning high on CPU usage. To avoid system failure, optimize system performance.'; |
||||
|
break; |
||||
|
case 'ram': |
||||
|
tooltip += '\n\nRunning low on reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.'; |
||||
|
break; |
||||
|
case 'disc': |
||||
|
tooltip += '\n\nRunning low on disk space. To avoid data loss, free up or expand the disk space.'; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return tooltip; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class SystemInfoDataSource implements DataSource<SystemInfoData> { |
||||
|
|
||||
|
private systemDataSubject = new BehaviorSubject<SystemInfoData[]>([]); |
||||
|
private pageDataSubject = new BehaviorSubject<PageData<SystemInfoData>>(emptyPageData<SystemInfoData>()); |
||||
|
|
||||
|
public pageData$ = this.pageDataSubject.asObservable(); |
||||
|
|
||||
|
private allSystemData: Observable<Array<SystemInfoData>>; |
||||
|
|
||||
|
private telemetrySubscriber: TelemetrySubscriber; |
||||
|
|
||||
|
constructor(private telemetryWsService: TelemetryWebsocketService, |
||||
|
private zone: NgZone) { |
||||
|
} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<SystemInfoData[] | ReadonlyArray<SystemInfoData>> { |
||||
|
return this.systemDataSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.systemDataSubject.complete(); |
||||
|
this.pageDataSubject.complete(); |
||||
|
if (this.telemetrySubscriber) { |
||||
|
this.telemetrySubscriber.unsubscribe(); |
||||
|
this.telemetrySubscriber = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
loadData(pageLink: PageLink): void { |
||||
|
this.getAllSystemData().pipe( |
||||
|
map((data) => pageLink.filterData(data)), |
||||
|
catchError(() => of(emptyPageData<SystemInfoData>())), |
||||
|
).subscribe( |
||||
|
(pageData) => { |
||||
|
this.onSystemData(pageData.data); |
||||
|
this.pageDataSubject.next(pageData); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getAllSystemData(): Observable<Array<SystemInfoData>> { |
||||
|
if (!this.allSystemData) { |
||||
|
this.telemetrySubscriber = TelemetrySubscriber.createEntityFilterLatestSubscription( |
||||
|
this.telemetryWsService, { type: AliasFilterType.apiUsageState }, this.zone, |
||||
|
[ { key: 'clusterSystemData', type: EntityKeyType.TIME_SERIES } ]); |
||||
|
this.telemetrySubscriber.subscribe(); |
||||
|
this.allSystemData = this.telemetrySubscriber.entityData$.pipe( |
||||
|
map((update) => this.toSystemInfoData(update)), |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} |
||||
|
return this.allSystemData; |
||||
|
} |
||||
|
|
||||
|
private toSystemInfoData(entityDataUpdate: EntityDataUpdate): Array<SystemInfoData> { |
||||
|
const entityData = entityDataUpdate.data?.data ? entityDataUpdate.data?.data : entityDataUpdate.update; |
||||
|
if (entityData && entityData.length) { |
||||
|
const latest = entityData[0].latest; |
||||
|
if (latest && latest[EntityKeyType.TIME_SERIES]) { |
||||
|
const latestTs = latest[EntityKeyType.TIME_SERIES]; |
||||
|
const tsValue = latestTs.clusterSystemData; |
||||
|
if (tsValue && tsValue.value) { |
||||
|
return JSON.parse(tsValue.value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
protected onSystemData(data: SystemInfoData[]) { |
||||
|
this.systemDataSubject.next(data); |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.systemDataSubject.pipe( |
||||
|
map((entities) => !entities.length), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.pageDataSubject.pipe( |
||||
|
map((pageData) => pageData.totalElements), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed 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 { SharedModule } from '@app/shared/shared.module'; |
||||
|
import { ClusterInfoTableComponent } from '@home/components/widget/lib/home-page/cluster-info-table.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: |
||||
|
[ |
||||
|
ClusterInfoTableComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule |
||||
|
], |
||||
|
exports: [ |
||||
|
ClusterInfoTableComponent |
||||
|
] |
||||
|
}) |
||||
|
export class HomePageWidgetsModule { } |
||||
Binary file not shown.
Loading…
Reference in new issue