committed by
GitHub
17 changed files with 9302 additions and 2688 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,54 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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-api-usage-panel" [style.padding]="padding" [style]="backgroundStyle$ | async"> |
|||
<div class="tb-api-usage-overlay" [style]="overlayStyle"></div> |
|||
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> |
|||
<div class="tb-api-usage-content"> |
|||
<div class="api-items-list"> |
|||
@for (api of apiUsages; track api){ |
|||
<div class="api-item {{api.status.value}}" |
|||
[class.cursor-pointer]="api.state" |
|||
[class.active]="api.state && api.state === currentState" |
|||
(click)="updateState($event, api.state)"> |
|||
<div class="api-item-content"> |
|||
<div class="api-item-title">{{ api.label }}</div> |
|||
<div class="api-item-statistic"> |
|||
<div class="api-item-statistic-count">{{ api.current.value }} / {{ api.maxLimit.value }}</div> |
|||
<div class="api-item-statistic-progress"> |
|||
<mat-progress-bar mode="determinate" |
|||
[value]="api.progress" |
|||
></mat-progress-bar> |
|||
</div> |
|||
<div class="api-item-statistic-status"> |
|||
<div class="statistic-status">{{ ('api-usage.status.' + api.status.value) | translate }}</div> |
|||
<div class="statistic-icon"> |
|||
@if (api.status.value === 'enabled') { |
|||
<tb-icon class="tb-mat-18">check_circle</tb-icon> |
|||
} @else { |
|||
<tb-icon class="tb-mat-18">warning</tb-icon> |
|||
} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<mat-divider></mat-divider> |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,125 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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"; |
|||
|
|||
$enabled-color: #198038; |
|||
$disabled-color: #D12730; |
|||
$warning-color: #FAA405; |
|||
|
|||
.tb-no-notification-svg-color { |
|||
color: $tb-primary-color; |
|||
} |
|||
|
|||
.tb-api-usage-panel { |
|||
> div:not(.tb-api-usage-overlay) { |
|||
z-index: 1; |
|||
} |
|||
.tb-api-usage-overlay { |
|||
position: absolute; |
|||
top: 12px; |
|||
left: 12px; |
|||
bottom: 12px; |
|||
right: 12px; |
|||
} |
|||
.tb-api-usage-content { |
|||
height: 100%; |
|||
min-height: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
.api-items-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
width: 100%; |
|||
.api-item { |
|||
&.enabled { |
|||
.api-item-statistic-status { |
|||
color: $enabled-color; |
|||
} |
|||
.mat-mdc-progress-bar { |
|||
--mdc-linear-progress-active-indicator-color: #{$enabled-color}; |
|||
} |
|||
} |
|||
&.disabled { |
|||
.api-item-statistic-status { |
|||
color: $disabled-color; |
|||
} |
|||
.mat-mdc-progress-bar { |
|||
--mdc-linear-progress-active-indicator-color: #{$disabled-color}; |
|||
} |
|||
} |
|||
&.warning { |
|||
.api-item-statistic-status { |
|||
color: $warning-color; |
|||
} |
|||
.mat-mdc-progress-bar { |
|||
--mdc-linear-progress-active-indicator-color: #{$warning-color}; |
|||
} |
|||
} |
|||
|
|||
&:hover { |
|||
background-color: rgba(0, 0, 0, 0.06); |
|||
} |
|||
&.active { |
|||
background-color: rgba($tb-primary-color, 0.06); |
|||
.mat-divider { |
|||
--mat-divider-color: #{$tb-primary-color}; |
|||
} |
|||
} |
|||
|
|||
.api-item-content { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
gap: 8px; |
|||
padding: 5px 16px; |
|||
.api-item-title { |
|||
display: flex; |
|||
flex: 1; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: $tb-primary-color; |
|||
} |
|||
.api-item-statistic { |
|||
display: flex; |
|||
flex: 1; |
|||
flex-direction: column; |
|||
&-count { |
|||
font-size: 14px; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
&-status { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
font-size: 14px; |
|||
} |
|||
&-progress { |
|||
--mdc-linear-progress-track-height: 8px; |
|||
--mdc-linear-progress-active-indicator-height: 8px; |
|||
padding: 4px 0; |
|||
.mat-mdc-progress-bar { |
|||
border-radius: 6px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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, TemplateRef, ViewEncapsulation } from '@angular/core'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/widget-settings.models'; |
|||
import { Observable } from 'rxjs'; |
|||
import { ImagePipe } from '@shared/pipe/image.pipe'; |
|||
import { DomSanitizer } from '@angular/platform-browser'; |
|||
import { DataKey, DatasourceType, widgetType } from "@shared/models/widget.models"; |
|||
import { WidgetSubscriptionOptions } from "@core/api/widget-api.models"; |
|||
import { formattedDataFormDatasourceData } from "@core/utils"; |
|||
|
|||
import { UtilsService } from "@core/services/utils.service"; |
|||
import { |
|||
ApiUsageDataKeysSettings, |
|||
apiUsageDefaultSettings, |
|||
ApiUsageWidgetSettings |
|||
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; |
|||
|
|||
@Component({ |
|||
selector: 'tb-api-usage-widget', |
|||
templateUrl: './api-usage-widget.component.html', |
|||
styleUrls: ['api-usage-widget.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class ApiUsageWidgetComponent implements OnInit, OnDestroy { |
|||
|
|||
settings: ApiUsageWidgetSettings; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
widgetTitlePanel: TemplateRef<any>; |
|||
|
|||
backgroundStyle$: Observable<ComponentStyle>; |
|||
overlayStyle: ComponentStyle = {}; |
|||
padding: string; |
|||
|
|||
apiUsages = []; |
|||
currentState = ''; |
|||
noDataDisplayMessageText: string; |
|||
|
|||
private contentResize$: ResizeObserver; |
|||
private powers: {key: string, value: number}[] = [ |
|||
{ key: 'Q', value: 1e15 }, |
|||
{ key: 'T', value: 1e12 }, |
|||
{ key: 'B', value: 1e9 }, |
|||
{ key: 'M', value: 1e6 }, |
|||
{ key: 'K', value: 1e3 } |
|||
]; |
|||
|
|||
constructor(private imagePipe: ImagePipe, |
|||
private utils: UtilsService, |
|||
private sanitizer: DomSanitizer, |
|||
private cd: ChangeDetectorRef) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.apiUsageWidget = this; |
|||
this.settings = {...apiUsageDefaultSettings, ...this.ctx.settings}; |
|||
|
|||
this.parseApiUsages(); |
|||
|
|||
const ds = { |
|||
type: DatasourceType.entity, |
|||
name: '', |
|||
entityAliasId: this.settings.dsEntityAliasId, |
|||
dataKeys: this.getUniqueDataKeys(this.settings.dataKeys) |
|||
} |
|||
|
|||
const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = { |
|||
datasources: [ds], |
|||
useDashboardTimewindow: false, |
|||
type: widgetType.latest, |
|||
callbacks: { |
|||
onDataUpdated: (subscription) => { |
|||
const data = formattedDataFormDatasourceData(subscription.data); |
|||
this.apiUsages.forEach(key => { |
|||
const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0; |
|||
key.progress = isFinite(progress) ? progress : 0; |
|||
key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled'; |
|||
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞'; |
|||
key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0; |
|||
}); |
|||
this.cd.detectChanges(); |
|||
} |
|||
} |
|||
}; |
|||
this.ctx.subscriptionApi.createSubscription(apiUsageSubscriptionOptions, true).subscribe(); |
|||
|
|||
this.currentState = this.ctx.stateController.getStateId(); |
|||
this.ctx.stateController.stateId().subscribe((state) => { |
|||
this.ctx.updateParamsFromData(true); |
|||
this.currentState = state; |
|||
this.cd.markForCheck(); |
|||
}); |
|||
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); |
|||
this.overlayStyle = overlayStyle(this.settings.background.overlay); |
|||
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; |
|||
} |
|||
|
|||
updateState($event: MouseEvent, stateName: string) { |
|||
$event?.preventDefault(); |
|||
if (stateName?.length) { |
|||
this.ctx.stateController.updateState(stateName, this.ctx.stateController.getStateParams(), this.ctx.isMobile); |
|||
} |
|||
} |
|||
|
|||
parseApiUsages() { |
|||
this.settings.dataKeys.forEach((key) => { |
|||
this.apiUsages.push({ |
|||
label: this.utils.customTranslation(key.label, key.label), |
|||
state: key.state, |
|||
progress: 0, |
|||
status: {key: key.status.name, value: 'enabled'}, |
|||
maxLimit: {key: key.maxLimit.name, value: '∞'}, |
|||
current: {key: key.current.name, value: 0}, |
|||
}); |
|||
}) |
|||
} |
|||
|
|||
getUniqueDataKeys(data: ApiUsageDataKeysSettings[]): DataKey[] { |
|||
const seenNames = new Set<string>(); |
|||
return data |
|||
.flatMap(item => [item.status, item.maxLimit, item.current]) |
|||
.filter(key => { |
|||
if (seenNames.has(key.name)) { |
|||
return false; |
|||
} |
|||
seenNames.add(key.name); |
|||
return true; |
|||
}); |
|||
}; |
|||
|
|||
|
|||
ngOnDestroy() { |
|||
if (this.contentResize$) { |
|||
this.contentResize$.disconnect(); |
|||
} |
|||
} |
|||
|
|||
private toShortNumber(number: any, decimals = 1) { |
|||
if (!Number.isFinite(number) || number < 0) { |
|||
return '0'; |
|||
} |
|||
for (const power of this.powers) { |
|||
if (number >= power.value) { |
|||
const reduced = number / power.value; |
|||
const rounded = Number(reduced.toFixed(decimals)); |
|||
return `${rounded}${power.key}`; |
|||
} |
|||
} |
|||
return `${Number(number.toFixed(decimals))}`; |
|||
} |
|||
|
|||
public onInit() { |
|||
const borderRadius = this.ctx.$widgetElement.css('borderRadius'); |
|||
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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 [formGroup]="dataKeyFormGroup" class="tb-form-table-row tb-api-usage-data-key-row"> |
|||
<div class="tb-source-field"> |
|||
<mat-form-field |
|||
class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
<tb-string-autocomplete [fetchOptionsFn]="fetchDashboardStates.bind(this)" |
|||
placeholderText="{{ 'widget-config.set' | translate }}" |
|||
style="flex: 1" |
|||
formControlName="state"> |
|||
</tb-string-autocomplete> |
|||
</div> |
|||
<div class="tb-data-key-fields"> |
|||
<tb-data-key-input |
|||
class="tb-data-key-field" |
|||
required |
|||
requiredText="{{ 'widgets.api-usage.status-required'}}" |
|||
[datasourceType]="DatasourceType.entity" |
|||
[entityAliasId]="dsEntityAliasId" |
|||
[aliasController]="context.aliasController" |
|||
[widgetType]="widgetType.latest" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="context.callbacks" |
|||
[generateKey]="context.generateDataKey" |
|||
(keyEdit)="editKey('status')" |
|||
formControlName="status"> |
|||
</tb-data-key-input> |
|||
<tb-data-key-input |
|||
class="tb-data-key-field" |
|||
required |
|||
requiredText="{{ 'widgets.api-usage.limit-required'}}" |
|||
[datasourceType]="DatasourceType.entity" |
|||
[entityAliasId]="dsEntityAliasId" |
|||
[aliasController]="context.aliasController" |
|||
[widgetType]="widgetType.latest" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="context.callbacks" |
|||
[generateKey]="context.generateDataKey" |
|||
(keyEdit)="editKey('maxLimit')" |
|||
formControlName="maxLimit"> |
|||
</tb-data-key-input> |
|||
<tb-data-key-input |
|||
class="tb-data-key-field" |
|||
required |
|||
requiredText="{{ 'widgets.api-usage.current-number-required'}}" |
|||
[datasourceType]="DatasourceType.entity" |
|||
[entityAliasId]="dsEntityAliasId" |
|||
[aliasController]="context.aliasController" |
|||
[widgetType]="widgetType.latest" |
|||
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
|||
[callbacks]="context.callbacks" |
|||
[generateKey]="context.generateDataKey" |
|||
(keyEdit)="editKey('current')" |
|||
formControlName="current"> |
|||
</tb-data-key-input> |
|||
</div> |
|||
<div class="tb-form-table-row-cell-buttons"> |
|||
<div class="tb-remove-button"> |
|||
<button type="button" |
|||
mat-icon-button |
|||
(click)="dataKeyRemoved.emit()" |
|||
matTooltip="{{ 'widgets.api-usage.delete-key' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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'; |
|||
|
|||
.tb-form-table-row.tb-api-usage-data-key-row { |
|||
|
|||
.tb-source-field { |
|||
flex: 1 1 25%; |
|||
display: flex; |
|||
gap: 12px; |
|||
.tb-label-field { |
|||
flex: 1; |
|||
} |
|||
} |
|||
|
|||
.tb-data-key-fields { |
|||
display: flex; |
|||
gap: 12px; |
|||
min-width: 0; |
|||
flex: 1 1 50%; |
|||
} |
|||
|
|||
.tb-data-key-field { |
|||
flex: 1; |
|||
min-width: 0; |
|||
} |
|||
|
|||
.tb-remove-button { |
|||
width: 40px; |
|||
min-width: 40px; |
|||
} |
|||
|
|||
@media #{$mat-lt-lg} { |
|||
.tb-source-field { |
|||
flex-direction: column; |
|||
flex: 1 1 50%; |
|||
} |
|||
.tb-data-key-fields{ |
|||
flex-direction: column; |
|||
flex: 1 1 50%; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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, |
|||
DestroyRef, |
|||
EventEmitter, |
|||
forwardRef, |
|||
Input, |
|||
OnInit, |
|||
Output, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { |
|||
ApiUsageDataKeysSettings, |
|||
ApiUsageSettingsContext |
|||
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; |
|||
import { Observable, of } from "rxjs"; |
|||
|
|||
@Component({ |
|||
selector: 'tb-api-usage-data-key-row', |
|||
templateUrl: './api-usage-data-key-row.component.html', |
|||
styleUrls: ['./api-usage-data-key-row.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ApiUsageDataKeyRowComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class ApiUsageDataKeyRowComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
DatasourceType = DatasourceType; |
|||
DataKeyType = DataKeyType; |
|||
|
|||
widgetType = widgetType; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
dsEntityAliasId: string; |
|||
|
|||
@Input() |
|||
context: ApiUsageSettingsContext; |
|||
|
|||
@Output() |
|||
dataKeyRemoved = new EventEmitter(); |
|||
|
|||
dataKeyFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: ApiUsageDataKeysSettings; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private cd: ChangeDetectorRef, |
|||
private destroyRef: DestroyRef) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.dataKeyFormGroup = this.fb.group({ |
|||
label: [null, [Validators.required]], |
|||
state: [null, []], |
|||
status: [null, [Validators.required]], |
|||
maxLimit: [null, [Validators.required]], |
|||
current: [null, [Validators.required]] |
|||
}); |
|||
this.dataKeyFormGroup.valueChanges.pipe( |
|||
takeUntilDestroyed(this.destroyRef) |
|||
).subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.dataKeyFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.dataKeyFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: ApiUsageDataKeysSettings): void { |
|||
this.modelValue = value; |
|||
this.dataKeyFormGroup.patchValue( |
|||
{ |
|||
label: value?.label, |
|||
state: value?.state, |
|||
status: value?.status, |
|||
maxLimit: value?.maxLimit, |
|||
current: value?.current |
|||
}, {emitEvent: false} |
|||
); |
|||
this.updateValidators(); |
|||
this.cd.markForCheck(); |
|||
} |
|||
|
|||
editKey(keyType: 'status' | 'maxLimit' | 'current') { |
|||
const targetDataKey: DataKey = this.dataKeyFormGroup.get(keyType).value; |
|||
this.context.editKey(targetDataKey, this.dsEntityAliasId).subscribe( |
|||
(updatedDataKey) => { |
|||
if (updatedDataKey) { |
|||
this.dataKeyFormGroup.get(keyType).patchValue(updatedDataKey); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
private updateValidators() { |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
fetchDashboardStates(searchText?: string): Observable<Array<string>> { |
|||
return of(this.context.callbacks.fetchDashboardStates(searchText)); |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { IAliasController } from '@core/api/widget-api.models'; |
|||
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; |
|||
import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; |
|||
import { Observable } from "rxjs"; |
|||
import { BackgroundSettings, BackgroundType } from "@shared/models/widget-settings.models"; |
|||
import { DataKeyType } from "@shared/models/telemetry/telemetry.models"; |
|||
import { materialColors } from "@shared/models/material.models"; |
|||
|
|||
export interface ApiUsageSettingsContext { |
|||
aliasController: IAliasController; |
|||
callbacks: WidgetConfigCallbacks; |
|||
widget: Widget; |
|||
editKey: (key: DataKey, entityAliasId: string, WidgetType?: widgetType) => Observable<DataKey>; |
|||
generateDataKey: (key: DataKey) => DataKey; |
|||
} |
|||
|
|||
|
|||
export interface ApiUsageWidgetSettings { |
|||
dsEntityAliasId: string; |
|||
dataKeys: ApiUsageDataKeysSettings[]; |
|||
targetDashboardState: string; |
|||
background: BackgroundSettings; |
|||
padding: string; |
|||
} |
|||
|
|||
export interface ApiUsageDataKeysSettings { |
|||
label: string; |
|||
state: string; |
|||
status: DataKey; |
|||
maxLimit: DataKey; |
|||
current: DataKey; |
|||
} |
|||
|
|||
const generateDataKey = (label: string, status: string, maxLimit: string, current: string) => { |
|||
return { |
|||
label, |
|||
state: '', |
|||
status: { |
|||
name: status, |
|||
label: status, |
|||
type: DataKeyType.timeseries, |
|||
funcBody: undefined, |
|||
settings: {}, |
|||
color: materialColors[0].value |
|||
}, |
|||
maxLimit: { |
|||
name: maxLimit, |
|||
label: maxLimit, |
|||
type: DataKeyType.timeseries, |
|||
funcBody: undefined, |
|||
settings: {}, |
|||
color: materialColors[0].value |
|||
}, |
|||
current: { |
|||
name: current, |
|||
label: current, |
|||
type: DataKeyType.timeseries, |
|||
funcBody: undefined, |
|||
settings: {}, |
|||
color: materialColors[0].value |
|||
} |
|||
} |
|||
} |
|||
|
|||
export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { |
|||
dsEntityAliasId: '', |
|||
dataKeys: [ |
|||
generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'), |
|||
generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'), |
|||
generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'), |
|||
generateDataKey('{i18n:api-usage.javascript-function-executions}', 'jsExecutionApiState', 'jsExecutionLimit', 'jsExecutionCount'), |
|||
generateDataKey('{i18n:api-usage.tbel-function-executions}', 'tbelExecutionApiState', 'tbelExecutionLimit', 'tbelExecutionCount'), |
|||
generateDataKey('{i18n:api-usage.data-points-storage-days}', 'dbApiState', 'storageDataPointsLimit', 'storageDataPointsCount'), |
|||
generateDataKey('{i18n:api-usage.alarms-created}', 'alarmApiState', 'createdAlarmsLimit', 'createdAlarmsCount'), |
|||
generateDataKey('{i18n:api-usage.emails}', 'emailApiState', 'emailLimit', 'emailCount'), |
|||
generateDataKey('{i18n:api-usage.sms}', 'notificationApiState', 'smsLimit', 'smsCount'), |
|||
], |
|||
targetDashboardState: 'default', |
|||
background: { |
|||
type: BackgroundType.color, |
|||
color: '#fff', |
|||
overlay: { |
|||
enabled: false, |
|||
color: 'rgba(255,255,255,0.72)', |
|||
blur: 3 |
|||
} |
|||
}, |
|||
padding: '0' |
|||
}; |
|||
|
|||
@ -0,0 +1,99 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="apiUsageWidgetSettingsForm"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.datasource</div> |
|||
<tb-entity-alias-select |
|||
class="tb-entity-alias-field" |
|||
tbRequired |
|||
[appearance]="'outline'" |
|||
[aliasController]="context.aliasController" |
|||
formControlName="dsEntityAliasId" |
|||
[callbacks]="context.callbacks"> |
|||
</tb-entity-alias-select> |
|||
|
|||
<div class="tb-form-panel no-padding no-border"> |
|||
<div class="tb-form-table tb-map-data-layers"> |
|||
<div class="tb-form-table-header no-padding-right"> |
|||
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.label</div> |
|||
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.state-name</div> |
|||
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.status</div> |
|||
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.limit</div> |
|||
<div class="tb-form-table-header-cell tb-key-header" translate>widgets.api-usage.current-number</div> |
|||
<div class="tb-form-table-header-cell tb-actions-header"></div> |
|||
</div> |
|||
<div cdkDropList cdkDropListOrientation="vertical" |
|||
[cdkDropListDisabled]="!dragEnabled" |
|||
(cdkDropListDropped)="layerDrop($event)" *ngIf="dataKeysFormArray().controls?.length; else noDataLayers" class="tb-form-table-body"> |
|||
<div cdkDrag [cdkDragDisabled]="!dragEnabled" |
|||
*ngFor="let dataKeyControl of dataKeysFormArray().controls; trackBy: trackByDataKey; let $index = index;"> |
|||
<div class="tb-draggable-form-table-row"> |
|||
<tb-api-usage-data-key-row class="flex-1" |
|||
[context]="context" |
|||
[dsEntityAliasId]="apiUsageWidgetSettingsForm.get('dsEntityAliasId').value" |
|||
[formControl]="dataKeyControl" |
|||
(dataKeyRemoved)="removeDataKey($index)"> |
|||
</tb-api-usage-data-key-row> |
|||
<div class="tb-form-table-row-cell-buttons"> |
|||
<button [class.tb-hidden]="!dragEnabled" |
|||
mat-icon-button |
|||
type="button" |
|||
cdkDragHandle |
|||
matTooltip="{{ 'action.drag' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>drag_indicator</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<button type="button" mat-stroked-button color="primary" (click)="addDataKey()"> |
|||
{{ 'widgets.api-usage.add-key' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #noDataLayers> |
|||
<span class="tb-prompt flex items-center justify-center">{{ 'widgets.api-usage.no-key' | translate }}</span> |
|||
</ng-template> |
|||
<tb-string-autocomplete [fetchOptionsFn]="fetchDashboardStates.bind(this)" |
|||
additionalClass="tb-suffix-show-on-hover" |
|||
appearance="outline" |
|||
panelWidth="" |
|||
label="{{ 'widgets.api-usage.target-dashboard-state' | translate }}" |
|||
style="flex: 1" |
|||
formControlName="targetDashboardState"> |
|||
</tb-string-autocomplete> |
|||
</div> |
|||
|
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widgets.background.background' | translate }}</div> |
|||
<tb-background-settings formControlName="background"> |
|||
</tb-background-settings> |
|||
</div> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'widget-config.card-padding' | translate }}</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
@ -0,0 +1,62 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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'; |
|||
|
|||
.tb-map-data-layers { |
|||
.tb-form-table-header-cell { |
|||
&.tb-source-header { |
|||
flex: 1 1 50%; |
|||
} |
|||
&.tb-x-pos-header { |
|||
flex: 1 1 25%; |
|||
} |
|||
&.tb-y-pos-header { |
|||
flex: 1 1 25%; |
|||
} |
|||
&.tb-key-header { |
|||
flex: 1 1 50%; |
|||
} |
|||
&.tb-actions-header { |
|||
width: 80px; |
|||
min-width: 80px; |
|||
} |
|||
@media #{$mat-lt-lg} { |
|||
&.tb-source-header { |
|||
flex: 1 1 30%; |
|||
} |
|||
&.tb-x-pos-header, &.tb-y-pos-header { |
|||
flex: 1 1 35%; |
|||
} |
|||
&.tb-key-header { |
|||
flex: 1 1 70%; |
|||
} |
|||
} |
|||
@media #{$mat-xs} { |
|||
&.tb-x-pos-header, &.tb-y-pos-header { |
|||
display: none; |
|||
} |
|||
&.tb-key-header { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-form-table-body { |
|||
tb-api-usage-data-key-row { |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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, forwardRef } from '@angular/core'; |
|||
import { |
|||
DataKey, |
|||
DataKeyConfigMode, |
|||
WidgetSettings, |
|||
WidgetSettingsComponent, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { |
|||
AbstractControl, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
ValidatorFn |
|||
} from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
ApiUsageDataKeysSettings, |
|||
apiUsageDefaultSettings, |
|||
ApiUsageSettingsContext |
|||
} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; |
|||
import { deepClone } from "@core/utils"; |
|||
import { Observable, of } from "rxjs"; |
|||
import { |
|||
DataKeyConfigDialogComponent, |
|||
DataKeyConfigDialogData |
|||
} from "@home/components/widget/lib/settings/common/key/data-key-config-dialog.component"; |
|||
import { MatDialog } from "@angular/material/dialog"; |
|||
import { CdkDragDrop } from "@angular/cdk/drag-drop"; |
|||
|
|||
@Component({ |
|||
selector: 'tb-api-usage-widget-settings', |
|||
templateUrl: './api-usage-widget-settings.component.html', |
|||
styleUrls: ['./../widget-settings.scss', 'api-usage-widget-settings.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ApiUsageWidgetSettingsComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
apiUsageWidgetSettingsForm: UntypedFormGroup; |
|||
|
|||
context: ApiUsageSettingsContext; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private dialog: MatDialog, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.context = { |
|||
aliasController: this.aliasController, |
|||
callbacks: this.callbacks, |
|||
widget: this.widget, |
|||
editKey: this.editKey.bind(this), |
|||
generateDataKey: this.generateDataKey.bind(this) |
|||
}; |
|||
} |
|||
|
|||
protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { |
|||
settingsForm.setControl('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false}); |
|||
} |
|||
|
|||
dataKeysFormArray(): UntypedFormArray { |
|||
return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; |
|||
} |
|||
|
|||
trackByDataKey(index: number): any { |
|||
return index; |
|||
} |
|||
|
|||
get dragEnabled(): boolean { |
|||
return this.dataKeysFormArray().controls.length > 1; |
|||
} |
|||
|
|||
layerDrop(event: CdkDragDrop<string[]>) { |
|||
const layer = this.dataKeysFormArray().at(event.previousIndex); |
|||
this.dataKeysFormArray().removeAt(event.previousIndex); |
|||
this.dataKeysFormArray().insert(event.currentIndex, layer); |
|||
} |
|||
|
|||
removeDataKey(index: number) { |
|||
(this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index); |
|||
} |
|||
|
|||
addDataKey() { |
|||
const dataKey = { |
|||
label: '', |
|||
state: '', |
|||
status: null, |
|||
maxLimit: null, |
|||
current: null |
|||
}; |
|||
const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; |
|||
const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]); |
|||
dataKeysArray.push(dataKeyControl); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.apiUsageWidgetSettingsForm; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return apiUsageDefaultSettings; |
|||
} |
|||
|
|||
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { |
|||
return { |
|||
dsEntityAliasId: settings?.dsEntityAliasId, |
|||
dataKeys: settings?.dataKeys, |
|||
targetDashboardState: settings?.targetDashboardState, |
|||
background: settings?.background, |
|||
padding: settings.padding |
|||
}; |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.apiUsageWidgetSettingsForm = this.fb.group({ |
|||
dsEntityAliasId: [settings?.dsEntityAliasId], |
|||
dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys), |
|||
targetDashboardState: [settings?.targetDashboardState], |
|||
background: [settings?.background, []], |
|||
padding: [settings.padding, []] |
|||
}); |
|||
} |
|||
|
|||
private prepareDataKeysFormArray(dataKeys: ApiUsageDataKeysSettings[]): UntypedFormArray { |
|||
const dataKeysControls: Array<AbstractControl> = []; |
|||
if (dataKeys) { |
|||
dataKeys.forEach((dataLayer) => { |
|||
dataKeysControls.push(this.fb.control(dataLayer, [this.apiUsageDataKeyValidator()])); |
|||
}); |
|||
} |
|||
return this.fb.array(dataKeysControls); |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return []; |
|||
} |
|||
|
|||
protected updateValidators() { |
|||
} |
|||
|
|||
apiUsageDataKeyValidator = (): ValidatorFn => { |
|||
return (control: AbstractControl): ValidationErrors | null => { |
|||
const value: ApiUsageDataKeysSettings = control.value; |
|||
if (!value?.label || !value?.current || !value?.maxLimit || !value?.status) { |
|||
return { |
|||
dataKey: true |
|||
} |
|||
} |
|||
return null; |
|||
}; |
|||
}; |
|||
|
|||
private editKey(key: DataKey, entityAliasId: string, _widgetType = widgetType.latest): Observable<DataKey> { |
|||
return this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
dataKey: deepClone(key), |
|||
dataKeyConfigMode: DataKeyConfigMode.general, |
|||
aliasController: this.aliasController, |
|||
widgetType: _widgetType, |
|||
entityAliasId, |
|||
showPostProcessing: true, |
|||
callbacks: this.callbacks, |
|||
hideDataKeyColor: true, |
|||
hideDataKeyDecimals: true, |
|||
hideDataKeyUnits: true, |
|||
widget: this.widget, |
|||
dashboard: null, |
|||
dataKeySettingsForm: null, |
|||
dataKeySettingsDirective: null |
|||
} |
|||
}).afterClosed(); |
|||
} |
|||
|
|||
private generateDataKey(key: DataKey): DataKey { |
|||
return this.callbacks.generateDataKey(key.name, key.type, null, false, null); |
|||
} |
|||
|
|||
fetchDashboardStates(searchText?: string): Observable<Array<string>> { |
|||
return of(this.callbacks.fetchDashboardStates(searchText)); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue