37 changed files with 1212 additions and 223 deletions
@ -0,0 +1,37 @@ |
|||
///
|
|||
/// 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 { Injectable } from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
|||
import { UsageInfo } from '@shared/models/usage.models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class UsageInfoService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) {} |
|||
|
|||
public getUsageInfo(config?: RequestConfig): Observable<UsageInfo> { |
|||
return this.http.get<UsageInfo>('/api/usage', |
|||
defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
/** |
|||
* 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 "../../../../../../../scss/constants"; |
|||
|
|||
:host { |
|||
|
|||
.tb-card-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.tb-title { |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
@media #{$mat-md-lg} { |
|||
font-size: 12px; |
|||
line-height: 16px; |
|||
} |
|||
} |
|||
|
|||
.tb-title-link { |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
position: relative; |
|||
border-bottom: none; |
|||
@media #{$mat-md-lg} { |
|||
font-size: 12px; |
|||
line-height: 16px; |
|||
} |
|||
&:hover, &:focus { |
|||
border-bottom: none; |
|||
} |
|||
&::after { |
|||
content: 'arrow_forward'; |
|||
display: inline-block; |
|||
transform: rotate(315deg); |
|||
font-family: 'Material Icons'; |
|||
font-weight: normal; |
|||
font-style: normal; |
|||
font-size: 18px; |
|||
color: rgba(0, 0, 0, 0.12); |
|||
vertical-align: bottom; |
|||
margin-left: 6px; |
|||
} |
|||
&:hover::after { |
|||
color: inherit; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> |
|||
<div class="tb-card-header"> |
|||
<div class="tb-title">{{ 'widgets.quick-links.title' | translate }}</div> |
|||
<button class="tb-title-icon" |
|||
matTooltip="{{ 'action.edit' | translate }}" |
|||
matTooltipPosition="above" |
|||
mat-icon-button |
|||
(click)="edit()"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
</div> |
|||
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize"> |
|||
<mat-grid-tile class="tb-links-tile" *ngFor="let quickLink of menuLinks$() | async"> |
|||
<a fxFlex class="tb-link-button" |
|||
[routerLink]="quickLink.path"> |
|||
<div class="tb-link-container"> |
|||
<div class="tb-link-icon-container"> |
|||
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon> |
|||
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon> |
|||
</div> |
|||
<div class="tb-link-text">{{ quickLink.name | translate }}</div> |
|||
</div> |
|||
</a> |
|||
</mat-grid-tile> |
|||
<mat-grid-tile class="tb-links-tile"> |
|||
<div fxFlex class="tb-add-link-button" |
|||
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="addLink()"> |
|||
<mat-icon class="tb-add-icon">add</mat-icon> |
|||
</div> |
|||
</mat-grid-tile> |
|||
</mat-grid-list> |
|||
</div> |
|||
@ -0,0 +1,162 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { map, Observable, of, Subscription } from 'rxjs'; |
|||
import { QuickLinks } from '@shared/models/user-settings.models'; |
|||
import { UserSettingsService } from '@core/http/user-settings.service'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; |
|||
import { MediaBreakpoints } from '@shared/models/constants'; |
|||
import { MenuService } from '@core/services/menu.service'; |
|||
import { MenuSection } from '@core/services/menu.models'; |
|||
|
|||
const defaultQuickLinksMap = new Map<Authority, QuickLinks>( |
|||
[ |
|||
[Authority.SYS_ADMIN, { |
|||
links: ['tenants', 'tenantProfiles'] |
|||
}], |
|||
[Authority.TENANT_ADMIN, { |
|||
links: ['alarms', 'dashboards', 'entitiesDevices'] |
|||
}], |
|||
[Authority.CUSTOMER_USER, { |
|||
links: ['alarms', 'dashboards', 'entitiesDevices'] |
|||
}] |
|||
] |
|||
); |
|||
|
|||
interface QuickLinksWidgetSettings { |
|||
columns: number; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-quick-links-widget', |
|||
templateUrl: './quick-links-widget.component.html', |
|||
styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'] |
|||
}) |
|||
export class QuickLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
settings: QuickLinksWidgetSettings; |
|||
columns: number; |
|||
rowHeight = '55px'; |
|||
gutterSize = '12px'; |
|||
|
|||
quickLinks: QuickLinks; |
|||
authUser = getCurrentAuthUser(this.store); |
|||
|
|||
private observeBreakpointSubscription: Subscription; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private cd: ChangeDetectorRef, |
|||
private userSettingsService: UserSettingsService, |
|||
private dialog: MatDialog, |
|||
private menuService: MenuService, |
|||
private breakpointObserver: BreakpointObserver) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.settings = this.ctx.settings; |
|||
this.columns = this.settings.columns || 3; |
|||
const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); |
|||
this.rowHeight = isMdLg ? '18px' : '55px'; |
|||
this.gutterSize = isMdLg ? '8px' : '12px'; |
|||
this.observeBreakpointSubscription = this.breakpointObserver |
|||
.observe(MediaBreakpoints['md-lg']) |
|||
.subscribe((state: BreakpointState) => { |
|||
if (state.matches) { |
|||
this.rowHeight = '18px'; |
|||
this.gutterSize = '8px'; |
|||
} else { |
|||
this.rowHeight = '55px'; |
|||
this.gutterSize = '12px'; |
|||
} |
|||
this.cd.markForCheck(); |
|||
} |
|||
); |
|||
this.loadQuickLinks(); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
if (this.observeBreakpointSubscription) { |
|||
this.observeBreakpointSubscription.unsubscribe(); |
|||
} |
|||
super.ngOnDestroy(); |
|||
} |
|||
|
|||
menuLinks$(): Observable<Array<MenuSection>> { |
|||
return this.quickLinks ? this.menuService.menuLinksByIds(this.quickLinks.links) : of([]); |
|||
} |
|||
|
|||
private loadQuickLinks() { |
|||
this.userSettingsService.getQuickLinks().pipe( |
|||
map((quickLinks) => { |
|||
if (!quickLinks || !quickLinks.links) { |
|||
return defaultQuickLinksMap.get(this.authUser.authority); |
|||
} else { |
|||
return quickLinks; |
|||
} |
|||
}) |
|||
).subscribe( |
|||
(quickLinks) => { |
|||
this.quickLinks = quickLinks; |
|||
this.cd.markForCheck(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
edit() { |
|||
/* this.dialog.open<EditDocLinksDialogComponent, EditDocLinksDialogData, |
|||
boolean>(EditDocLinksDialogComponent, { |
|||
disableClose: true, |
|||
autoFocus: false, |
|||
data: { |
|||
docLinks: this.documentationLinks |
|||
}, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed().subscribe( |
|||
(result) => { |
|||
if (result) { |
|||
this.loadDocLinks(); |
|||
} |
|||
}); */ |
|||
} |
|||
|
|||
addLink() { |
|||
/* this.dialog.open<AddDocLinkDialogComponent, any, |
|||
DocumentationLink>(AddDocLinkDialogComponent, { |
|||
disableClose: true, |
|||
autoFocus: false, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed().subscribe( |
|||
(docLink) => { |
|||
if (docLink) { |
|||
this.documentationLinks.links.push(docLink); |
|||
this.cd.markForCheck(); |
|||
this.userSettingsService.updateDocumentationLinks(this.documentationLinks).subscribe(); |
|||
} |
|||
}); */ |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> |
|||
<div class="tb-card-header"> |
|||
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a> |
|||
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[ |
|||
{ |
|||
name: ctx.translate.instant('widgets.usage-info.entities'), |
|||
value: 'entities' |
|||
}, |
|||
{ |
|||
name: ctx.translate.instant('widgets.usage-info.api-calls'), |
|||
value: 'apiCalls' |
|||
} |
|||
]"> |
|||
</tb-toggle-header> |
|||
</div> |
|||
<ng-container [ngSwitch]="usageToggle.value"> |
|||
<ng-template [ngSwitchCase]="'entities'"> |
|||
<div fxFlex class="tb-usage-list"> |
|||
<div class="tb-usage-items"> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div> |
|||
</div> |
|||
<div class="tb-usage-items-values"> |
|||
<div class="tb-usage-items-counts"> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div> |
|||
</div> |
|||
<div class="tb-usage-items-progress"> |
|||
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="'apiCalls'"> |
|||
<div fxFlex class="tb-usage-list"> |
|||
<div class="tb-usage-items"> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div> |
|||
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div> |
|||
</div> |
|||
<div class="tb-usage-items-values"> |
|||
<div class="tb-usage-items-counts"> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div> |
|||
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div> |
|||
</div> |
|||
<div class="tb-usage-items-progress"> |
|||
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar> |
|||
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
</ng-container> |
|||
</div> |
|||
@ -0,0 +1,108 @@ |
|||
/** |
|||
* 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 "../../../../../../../scss/constants"; |
|||
|
|||
:host { |
|||
.tb-card-header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.tb-usage-list { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.tb-usage-items, .tb-usage-items-counts, .tb-usage-items-progress { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: space-evenly; |
|||
align-items: flex-start; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.tb-usage-items-progress { |
|||
width: 34px; |
|||
@media #{$mat-md} { |
|||
display: none; |
|||
} |
|||
.mdc-linear-progress { |
|||
height: 8px; |
|||
margin-top: 6px; |
|||
margin-bottom: 6px; |
|||
border-radius: 2px; |
|||
@media #{$mat-md-lg} { |
|||
margin-top: 4px; |
|||
margin-bottom: 4px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-usage-items-values { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: stretch; |
|||
gap: 16px; |
|||
} |
|||
|
|||
.tb-usage-item, .tb-usage-item-counts { |
|||
font-weight: 400; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
letter-spacing: 0.2px; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
width: 100%; |
|||
@media #{$mat-md-lg} { |
|||
font-size: 11px; |
|||
line-height: 16px; |
|||
} |
|||
&.critical { |
|||
color: #D12730; |
|||
} |
|||
} |
|||
|
|||
.tb-usage-item { |
|||
color: rgba(0, 0, 0, 0.38); |
|||
} |
|||
|
|||
.tb-usage-item-counts { |
|||
color: rgba(0, 0, 0, 0.76); |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-usage-items-progress { |
|||
.mat-mdc-progress-bar { |
|||
.mdc-linear-progress__bar-inner { |
|||
border-top-width: 8px; |
|||
} |
|||
&.critical { |
|||
.mdc-linear-progress__buffer-bar { |
|||
background: rgba(209, 39, 48, 0.06); |
|||
} |
|||
.mdc-linear-progress__bar-inner { |
|||
border-top-color: #D12730; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { of } from 'rxjs'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { UsageInfo } from '@shared/models/usage.models'; |
|||
import { UsageInfoService } from '@core/http/usage-info.service'; |
|||
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-usage-info-widget', |
|||
templateUrl: './usage-info-widget.component.html', |
|||
styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss'] |
|||
}) |
|||
export class UsageInfoWidgetComponent extends PageComponent implements OnInit, OnDestroy { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
usageInfo: UsageInfo; |
|||
authUser = getCurrentAuthUser(this.store); |
|||
|
|||
toggleValue: 'entities' | 'apiCalls' = 'entities'; |
|||
|
|||
entityItemCritical: {[key: string]: boolean} = {}; |
|||
apiCallItemCritical: {[key: string]: boolean} = {}; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private cd: ChangeDetectorRef, |
|||
private shortNumberPipe: ShortNumberPipe, |
|||
private usageInfoService: UsageInfoService) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
(this.authUser.authority === Authority.TENANT_ADMIN ? |
|||
this.usageInfoService.getUsageInfo() : of(null)).subscribe( |
|||
(usageInfo) => { |
|||
this.usageInfo = usageInfo; |
|||
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices); |
|||
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets); |
|||
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers); |
|||
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards); |
|||
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers); |
|||
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages, |
|||
this.usageInfo?.maxTransportMessages); |
|||
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions); |
|||
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms); |
|||
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails); |
|||
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms); |
|||
let entitiesHasCriticalItem = false; |
|||
let apiCallsHasCriticalItem = false; |
|||
for (const key of Object.keys(this.entityItemCritical)) { |
|||
if (this.entityItemCritical[key]) { |
|||
entitiesHasCriticalItem = true; |
|||
break; |
|||
} |
|||
} |
|||
for (const key of Object.keys(this.apiCallItemCritical)) { |
|||
if (this.apiCallItemCritical[key]) { |
|||
apiCallsHasCriticalItem = true; |
|||
break; |
|||
} |
|||
} |
|||
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) { |
|||
this.toggleValue = 'apiCalls'; |
|||
} |
|||
this.cd.markForCheck(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
maxValue(max: number): number | string { |
|||
return max ? this.shortNumberPipe.transform(max) : '∞'; |
|||
} |
|||
|
|||
progressValue(value: number, max: number): number { |
|||
if (max && value) { |
|||
return (value / max) * 100; |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
private isItemCritical(value: number, max: number): boolean { |
|||
if (max && value) { |
|||
return (value / max) >= 0.85; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<section class="tb-widget-settings" [formGroup]="quickLinksWidgetSettingsForm" fxLayout="column"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>widgets.quick-links.columns</mat-label> |
|||
<input required matInput type="number" step="1" min="1" max="20" formControlName="columns"> |
|||
</mat-form-field> |
|||
</section> |
|||
@ -0,0 +1,52 @@ |
|||
///
|
|||
/// 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 { Component } from '@angular/core'; |
|||
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-quick-links-widget-settings', |
|||
templateUrl: './quick-links-widget-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss'] |
|||
}) |
|||
export class QuickLinksWidgetSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
quickLinksWidgetSettingsForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.quickLinksWidgetSettingsForm; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return { |
|||
columns: 3 |
|||
}; |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.quickLinksWidgetSettingsForm = this.fb.group({ |
|||
columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]] |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
///
|
|||
/// 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.
|
|||
///
|
|||
|
|||
export interface UsageInfo { |
|||
devices: number; |
|||
maxDevices: number; |
|||
assets: number; |
|||
maxAssets: number; |
|||
customers: number; |
|||
maxCustomers: number; |
|||
users: number; |
|||
maxUsers: number; |
|||
dashboards: number; |
|||
maxDashboards: number; |
|||
|
|||
transportMessages: number; |
|||
maxTransportMessages: number; |
|||
jsExecutions: number; |
|||
maxJsExecutions: number; |
|||
emails: number; |
|||
maxEmails: number; |
|||
sms: number; |
|||
maxSms: number; |
|||
alarms: number; |
|||
maxAlarms: number; |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
///
|
|||
/// 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 { Pipe, PipeTransform } from '@angular/core'; |
|||
|
|||
@Pipe({ |
|||
name: 'shortNumber' |
|||
}) |
|||
export class ShortNumberPipe implements PipeTransform { |
|||
|
|||
transform(number: number, args?: any): any { |
|||
if (isNaN(number)) return 0; |
|||
if (number === null) return 0; |
|||
if (number === 0) return 0; |
|||
let abs = Math.abs(number); |
|||
const rounder = Math.pow(10, 1); |
|||
const isNegative = number < 0; |
|||
const isLong = args && args.long; |
|||
let key = ''; |
|||
|
|||
const powers = [ |
|||
{key: 'Q', longKey: ' quadrillion', value: Math.pow(10, 15)}, |
|||
{key: 'T', longKey: ' trillion', value: Math.pow(10, 12)}, |
|||
{key: 'B', longKey: ' billion', value: Math.pow(10, 9)}, |
|||
{key: 'M', longKey: ' million', value: Math.pow(10, 6)}, |
|||
{key: 'K', longKey: ' thousand', value: 1000} |
|||
]; |
|||
|
|||
for (let i = 0; i < powers.length; i++) { |
|||
let reduced = abs / powers[i].value; |
|||
reduced = Math.round(reduced * rounder) / rounder; |
|||
if (reduced >= 1) { |
|||
abs = reduced; |
|||
key = isLong ? powers[i].longKey : powers[i].key; |
|||
break; |
|||
} |
|||
} |
|||
return (isNegative ? '-' : '') + abs + key; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue