UI: add api usage widget and update dashboard
This commit is contained in:
parent
7ee8f11a65
commit
7b9f6c76cf
@ -24,6 +24,7 @@
|
||||
"cards.html_value_card",
|
||||
"cards.markdown_card",
|
||||
"cards.simple_card",
|
||||
"unread_notifications"
|
||||
"unread_notifications",
|
||||
"api_usage"
|
||||
]
|
||||
}
|
||||
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,124 @@
|
||||
/**
|
||||
* 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;
|
||||
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,178 @@
|
||||
///
|
||||
/// 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) => {
|
||||
// @ts-ignore
|
||||
this.ctx.dashboardWidget.updateCustomHeaderActions();
|
||||
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,82 @@
|
||||
<!--
|
||||
|
||||
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>
|
||||
<mat-form-field
|
||||
class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic">
|
||||
<input matInput formControlName="state" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<tb-data-key-input
|
||||
class="tb-data-key-field"
|
||||
required
|
||||
requiredText="{{ 'widgets.maps.data-layer.marker.latitude-key-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.maps.data-layer.marker.latitude-key-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.maps.data-layer.marker.latitude-key-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 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,58 @@
|
||||
/**
|
||||
* 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 50%;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.tb-label-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-data-key-field {
|
||||
flex: 1 1 25%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tb-remove-button {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
@media #{$mat-lt-lg} {
|
||||
.tb-source-field {
|
||||
flex-direction: column;
|
||||
flex: 1 1 30%;
|
||||
}
|
||||
.tb-data-key-field{
|
||||
flex: 1 1 35%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 450px) and (max-width: 599px) {
|
||||
.tb-source-field {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
@media #{$mat-xs} {
|
||||
.tb-data-key-field {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
///
|
||||
/// 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";
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@ -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,96 @@
|
||||
<!--
|
||||
|
||||
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>
|
||||
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label translate>widgets.api-usage.target-dashboard-state</mat-label>
|
||||
<input matInput formControlName="targetDashboardState" placeholder="{{ 'widget-config.set' | translate }}">
|
||||
</mat-form-field>
|
||||
</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,193 @@
|
||||
///
|
||||
/// 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 } 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)
|
||||
};
|
||||
}
|
||||
|
||||
dataKeysFormArray(): UntypedFormArray {
|
||||
return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray;
|
||||
}
|
||||
|
||||
trackByDataKey(index: number, dataKeyControl: AbstractControl): any {
|
||||
return dataKeyControl;
|
||||
}
|
||||
|
||||
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.mapDataKeyValidator()]);
|
||||
dataKeysArray.push(dataKeyControl);
|
||||
}
|
||||
|
||||
protected settingsForm(): UntypedFormGroup {
|
||||
return this.apiUsageWidgetSettingsForm;
|
||||
}
|
||||
|
||||
protected defaultSettings(): WidgetSettings {
|
||||
return apiUsageDefaultSettings;
|
||||
}
|
||||
|
||||
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.mapDataKeyValidator()]));
|
||||
});
|
||||
}
|
||||
return this.fb.array(dataKeysControls);
|
||||
}
|
||||
|
||||
protected validatorTriggers(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected updateValidators() {
|
||||
}
|
||||
|
||||
mapDataKeyValidator = (): 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);
|
||||
}
|
||||
}
|
||||
@ -375,6 +375,12 @@ import {
|
||||
ValueStepperWidgetSettingsComponent
|
||||
} from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component';
|
||||
import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component';
|
||||
import {
|
||||
ApiUsageWidgetSettingsComponent
|
||||
} from "@home/components/widget/lib/settings/cards/api-usage-widget-settings.component";
|
||||
import {
|
||||
ApiUsageDataKeyRowComponent
|
||||
} from "@home/components/widget/lib/settings/cards/api-usage-data-key-row.component";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -508,7 +514,9 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings
|
||||
LabelValueCardWidgetSettingsComponent,
|
||||
UnreadNotificationWidgetSettingsComponent,
|
||||
ScadaSymbolWidgetSettingsComponent,
|
||||
MapWidgetSettingsComponent
|
||||
MapWidgetSettingsComponent,
|
||||
ApiUsageWidgetSettingsComponent,
|
||||
ApiUsageDataKeyRowComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -647,7 +655,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings
|
||||
LabelValueCardWidgetSettingsComponent,
|
||||
UnreadNotificationWidgetSettingsComponent,
|
||||
ScadaSymbolWidgetSettingsComponent,
|
||||
MapWidgetSettingsComponent
|
||||
MapWidgetSettingsComponent,
|
||||
ApiUsageWidgetSettingsComponent
|
||||
]
|
||||
})
|
||||
export class WidgetSettingsModule {
|
||||
|
||||
@ -94,6 +94,7 @@ import {
|
||||
SelectMapEntityPanelComponent
|
||||
} from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component';
|
||||
import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component';
|
||||
import { ApiUsageWidgetComponent } from "@home/components/widget/lib/cards/api-usage-widget.component";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -151,7 +152,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane
|
||||
ScadaSymbolWidgetComponent,
|
||||
SelectMapEntityPanelComponent,
|
||||
MapTimelinePanelComponent,
|
||||
MapWidgetComponent
|
||||
MapWidgetComponent,
|
||||
ApiUsageWidgetComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -214,7 +216,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane
|
||||
UnreadNotificationWidgetComponent,
|
||||
NotificationTypeFilterPanelComponent,
|
||||
ScadaSymbolWidgetComponent,
|
||||
MapWidgetComponent
|
||||
MapWidgetComponent,
|
||||
ApiUsageWidgetComponent
|
||||
],
|
||||
providers: [
|
||||
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -865,15 +865,18 @@
|
||||
"api-features": "API features",
|
||||
"api-usage": "API usage",
|
||||
"alarm": "Alarm",
|
||||
"alarms-created": "Alarms created",
|
||||
"alarms-created": "Created alarms",
|
||||
"queue-stats": "Queue Stats",
|
||||
"processing-failures-and-timeouts": "Processing Failures and Timeouts",
|
||||
"exceptions": "Exceptions",
|
||||
"alarms-created-daily-activity": "Alarms created daily activity",
|
||||
"alarms-created-hourly-activity": "Alarms created hourly activity",
|
||||
"alarms-created-monthly-activity": "Alarms created monthly activity",
|
||||
"alarms-created-daily-activity": "Created alarms daily activity",
|
||||
"alarms-created-hourly-activity": "Created alarms hourly activity",
|
||||
"alarms-created-monthly-activity": "Created alarms monthly activity",
|
||||
"data-points": "Data points",
|
||||
"data-points-storage-days": "Data points storage days",
|
||||
"data-points-storage-days-hourly-activity": "Data points storage days hourly activity",
|
||||
"data-points-storage-days-daily-activity": "Data points storage days daily activity",
|
||||
"data-points-storage-days-monthly-activity": "Data points storage days monthly activity",
|
||||
"device-api": "Device API",
|
||||
"email": "Email",
|
||||
"email-messages": "Email messages",
|
||||
@ -899,14 +902,15 @@
|
||||
"processing-timeouts": "${entityName} Processing Timeouts",
|
||||
"rule-chain": "Rule Chain",
|
||||
"rule-engine": "Rule Engine",
|
||||
"rule-engine-daily-activity": "Rule Engine daily activity",
|
||||
"rule-engine-executions": "Rule Engine executions",
|
||||
"rule-engine-hourly-activity": "Rule Engine hourly activity",
|
||||
"rule-engine-daily-activity": "Rule Engine daily activity",
|
||||
"rule-engine-monthly-activity": "Rule Engine monthly activity",
|
||||
"rule-engine-statistics": "Rule Engine Statistics",
|
||||
"rule-node": "Rule Node",
|
||||
"sms": "SMS",
|
||||
"sms-messages": "SMS messages",
|
||||
"sms-messages-hourly-activity": "SMS messages hourly activity",
|
||||
"sms-messages-daily-activity": "SMS messages daily activity",
|
||||
"sms-messages-monthly-activity": "SMS messages monthly activity",
|
||||
"successful": "${entityName} Successful",
|
||||
@ -916,13 +920,40 @@
|
||||
"telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity",
|
||||
"telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity",
|
||||
"transport": "Transport",
|
||||
"transport-msg-hourly-activity": "Transport messages hourly activity",
|
||||
"transport-msg-daily-activity": "Transport messages daily activity",
|
||||
"transport-msg-monthly-activity": "Transport messages monthly activity",
|
||||
"transport-daily-activity": "Transport daily activity",
|
||||
"transport-data-points": "Transport data points",
|
||||
"transport-hourly-activity": "Transport hourly activity",
|
||||
"transport-messages": "Transport messages",
|
||||
"transport-monthly-activity": "Transport monthly activity",
|
||||
"transport-data-points-hourly-activity": "Transport data points hourly activity",
|
||||
"transport-data-points-daily-activity": "Transport data points daily activity",
|
||||
"transport-data-points-monthly-activity": "Transport data points monthly activity",
|
||||
"view-details": "View details",
|
||||
"view-statistics": "View statistics"
|
||||
"view-statistics": "View statistics",
|
||||
"transport-messages": "Transport messages",
|
||||
"transport-messages-hourly-activity": "Transport messages hourly activity",
|
||||
"transport-data-point-hourly-activity": "Transport data point hourly activity",
|
||||
"javascript-function-executions": "JavaScript function executions",
|
||||
"javascript-function-executions-hourly-activity": "JavaScript function executions hourly activity",
|
||||
"javascript-function-executions-daily-activity": "JavaScript function executions daily activity",
|
||||
"javascript-function-executions-monthly-activity": "JavaScript function executions monthly activity",
|
||||
"tbel-function-executions": "TBEL function executions",
|
||||
"tbel-function-executions-hourly-activity": "TBEL function executions hourly activity",
|
||||
"tbel-function-executions-daily-activity": "TBEL function executions daily activity",
|
||||
"tbel-function-executions-monthly-activity": "TBEL function executions monthly activity",
|
||||
"created-reports": "Created reports",
|
||||
"created-reports-hourly-activity": "Created reports hourly activity",
|
||||
"created-reports-daily-activity": "Created reports daily activity",
|
||||
"created-reports-monthly-activity": "Created reports monthly activity",
|
||||
"emails": "Emails",
|
||||
"emails-hourly-activity": "Emails hourly activity",
|
||||
"emails-daily-activity": "Emails daily activity",
|
||||
"emails-monthly-activity": "Emails monthly activity",
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"warning": "Warning"
|
||||
}
|
||||
},
|
||||
"api-limit": {
|
||||
"cassandra-write-queries-core": "Rest API Cassandra write queries",
|
||||
@ -9483,6 +9514,18 @@
|
||||
"how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-usage": {
|
||||
"api-usage": "API usage",
|
||||
"label": "Label",
|
||||
"state-name": "State name",
|
||||
"status": "Status",
|
||||
"limit": "Max limit",
|
||||
"current-number": "Current number",
|
||||
"add-key": "Add key",
|
||||
"no-key": "No key",
|
||||
"delete-key": "Delete key",
|
||||
"target-dashboard-state": "Target dashboard state"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user