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