101 changed files with 4030 additions and 1417 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,26 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
.widget-preview-section { |
|||
position: absolute; |
|||
top: 72px; |
|||
left: 16px; |
|||
right: 16px; |
|||
bottom: 16px; |
|||
border: 1px solid rgba(0, 0, 0, 0.05); |
|||
border-radius: 4px; |
|||
} |
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<form [formGroup]="actionsSettings" style="width: 900px;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content style="position: relative; height: 500px;"> |
|||
<tb-manage-widget-actions |
|||
[callbacks]="data.callbacks" |
|||
[widgetType] = "data.widgetType" |
|||
[actionSources]="actionSources" |
|||
formControlName="actions"> |
|||
</tb-manage-widget-actions> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
(click)="save()" |
|||
[disabled]="(isLoading$ | async) || actionsSettings.invalid || !actionsSettings.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,71 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { widgetType } from '@shared/models/widget.models'; |
|||
import { |
|||
WidgetActionCallbacks, |
|||
WidgetActionsData |
|||
} from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { Component, Inject, OnInit } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
|
|||
export interface ManageWidgetActionsDialogData { |
|||
widgetTitle: string; |
|||
actionsData: WidgetActionsData; |
|||
callbacks: WidgetActionCallbacks; |
|||
widgetType: widgetType; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-manage-widget-actions-dialog', |
|||
templateUrl: './manage-widget-actions-dialog.component.html', |
|||
providers: [], |
|||
styleUrls: [] |
|||
}) |
|||
export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWidgetActionsDialogComponent, |
|||
WidgetActionsData> implements OnInit { |
|||
|
|||
actionSources = this.data.actionsData.actionSources; |
|||
actionsSettings: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: ManageWidgetActionsDialogData, |
|||
private fb: UntypedFormBuilder, |
|||
public dialogRef: MatDialogRef<ManageWidgetActionsDialogComponent, WidgetActionsData>) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.actionsSettings = this.fb.group({ |
|||
actions: [this.data.actionsData.actionsMap, []] |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.dialogRef.close(this.actionsSettings.get('actions').value); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { NgModule, Type } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { IBasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
|||
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; |
|||
import { |
|||
SimpleCardBasicConfigComponent |
|||
} from '@home/components/widget/config/basic/cards/simple-card-basic-config.component'; |
|||
import { |
|||
WidgetActionsPanelComponent |
|||
} from '@home/components/widget/config/basic/common/widget-actions-panel.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
WidgetActionsPanelComponent, |
|||
SimpleCardBasicConfigComponent |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
WidgetConfigComponentsModule |
|||
], |
|||
exports: [ |
|||
WidgetActionsPanelComponent, |
|||
SimpleCardBasicConfigComponent |
|||
] |
|||
}) |
|||
export class BasicWidgetConfigModule { |
|||
} |
|||
|
|||
export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = { |
|||
'tb-simple-card-basic-config': SimpleCardBasicConfigComponent |
|||
}; |
|||
@ -0,0 +1,86 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<ng-container [formGroup]="simpleCardWidgetConfigForm"> |
|||
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig" |
|||
[onlyHistoryTimewindow]="onlyHistoryTimewindow()" |
|||
formControlName="timewindowConfig"> |
|||
</tb-timewindow-config-panel> |
|||
<tb-datasources |
|||
[configMode]="basicMode" |
|||
hideDataKeyLabel |
|||
hideDataKeyColor |
|||
hideDataKeyUnits |
|||
hideDataKeyDecimals |
|||
formControlName="datasources"> |
|||
</tb-datasources> |
|||
<div class="tb-widget-config-panel"> |
|||
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div> |
|||
<div class="tb-widget-config-row"> |
|||
<div translate>widgets.simple-card.label</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-widget-config-row space-between"> |
|||
<div translate>widgets.simple-card.label-position</div> |
|||
<mat-form-field appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-select formControlName="labelPosition"> |
|||
<mat-option [value]="'left'"> |
|||
{{ 'widgets.simple-card.label-position-left' | translate }} |
|||
</mat-option> |
|||
<mat-option [value]="'top'"> |
|||
{{ 'widgets.simple-card.label-position-top' | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-widget-config-row space-between"> |
|||
<div translate>widget-config.units-short</div> |
|||
<tb-widget-units |
|||
formControlName="units"> |
|||
</tb-widget-units> |
|||
</div> |
|||
<div class="tb-widget-config-row space-between"> |
|||
<div translate>widget-config.decimals-short</div> |
|||
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic"> |
|||
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-widget-config-row space-between same-padding"> |
|||
<div>{{ 'widget-config.text-color' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> |
|||
<mat-divider vertical></mat-divider> |
|||
<tb-color-input asBoxInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-widget-config-row space-between same-padding"> |
|||
<div>{{ 'widget-config.background' | translate }}</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> |
|||
<mat-divider vertical></mat-divider> |
|||
<tb-color-input asBoxInput |
|||
formControlName="backgroundColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tb-widget-actions-panel |
|||
formControlName="actions"> |
|||
</tb-widget-actions-panel> |
|||
</ng-container> |
|||
@ -0,0 +1,110 @@ |
|||
///
|
|||
/// 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 { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; |
|||
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
|||
import { |
|||
Datasource, |
|||
datasourcesHasAggregation, |
|||
datasourcesHasOnlyComparisonAggregation |
|||
} from '@shared/models/widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-simple-card-basic-config', |
|||
templateUrl: './simple-card-basic-config.component.html', |
|||
styleUrls: ['../basic-config.scss', '../../widget-config.scss'] |
|||
}) |
|||
export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { |
|||
|
|||
public get displayTimewindowConfig(): boolean { |
|||
const datasources = this.simpleCardWidgetConfigForm.get('datasources').value; |
|||
return datasourcesHasAggregation(datasources); |
|||
} |
|||
|
|||
public onlyHistoryTimewindow(): boolean { |
|||
const datasources = this.simpleCardWidgetConfigForm.get('datasources').value; |
|||
return datasourcesHasOnlyComparisonAggregation(datasources); |
|||
} |
|||
|
|||
simpleCardWidgetConfigForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected configForm(): UntypedFormGroup { |
|||
return this.simpleCardWidgetConfigForm; |
|||
} |
|||
|
|||
protected onConfigSet(configData: WidgetConfigComponentData) { |
|||
this.simpleCardWidgetConfigForm = this.fb.group({ |
|||
timewindowConfig: [{ |
|||
useDashboardTimewindow: configData.config.useDashboardTimewindow, |
|||
displayTimewindow: configData.config.useDashboardTimewindow, |
|||
timewindow: configData.config.timewindow |
|||
}, []], |
|||
datasources: [configData.config.datasources, []], |
|||
label: [this.getDataKeyLabel(configData.config.datasources), []], |
|||
labelPosition: [configData.config.settings?.labelPosition, []], |
|||
units: [configData.config.units, []], |
|||
decimals: [configData.config.decimals, []], |
|||
color: [configData.config.color, []], |
|||
backgroundColor: [configData.config.backgroundColor, []], |
|||
actions: [configData.config.actions || {}, []] |
|||
}); |
|||
} |
|||
|
|||
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
|||
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; |
|||
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; |
|||
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; |
|||
this.widgetConfig.config.datasources = config.datasources; |
|||
this.setDataKeyLabel(config.label, this.widgetConfig.config.datasources); |
|||
this.widgetConfig.config.actions = config.actions; |
|||
this.widgetConfig.config.units = config.units; |
|||
this.widgetConfig.config.decimals = config.decimals; |
|||
this.widgetConfig.config.color = config.color; |
|||
this.widgetConfig.config.backgroundColor = config.backgroundColor; |
|||
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; |
|||
this.widgetConfig.config.settings.labelPosition = config.labelPosition; |
|||
return this.widgetConfig; |
|||
} |
|||
|
|||
private getDataKeyLabel(datasources?: Datasource[]): string { |
|||
if (datasources && datasources.length) { |
|||
const dataKeys = datasources[0].dataKeys; |
|||
if (dataKeys && dataKeys.length) { |
|||
return dataKeys[0].label; |
|||
} |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
private setDataKeyLabel(label: string, datasources?: Datasource[]) { |
|||
if (datasources && datasources.length) { |
|||
const dataKeys = datasources[0].dataKeys; |
|||
if (dataKeys && dataKeys.length) { |
|||
dataKeys[0].label = label; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<!-- |
|||
|
|||
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-widget-config-panel" (click)="manageWidgetActions()" style="cursor: pointer;"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> |
|||
<div class="tb-widget-config-panel-title" translate style="padding-right: 48px;">widget-config.actions</div> |
|||
<mat-chip-listbox fxFlex> |
|||
<ng-container *ngFor="let actionSourceId of widgetActionSourceIds"> |
|||
<mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)"> |
|||
<mat-icon matChipAvatar>{{ widgetAction.icon }}</mat-icon> |
|||
{{ widgetAction.name }} |
|||
</mat-chip> |
|||
</ng-container> |
|||
</mat-chip-listbox> |
|||
<button mat-stroked-button color="primary" *ngIf="!hasWidgetActions"> |
|||
{{ 'widget-config.add-action' | translate }} |
|||
</button> |
|||
<button mat-icon-button color="primary" *ngIf="hasWidgetActions"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,133 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { WidgetActionsData } from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { WidgetActionDescriptor } from '@shared/models/widget.models'; |
|||
import { |
|||
ManageWidgetActionsDialogComponent, |
|||
ManageWidgetActionsDialogData |
|||
} from '@home/components/widget/action/manage-widget-actions-dialog.component'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-actions-panel', |
|||
templateUrl: './widget-actions-panel.component.html', |
|||
styleUrls: ['../../widget-config.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => WidgetActionsPanelComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
actionsFormGroup: UntypedFormGroup; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private dialog: MatDialog, |
|||
private cd: ChangeDetectorRef, |
|||
private widgetConfigComponent: WidgetConfigComponent) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.actionsFormGroup = this.fb.group({ |
|||
actions: [null, []] |
|||
}); |
|||
this.actionsFormGroup.get('actions').valueChanges.subscribe( |
|||
(val) => this.propagateChange(val) |
|||
); |
|||
} |
|||
|
|||
writeValue(actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}): void { |
|||
this.actionsFormGroup.get('actions').patchValue(actions || {}, {emitEvent: false}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.actionsFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.actionsFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
public get widgetActionSourceIds(): Array<string> { |
|||
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value; |
|||
return actions ? Object.keys(actions) : []; |
|||
} |
|||
|
|||
public widgetActionsByActionSourceId(actionSourceId: string): Array<WidgetActionDescriptor> { |
|||
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value; |
|||
return actions[actionSourceId] || []; |
|||
} |
|||
|
|||
public get hasWidgetActions(): boolean { |
|||
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value; |
|||
if (actions) { |
|||
for (const actionSourceId of Object.keys(actions)) { |
|||
if (actions[actionSourceId] && actions[actionSourceId].length) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
public manageWidgetActions() { |
|||
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value; |
|||
const actionsData: WidgetActionsData = { |
|||
actionsMap: deepClone(actions), |
|||
actionSources: this.widgetConfigComponent.modelValue.actionSources || {} |
|||
}; |
|||
this.dialog.open<ManageWidgetActionsDialogComponent, ManageWidgetActionsDialogData, |
|||
{[actionSourceId: string]: Array<WidgetActionDescriptor>}>(ManageWidgetActionsDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
widgetTitle: this.widgetConfigComponent.modelValue.widgetName, |
|||
callbacks: this.widgetConfigComponent.widgetConfigCallbacks, |
|||
actionsData, |
|||
widgetType: this.widgetConfigComponent.widgetType |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
this.actionsFormGroup.get('actions').patchValue(res); |
|||
this.cd.markForCheck(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
<!-- |
|||
|
|||
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-datasource-section" [formGroup]="datasourceFormGroup"> |
|||
<mat-form-field *ngIf="!basicMode" class="tb-datasource-type" hideRequiredMarker> |
|||
<mat-label translate>widget-config.datasource-type</mat-label> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> |
|||
{{ datasourceTypesTranslations.get(datasourceType) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<section fxLayout="column" [ngSwitch]="datasourceFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="datasourceType.function"> |
|||
<mat-form-field fxFlex> |
|||
<mat-label translate>datasource.label</mat-label> |
|||
<input matInput |
|||
formControlName="name"> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="datasourceFormGroup.get('type').value === datasourceType.device || |
|||
datasourceFormGroup.get('type').value === datasourceType.entity || |
|||
datasourceFormGroup.get('type').value === datasourceType.entityCount || |
|||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''"> |
|||
<tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount" |
|||
propagatedFilter="false" |
|||
style="height: 56px; margin-bottom: 22px;" |
|||
formControlName="alarmFilterConfig"></tb-alarm-filter-config> |
|||
<tb-entity-autocomplete *ngIf="datasourceFormGroup.get('type').value === datasourceType.device" |
|||
required |
|||
[entityType]="entityType.DEVICE" |
|||
formControlName="deviceId"> |
|||
</tb-entity-autocomplete> |
|||
<tb-entity-alias-select |
|||
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount" |
|||
[showLabel]="true" |
|||
[tbRequired]="true" |
|||
[aliasController]="aliasController" |
|||
formControlName="entityAliasId" |
|||
[callbacks]="entityAliasSelectCallbacks"> |
|||
</tb-entity-alias-select> |
|||
<mat-form-field *ngIf="[datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)" |
|||
fxFlex> |
|||
<input matInput |
|||
placeholder="{{ 'datasource.label' | translate }}" |
|||
formControlName="name"> |
|||
</mat-form-field> |
|||
</ng-template> |
|||
</section> |
|||
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex> |
|||
<tb-data-keys class="tb-data-keys" fxFlex |
|||
[widgetType]="widgetType" |
|||
[datasourceType]="datasourceFormGroup.get('type').value" |
|||
[maxDataKeys]="maxDataKeys" |
|||
[optDataKeys]="isDataKeysOptional(datasourceFormGroup.get('type').value)" |
|||
[simpleDataKeysLabel]="!hasAdditionalLatestDataKeys" |
|||
[aliasController]="aliasController" |
|||
[datakeySettingsSchema]="dataKeySettingsSchema" |
|||
[dataKeySettingsDirective]="dataKeySettingsDirective" |
|||
[dashboard]="dashboard" |
|||
[widget]="widget" |
|||
[callbacks]="dataKeysCallbacks" |
|||
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value" |
|||
[deviceId]="datasourceFormGroup.get('deviceId').value" |
|||
formControlName="dataKeys"> |
|||
</tb-data-keys> |
|||
<tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex |
|||
[widgetType]="widgetTypes.latest" |
|||
[datasourceType]="datasourceFormGroup.get('type').value" |
|||
[optDataKeys]="true" |
|||
[aliasController]="aliasController" |
|||
[datakeySettingsSchema]="latestDataKeySettingsSchema" |
|||
[dataKeySettingsDirective]="latestDataKeySettingsDirective" |
|||
[dashboard]="dashboard" |
|||
[widget]="widget" |
|||
[callbacks]="dataKeysCallbacks" |
|||
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value" |
|||
[deviceId]="datasourceFormGroup.get('deviceId').value" |
|||
formControlName="latestDataKeys"> |
|||
</tb-data-keys> |
|||
</section> |
|||
<tb-filter-select |
|||
*ngIf="!basicMode && ![datasourceType.function, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)" |
|||
[showLabel]="true" |
|||
[aliasController]="aliasController" |
|||
formControlName="filterId" |
|||
[callbacks]="filterSelectCallbacks"> |
|||
</tb-filter-select> |
|||
</section> |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
.tb-datasource-section { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: stretch; |
|||
flex: 1; |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-datasource-section { |
|||
tb-alarm-filter-config { |
|||
.mdc-button { |
|||
width: 100%; |
|||
height: 100%; |
|||
justify-content: flex-start; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,275 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnInit, Optional } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { |
|||
Datasource, |
|||
DatasourceType, |
|||
datasourceTypeTranslationMap, |
|||
JsonSettingsSchema, |
|||
Widget, WidgetConfigMode, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { AlarmSearchStatus } from '@shared/models/alarm.models'; |
|||
import { Dashboard } from '@shared/models/dashboard.models'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models'; |
|||
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-datasource', |
|||
templateUrl: './datasource.component.html', |
|||
styleUrls: ['./datasource.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => DatasourceComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => DatasourceComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator { |
|||
|
|||
public get basicMode(): boolean { |
|||
return !this.widgetConfigComponent.widgetEditMode && this.widgetConfigComponent.widgetConfigMode === WidgetConfigMode.basic; |
|||
} |
|||
|
|||
public get widgetType(): widgetType { |
|||
return this.widgetConfigComponent.widgetType; |
|||
} |
|||
|
|||
public get aliasController(): IAliasController { |
|||
return this.widgetConfigComponent.aliasController; |
|||
} |
|||
|
|||
public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks { |
|||
return this.widgetConfigComponent.widgetConfigCallbacks; |
|||
} |
|||
|
|||
public get filterSelectCallbacks(): FilterSelectCallbacks { |
|||
return this.widgetConfigComponent.widgetConfigCallbacks; |
|||
} |
|||
|
|||
public get dataKeysCallbacks(): DataKeysCallbacks { |
|||
return this.widgetConfigComponent.widgetConfigCallbacks; |
|||
} |
|||
|
|||
public get hasAdditionalLatestDataKeys(): boolean { |
|||
return this.widgetConfigComponent.widgetType === widgetType.timeseries && |
|||
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys; |
|||
} |
|||
|
|||
public get dataKeysOptional(): boolean { |
|||
return this.widgetConfigComponent.modelValue?.typeParameters?.dataKeysOptional; |
|||
} |
|||
|
|||
public get maxDataKeys(): number { |
|||
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys; |
|||
} |
|||
|
|||
public get dataKeySettingsSchema(): JsonSettingsSchema { |
|||
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; |
|||
} |
|||
|
|||
public get dataKeySettingsDirective(): string { |
|||
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; |
|||
} |
|||
|
|||
public get latestDataKeySettingsSchema(): JsonSettingsSchema { |
|||
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema; |
|||
} |
|||
|
|||
public get latestDataKeySettingsDirective(): string { |
|||
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective; |
|||
} |
|||
|
|||
public get dashboard(): Dashboard { |
|||
return this.widgetConfigComponent.dashboard; |
|||
} |
|||
|
|||
public get widget(): Widget { |
|||
return this.widgetConfigComponent.widget; |
|||
} |
|||
|
|||
public get hideDataKeyLabel(): boolean { |
|||
return this.datasourcesComponent?.hideDataKeyLabel; |
|||
} |
|||
|
|||
public get hideDataKeyColor(): boolean { |
|||
return this.datasourcesComponent?.hideDataKeyColor; |
|||
} |
|||
|
|||
public get hideDataKeyUnits(): boolean { |
|||
return this.datasourcesComponent?.hideDataKeyUnits; |
|||
} |
|||
|
|||
public get hideDataKeyDecimals(): boolean { |
|||
return this.datasourcesComponent?.hideDataKeyDecimals; |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
widgetTypes = widgetType; |
|||
|
|||
entityType = EntityType; |
|||
|
|||
datasourceType = DatasourceType; |
|||
datasourceTypes: Array<DatasourceType> = []; |
|||
datasourceTypesTranslations = datasourceTypeTranslationMap; |
|||
|
|||
datasourceFormGroup: UntypedFormGroup; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
@Optional() |
|||
private datasourcesComponent: DatasourcesComponent, |
|||
private widgetConfigComponent: WidgetConfigComponent) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
if (!this.datasourceFormGroup.valid) { |
|||
setTimeout(() => { |
|||
this.datasourceUpdated(this.datasourceFormGroup.value); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.datasourceFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.datasourceFormGroup.enable({emitEvent: false}); |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
ngOnInit() { |
|||
if (this.widgetConfigComponent.functionsOnly) { |
|||
this.datasourceTypes = [DatasourceType.function]; |
|||
} else { |
|||
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity]; |
|||
if (this.widgetConfigComponent.widgetType === widgetType.latest) { |
|||
this.datasourceTypes.push(DatasourceType.entityCount); |
|||
this.datasourceTypes.push(DatasourceType.alarmCount); |
|||
} |
|||
} |
|||
|
|||
this.datasourceFormGroup = this.fb.group( |
|||
{ |
|||
type: [null, [Validators.required]], |
|||
name: [null, []], |
|||
deviceId: [null, []], |
|||
entityAliasId: [null, []], |
|||
filterId: [null, []], |
|||
dataKeys: [null, []], |
|||
alarmFilterConfig: [null, []] |
|||
} |
|||
); |
|||
if (this.hasAdditionalLatestDataKeys) { |
|||
this.datasourceFormGroup.addControl('latestDataKeys', this.fb.control(null)); |
|||
} |
|||
this.datasourceFormGroup.get('type').valueChanges.subscribe(() => { |
|||
this.updateValidators(); |
|||
}); |
|||
this.datasourceFormGroup.valueChanges.subscribe( |
|||
() => { |
|||
this.datasourceUpdated(this.datasourceFormGroup.value); |
|||
} |
|||
); |
|||
} |
|||
|
|||
writeValue(datasource?: Datasource): void { |
|||
this.datasourceFormGroup.patchValue({ |
|||
type: datasource?.type, |
|||
name: datasource?.name, |
|||
deviceId: datasource?.deviceId, |
|||
entityAliasId: datasource?.entityAliasId, |
|||
filterId: datasource?.filterId, |
|||
dataKeys: datasource?.dataKeys, |
|||
alarmFilterConfig: datasource?.alarmFilterConfig ? |
|||
datasource?.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] } |
|||
}, {emitEvent: false}); |
|||
if (this.hasAdditionalLatestDataKeys) { |
|||
this.datasourceFormGroup.patchValue({ |
|||
latestDataKeys: datasource?.latestDataKeys |
|||
}, {emitEvent: false}); |
|||
} |
|||
this.updateValidators(); |
|||
} |
|||
|
|||
validate(c: UntypedFormControl) { |
|||
return (this.datasourceFormGroup.valid) ? null : { |
|||
datasource: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
public isDataKeysOptional(type?: DatasourceType): boolean { |
|||
if (this.hasAdditionalLatestDataKeys) { |
|||
return true; |
|||
} else { |
|||
return this.dataKeysOptional |
|||
&& type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount; |
|||
} |
|||
} |
|||
|
|||
private datasourceUpdated(datasource: Datasource) { |
|||
this.propagateChange(datasource); |
|||
} |
|||
|
|||
private updateValidators() { |
|||
const type: DatasourceType = this.datasourceFormGroup.get('type').value; |
|||
this.datasourceFormGroup.get('deviceId').setValidators( |
|||
type === DatasourceType.device ? [Validators.required] : [] |
|||
); |
|||
this.datasourceFormGroup.get('entityAliasId').setValidators( |
|||
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] |
|||
); |
|||
const newDataKeysRequired = !this.isDataKeysOptional(type); |
|||
this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []); |
|||
this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false}); |
|||
this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false}); |
|||
this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
<!-- |
|||
|
|||
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 [formGroup]="datasourcesFormGroup" class="tb-widget-config-panel tb-datasources" [ngClass]="{'no-padding-bottom': !showAddDatasource}"> |
|||
<div fxLayout="column"> |
|||
<div fxLayout="row" fxLayoutAlign="space-between center"> |
|||
<div class="tb-widget-config-panel-title">{{ (singleDatasource ? 'widget-config.datasource' : 'widget-config.datasources') | translate }}</div> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="center center" *ngIf="timeseriesKeyError"> |
|||
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error> |
|||
</div> |
|||
<tb-toggle-header *ngIf="basicMode" (valueChange)="datasourcesModeChange($event)" ignoreMdLgSize="true" |
|||
[options]="[ |
|||
{ |
|||
name: translate.instant('device.device'), |
|||
value: datasourceType.device |
|||
}, |
|||
{ |
|||
name: translate.instant('entity.entity-alias'), |
|||
value: datasourceType.entity |
|||
} |
|||
]" [value]="datasourcesMode" name="widgetConfigModeHeader" useSelectOnMdLg="false"> |
|||
</tb-toggle-header> |
|||
</div> |
|||
<div *ngIf="maxDatasources > 1" |
|||
class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div> |
|||
</div> |
|||
<div *ngIf="datasourcesFormArray.length === 0; else datasourcesTemplate"> |
|||
<span translate fxLayoutAlign="center center" |
|||
class="tb-prompt">datasource.add-datasource-prompt</span> |
|||
</div> |
|||
<ng-template #datasourcesTemplate> |
|||
<div style="overflow: auto;"> |
|||
<mat-list class="tb-drop-list" |
|||
cdkDropList |
|||
cdkDropListOrientation="vertical" |
|||
(cdkDropListDropped)="onDatasourceDrop($event)" |
|||
[cdkDropListDisabled]="dragDisabled" |
|||
formArrayName="datasources"> |
|||
<mat-list-item *ngFor="let datasourceControl of datasourcesControls; trackBy: trackByDatasource; let $index = index;" |
|||
class="tb-datasource-list-item tb-draggable" [ngClass]="{'bordered': !singleDatasource}" cdkDrag |
|||
[cdkDragDisabled]="dragDisabled"> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start start"> |
|||
<div *ngIf="!singleDatasource" fxLayout="row" fxLayoutAlign="center center" style="width: 42px; height: 56px;"> |
|||
<div class="tb-datasource-index">{{$index + 1}}</div> |
|||
</div> |
|||
<div class="tb-datasource-params" fxFlex> |
|||
<div fxFlex |
|||
fxLayout="row" |
|||
fxLayoutAlign="start"> |
|||
<tb-datasource fxFlex |
|||
[formControl]="datasourceControl"> |
|||
</tb-datasource> |
|||
<div *ngIf="!singleDatasource && !disabled" fxLayout="row" fxLayoutAlign="center center" style="height: 56px;"> |
|||
<button type="button" |
|||
mat-icon-button color="primary" |
|||
(click)="removeDatasource($index)" |
|||
matTooltip="{{ 'widget-config.remove-datasource' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
<button *ngIf="!dragDisabled" |
|||
mat-icon-button color="primary" |
|||
type="button" |
|||
cdkDragHandle |
|||
matTooltip="{{ 'action.drag' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>drag_indicator</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<tb-error class="tb-datasource-error" [error]="datasourceError[$index] ? datasourceError[$index] : ''"></tb-error> |
|||
</div> |
|||
</div> |
|||
</mat-list-item> |
|||
</mat-list> |
|||
</div> |
|||
</ng-template> |
|||
<div *ngIf="showAddDatasource"> |
|||
<button type="button" |
|||
mat-stroked-button color="primary" |
|||
(click)="addDatasource()" |
|||
matTooltip="{{ 'widget-config.add-datasource' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<span translate>widget-config.add-datasource</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,83 @@ |
|||
/** |
|||
* 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 "../../../../../../theme"; |
|||
|
|||
.tb-datasource-list-item { |
|||
&.mat-mdc-list-item { |
|||
height: auto; |
|||
display: block; |
|||
padding: 0; |
|||
&.bordered { |
|||
padding-top: 16px; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
border-radius: 6px; |
|||
} |
|||
} |
|||
&.tb-draggable { |
|||
&.cdk-drag-preview { |
|||
background: #fff; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-datasource-list-item + .tb-datasource-list-item { |
|||
margin-top: 16px; |
|||
} |
|||
|
|||
.tb-datasource-index { |
|||
width: 24px; |
|||
height: 24px; |
|||
position: relative; |
|||
font-weight: 400; |
|||
font-size: 16px; |
|||
text-align: center; |
|||
color: $tb-primary-color; |
|||
&:before { |
|||
content: ""; |
|||
display: block; |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
background-color: $tb-primary-color; |
|||
opacity: 0.06; |
|||
border-radius: 100%; |
|||
} |
|||
} |
|||
|
|||
.tb-datasource-params { |
|||
position: relative; |
|||
tb-error.tb-datasource-error { |
|||
position: absolute; |
|||
bottom: 4px; |
|||
left: 8px; |
|||
} |
|||
} |
|||
|
|||
:host { |
|||
.tb-datasources { |
|||
|
|||
.handle { |
|||
cursor: move; |
|||
} |
|||
|
|||
.mat-mdc-list { |
|||
min-height: 68px; |
|||
padding-left: 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,329 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
FormControl, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
Validator |
|||
} from '@angular/forms'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { |
|||
Datasource, |
|||
DatasourceType, |
|||
JsonSettingsSchema, |
|||
WidgetConfigMode, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-datasources', |
|||
templateUrl: './datasources.component.html', |
|||
styleUrls: ['./datasources.component.scss', './widget-config.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => DatasourcesComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => DatasourcesComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class DatasourcesComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { |
|||
|
|||
datasourceType = DatasourceType; |
|||
|
|||
public get basicMode(): boolean { |
|||
return !this.widgetConfigComponent.widgetEditMode && this.configMode === WidgetConfigMode.basic; |
|||
} |
|||
|
|||
public get maxDatasources(): number { |
|||
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources; |
|||
} |
|||
|
|||
public get singleDatasource(): boolean { |
|||
return this.maxDatasources === 1; |
|||
} |
|||
|
|||
public get showAddDatasource(): boolean { |
|||
return this.widgetConfigComponent.modelValue?.typeParameters && |
|||
(this.maxDatasources === -1 || this.datasourcesFormArray.length < this.maxDatasources); |
|||
} |
|||
|
|||
public get dragDisabled(): boolean { |
|||
return this.disabled || this.singleDatasource || this.datasourcesFormArray.length < 2; |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
hideDataKeyLabel = false; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
hideDataKeyColor = false; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
hideDataKeyUnits = false; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
hideDataKeyDecimals = false; |
|||
|
|||
@Input() |
|||
configMode: WidgetConfigMode; |
|||
|
|||
datasourcesFormGroup: UntypedFormGroup; |
|||
|
|||
timeseriesKeyError = false; |
|||
|
|||
datasourceError: string[] = []; |
|||
|
|||
datasourcesMode: DatasourceType; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private utils: UtilsService, |
|||
public translate: TranslateService, |
|||
private widgetConfigComponent: WidgetConfigComponent) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
if (this.validate(null)) { |
|||
setTimeout(() => { |
|||
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.datasourcesFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.datasourcesFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.datasourcesFormGroup = this.fb.group({ |
|||
datasources: this.fb.array([]) |
|||
}); |
|||
this.datasourcesFormGroup.valueChanges.subscribe( |
|||
() => { |
|||
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (propName === 'configMode') { |
|||
this.configModeChanged(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
writeValue(datasources?: Datasource[]): void { |
|||
this.datasourcesFormArray.clear({emitEvent: false}); |
|||
this.datasourcesMode = this.detectDatasourcesMode(datasources); |
|||
let changed = false; |
|||
if (datasources) { |
|||
datasources.forEach((datasource) => { |
|||
if (this.basicMode && datasource.type !== this.datasourcesMode) { |
|||
datasource.type = this.datasourcesMode; |
|||
changed = true; |
|||
} |
|||
this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false}); |
|||
}); |
|||
} |
|||
if (this.singleDatasource && !this.datasourcesFormArray.length) { |
|||
this.addDatasource(false); |
|||
} |
|||
if (changed) { |
|||
setTimeout(() => { |
|||
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
validate(c: UntypedFormControl) { |
|||
this.timeseriesKeyError = false; |
|||
this.datasourceError = []; |
|||
if (!this.datasourcesFormGroup.valid) { |
|||
return { |
|||
datasources: { |
|||
valid: false, |
|||
} |
|||
}; |
|||
} |
|||
const datasources: Datasource[] = this.datasourcesFormGroup.get('datasources').value; |
|||
if (!this.datasourcesOptional && (!datasources || !datasources.length)) { |
|||
return { |
|||
datasources: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
if (this.hasAdditionalLatestDataKeys) { |
|||
let valid = datasources.filter(datasource => datasource?.dataKeys?.length).length > 0; |
|||
if (!valid) { |
|||
this.timeseriesKeyError = true; |
|||
return { |
|||
timeseriesDataKeys: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} else { |
|||
const emptyDatasources = datasources.filter(datasource => !datasource?.dataKeys?.length && |
|||
!datasource?.latestDataKeys?.length); |
|||
valid = emptyDatasources.length === 0; |
|||
if (!valid) { |
|||
for (const emptyDatasource of emptyDatasources) { |
|||
const i = datasources.indexOf(emptyDatasource); |
|||
this.datasourceError[i] = 'At least one data key should be specified'; |
|||
} |
|||
return { |
|||
dataKeys: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
datasourcesModeChange(datasourcesMode: DatasourceType) { |
|||
this.datasourcesMode = datasourcesMode; |
|||
if (this.basicMode) { |
|||
for (const datasourceControl of this.datasourcesControls) { |
|||
const datasource: Datasource = datasourceControl.value; |
|||
if (datasource.type !== datasourcesMode) { |
|||
datasource.type = datasourcesMode; |
|||
datasourceControl.patchValue(datasource); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private configModeChanged() { |
|||
if (this.basicMode) { |
|||
let datasourcesMode = this.detectDatasourcesMode(this.datasourcesFormGroup.get('datasources').value); |
|||
this.datasourcesModeChange(datasourcesMode); |
|||
} |
|||
} |
|||
|
|||
private detectDatasourcesMode(datasources?: Datasource[]) { |
|||
let datasourcesMode = DatasourceType.device; |
|||
if (datasources && datasources.length) { |
|||
datasourcesMode = datasources[0].type; |
|||
} |
|||
if (datasourcesMode !== DatasourceType.device && datasourcesMode !== DatasourceType.entity) { |
|||
datasourcesMode = DatasourceType.device; |
|||
} |
|||
return datasourcesMode; |
|||
} |
|||
|
|||
get datasourcesFormArray(): UntypedFormArray { |
|||
return this.datasourcesFormGroup.get('datasources') as UntypedFormArray; |
|||
} |
|||
|
|||
get datasourcesControls(): FormControl[] { |
|||
return this.datasourcesFormArray.controls as FormControl[]; |
|||
} |
|||
|
|||
public trackByDatasource(index: number, datasourceControl: AbstractControl): any { |
|||
return datasourceControl; |
|||
} |
|||
|
|||
private datasourcesUpdated(datasources: Datasource[]) { |
|||
this.propagateChange(datasources); |
|||
} |
|||
|
|||
public onDatasourceDrop(event: CdkDragDrop<string[]>) { |
|||
const datasourceForm = this.datasourcesFormArray.at(event.previousIndex); |
|||
this.datasourcesFormArray.removeAt(event.previousIndex); |
|||
this.datasourcesFormArray.insert(event.currentIndex, datasourceForm); |
|||
} |
|||
|
|||
public removeDatasource(index: number) { |
|||
this.datasourcesFormArray.removeAt(index); |
|||
} |
|||
|
|||
public addDatasource(emitEvent = true) { |
|||
let newDatasource: Datasource; |
|||
if (this.widgetConfigComponent.functionsOnly) { |
|||
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); |
|||
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)]; |
|||
} else { |
|||
const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity; |
|||
newDatasource = { type, |
|||
dataKeys: [] |
|||
}; |
|||
} |
|||
if (this.hasAdditionalLatestDataKeys) { |
|||
newDatasource.latestDataKeys = []; |
|||
} |
|||
this.datasourcesFormArray.push(this.fb.control(newDatasource, []), {emitEvent}); |
|||
} |
|||
|
|||
private get dataKeySettingsSchema(): JsonSettingsSchema { |
|||
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; |
|||
} |
|||
|
|||
private get dataKeysCallbacks(): DataKeysCallbacks { |
|||
return this.widgetConfigComponent.widgetConfigCallbacks; |
|||
} |
|||
|
|||
private get hasAdditionalLatestDataKeys(): boolean { |
|||
return this.widgetConfigComponent.widgetType === widgetType.timeseries && |
|||
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys; |
|||
} |
|||
|
|||
private get datasourcesOptional(): boolean { |
|||
return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional; |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
<!-- |
|||
|
|||
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 [formGroup]="timewindowConfig" class="tb-widget-config-panel"> |
|||
<div fxLayout="row" fxLayoutAlign="space-between center"> |
|||
<div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div> |
|||
<tb-toggle-header (valueChange)="timewindowConfig.get('useDashboardTimewindow').patchValue($event)" ignoreMdLgSize="true" |
|||
[options]="[ |
|||
{ name: translate.instant('widget-config.use-dashboard-timewindow'), value: true}, |
|||
{ name: translate.instant('widget-config.use-widget-timewindow'), value: false} |
|||
]" |
|||
[value]="timewindowConfig.get('useDashboardTimewindow').value" name="useDashboardTimewindow" useSelectOnMdLg="false"> |
|||
</tb-toggle-header> |
|||
</div> |
|||
<div class="tb-widget-config-row"> |
|||
<tb-timewindow asButton="true" |
|||
strokedButton |
|||
isEdit="true" |
|||
alwaysDisplayTypePrefix |
|||
[historyOnly]="onlyHistoryTimewindow" |
|||
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}" |
|||
aggregation="{{ widgetType === widgetTypes.timeseries }}" |
|||
formControlName="timewindow"></tb-timewindow> |
|||
<mat-slide-toggle class="mat-slide" formControlName="displayTimewindow"> |
|||
{{ 'widget-config.display-timewindow' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,115 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; |
|||
import { widgetType } from '@shared/models/widget.models'; |
|||
import { Timewindow } from '@shared/models/time/time.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
|
|||
export interface TimewindowConfigData { |
|||
useDashboardTimewindow: boolean; |
|||
displayTimewindow: boolean; |
|||
timewindow: Timewindow; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-timewindow-config-panel', |
|||
templateUrl: './timewindow-config-panel.component.html', |
|||
styleUrls: ['./widget-config.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TimewindowConfigPanelComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
widgetTypes = widgetType; |
|||
|
|||
public get widgetType(): widgetType { |
|||
return this.widgetConfigComponent.widgetType; |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
onlyHistoryTimewindow = false; |
|||
|
|||
|
|||
timewindowConfig: UntypedFormGroup; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
public translate: TranslateService, |
|||
private widgetConfigComponent: WidgetConfigComponent) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.timewindowConfig = this.fb.group({ |
|||
useDashboardTimewindow: [null, []], |
|||
displayTimewindow: [null, []], |
|||
timewindow: [null, []] |
|||
}); |
|||
this.timewindowConfig.valueChanges.subscribe( |
|||
(val) => this.propagateChange(val) |
|||
); |
|||
this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => { |
|||
this.updateTimewindowConfigEnabledState(); |
|||
}); |
|||
} |
|||
|
|||
writeValue(data?: TimewindowConfigData): void { |
|||
this.timewindowConfig.patchValue(data || {}, {emitEvent: false}); |
|||
this.updateTimewindowConfigEnabledState(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.timewindowConfig.disable({emitEvent: false}); |
|||
} else { |
|||
this.timewindowConfig.enable({emitEvent: false}); |
|||
this.updateTimewindowConfigEnabledState(); |
|||
} |
|||
} |
|||
|
|||
private updateTimewindowConfigEnabledState() { |
|||
const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value; |
|||
if (useDashboardTimewindow) { |
|||
this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false}); |
|||
this.timewindowConfig.get('timewindow').disable({emitEvent: false}); |
|||
} else { |
|||
this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false}); |
|||
this.timewindowConfig.get('timewindow').enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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 { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@app/shared/shared.module'; |
|||
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component'; |
|||
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; |
|||
import { DataKeysComponent } from '@home/components/widget/config/data-keys.component'; |
|||
import { DataKeyConfigDialogComponent } from '@home/components/widget/config/data-key-config-dialog.component'; |
|||
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component'; |
|||
import { DatasourceComponent } from '@home/components/widget/config/datasource.component'; |
|||
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component'; |
|||
import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component'; |
|||
import { FilterSelectComponent } from '@home/components/filter/filter-select.component'; |
|||
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module'; |
|||
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component'; |
|||
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component'; |
|||
import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: |
|||
[ |
|||
AlarmAssigneeSelectComponent, |
|||
AlarmFilterConfigComponent, |
|||
DataKeysComponent, |
|||
DataKeyConfigDialogComponent, |
|||
DataKeyConfigComponent, |
|||
DatasourceComponent, |
|||
DatasourcesComponent, |
|||
EntityAliasSelectComponent, |
|||
FilterSelectComponent, |
|||
TimewindowConfigPanelComponent, |
|||
WidgetUnitsComponent, |
|||
WidgetSettingsComponent |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
WidgetSettingsModule |
|||
], |
|||
exports: [ |
|||
AlarmAssigneeSelectComponent, |
|||
AlarmFilterConfigComponent, |
|||
DataKeysComponent, |
|||
DataKeyConfigDialogComponent, |
|||
DataKeyConfigComponent, |
|||
DatasourceComponent, |
|||
DatasourcesComponent, |
|||
EntityAliasSelectComponent, |
|||
FilterSelectComponent, |
|||
TimewindowConfigPanelComponent, |
|||
WidgetUnitsComponent, |
|||
WidgetSettingsComponent |
|||
] |
|||
}) |
|||
export class WidgetConfigComponentsModule { } |
|||
@ -0,0 +1,115 @@ |
|||
///
|
|||
/// 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 { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; |
|||
import { DatasourceCallbacks } from '@home/components/widget/config/datasource.component.models'; |
|||
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
|||
import { Observable } from 'rxjs'; |
|||
import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; |
|||
import { WidgetConfigMode } from '@shared/models/widget.models'; |
|||
|
|||
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; |
|||
|
|||
export interface IBasicWidgetConfigComponent { |
|||
|
|||
widgetConfig: WidgetConfigComponentData; |
|||
widgetConfigChanged: Observable<WidgetConfigComponentData>; |
|||
validateConfig(): boolean; |
|||
|
|||
} |
|||
|
|||
@Directive() |
|||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
|||
export abstract class BasicWidgetConfigComponent extends PageComponent implements |
|||
IBasicWidgetConfigComponent, OnInit, AfterViewInit { |
|||
|
|||
basicMode = WidgetConfigMode.basic; |
|||
|
|||
widgetConfigValue: WidgetConfigComponentData; |
|||
|
|||
set widgetConfig(value: WidgetConfigComponentData) { |
|||
this.widgetConfigValue = value; |
|||
this.setupConfig(this.widgetConfigValue); |
|||
} |
|||
|
|||
get widgetConfig(): WidgetConfigComponentData { |
|||
return this.widgetConfigValue; |
|||
} |
|||
|
|||
widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>(); |
|||
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); |
|||
|
|||
protected constructor(@Inject(Store) protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() {} |
|||
|
|||
ngAfterViewInit(): void { |
|||
setTimeout(() => { |
|||
if (!this.validateConfig()) { |
|||
this.onConfigChanged(this.prepareOutputConfig(this.configForm().value)); |
|||
} |
|||
}, 0); |
|||
} |
|||
|
|||
protected setupConfig(widgetConfig: WidgetConfigComponentData) { |
|||
this.onConfigSet(widgetConfig); |
|||
this.updateValidators(false); |
|||
for (const trigger of this.validatorTriggers()) { |
|||
const path = trigger.split('.'); |
|||
let control: AbstractControl = this.configForm(); |
|||
for (const part of path) { |
|||
control = control.get(part); |
|||
} |
|||
control.valueChanges.subscribe(() => { |
|||
this.updateValidators(true, trigger); |
|||
}); |
|||
} |
|||
this.configForm().valueChanges.subscribe((updated: any) => { |
|||
this.onConfigChanged(this.prepareOutputConfig(updated)); |
|||
}); |
|||
} |
|||
|
|||
protected updateValidators(emitEvent: boolean, trigger?: string) { |
|||
} |
|||
|
|||
protected validatorTriggers(): string[] { |
|||
return []; |
|||
} |
|||
|
|||
protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { |
|||
this.widgetConfigValue = widgetConfig; |
|||
this.widgetConfigChangedEmitter.emit(this.widgetConfigValue); |
|||
} |
|||
|
|||
protected prepareOutputConfig(config: any): WidgetConfigComponentData { |
|||
return config; |
|||
} |
|||
|
|||
public validateConfig(): boolean { |
|||
return this.configForm().valid; |
|||
} |
|||
|
|||
protected abstract configForm(): UntypedFormGroup; |
|||
|
|||
protected abstract onConfigSet(configData: WidgetConfigComponentData); |
|||
|
|||
} |
|||
@ -0,0 +1,169 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
.tb-widget-config-panel { |
|||
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); |
|||
border-radius: 4px; |
|||
padding: 16px; |
|||
gap: 16px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
letter-spacing: 0.15px; |
|||
position: relative; |
|||
&.no-padding-bottom { |
|||
padding-bottom: 0; |
|||
} |
|||
&.stroked { |
|||
box-shadow: none; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
border-radius: 6px; |
|||
} |
|||
} |
|||
.tb-widget-config-panel-title { |
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
} |
|||
.tb-widget-config-panel-hint { |
|||
font-size: 12px; |
|||
color: #808080; |
|||
} |
|||
.tb-widget-config-row { |
|||
height: 56px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: 16px; |
|||
padding-left: 16px; |
|||
padding-right: 12px; |
|||
border: 1px solid rgba(0, 0, 0, 0.12); |
|||
border-radius: 6px; |
|||
&.same-padding { |
|||
padding-right: 16px; |
|||
} |
|||
&.space-between { |
|||
justify-content: space-between; |
|||
} |
|||
.mat-divider-vertical { |
|||
height: 56px; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
|
|||
.mat-slide { |
|||
margin: 8px 0; |
|||
.mdc-form-field>label { |
|||
font-weight: 400; |
|||
font-size: 16px; |
|||
line-height: 24px; |
|||
margin-left: 12px; |
|||
} |
|||
} |
|||
|
|||
.slide-block { |
|||
display: block; |
|||
&:not(:last-child) { |
|||
margin-bottom: 8px; |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-form-field { |
|||
&.center { |
|||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined { |
|||
.mat-mdc-form-field-infix { |
|||
.mdc-text-field__input { |
|||
text-align: center; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
&.number { |
|||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined { |
|||
padding-right: 4px; |
|||
.mat-mdc-form-field-infix { |
|||
width: 80px; |
|||
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, |
|||
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-widget-config-row { |
|||
.mat-mdc-form-field { |
|||
.mat-mdc-text-field-wrapper.mdc-text-field--outlined { |
|||
padding-right: 12px; |
|||
padding-left: 12px; |
|||
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) { |
|||
.mdc-notched-outline__leading, .mdc-notched-outline__trailing { |
|||
border-color: rgba(0, 0, 0, 0.12); |
|||
} |
|||
} |
|||
.mat-mdc-form-field-infix { |
|||
padding-top: 7px; |
|||
padding-bottom: 7px; |
|||
min-height: 38px; |
|||
width: 72px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-widget-config-panel { |
|||
.mat-expansion-panel { |
|||
&.tb-settings { |
|||
box-shadow: none; |
|||
.mat-content { |
|||
overflow: visible; |
|||
} |
|||
.mat-expansion-panel-header { |
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
line-height: 24px; |
|||
letter-spacing: 0.25px; |
|||
padding: 0; |
|||
.mat-content { |
|||
flex: 0; |
|||
white-space: nowrap; |
|||
} |
|||
&:hover { |
|||
background: none; |
|||
} |
|||
.mat-expansion-indicator { |
|||
height: 32px; |
|||
padding: 2px; |
|||
} |
|||
} |
|||
.mat-expansion-panel-header-description { |
|||
align-items: center; |
|||
} |
|||
> .mat-expansion-panel-content { |
|||
> .mat-expansion-panel-body { |
|||
padding: 0; |
|||
} |
|||
} |
|||
.tb-json-object-panel, .tb-css-content-panel { |
|||
margin: 0 0 8px; |
|||
} |
|||
} |
|||
.mat-expansion-panel-content { |
|||
font: inherit; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic"> |
|||
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}"> |
|||
</mat-form-field> |
|||
@ -0,0 +1,67 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
|||
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-units', |
|||
templateUrl: './widget-units.component.html', |
|||
styleUrls: ['./widget-config.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => WidgetUnitsComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class WidgetUnitsComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
unitsFormControl: FormControl; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.unitsFormControl = this.fb.control('', []); |
|||
} |
|||
|
|||
writeValue(units?: string): void { |
|||
this.unitsFormControl.patchValue(units, {emitEvent: false}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.unitsFormControl.disable({emitEvent: false}); |
|||
} else { |
|||
this.unitsFormControl.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<!-- |
|||
|
|||
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-absolute-fill" [fxLayout]="legendContainerLayoutType"> |
|||
<tb-legend *ngIf="displayLegend && isLegendFirst" |
|||
[ngStyle]="legendStyle" |
|||
[legendConfig]="legendConfig" |
|||
[legendData]="legendData" |
|||
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)"> |
|||
</tb-legend> |
|||
<div fxFlex #flotElement></div> |
|||
<tb-legend *ngIf="displayLegend && !isLegendFirst" |
|||
[ngStyle]="legendStyle" |
|||
[legendConfig]="legendConfig" |
|||
[legendData]="legendData" |
|||
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)"> |
|||
</tb-legend> |
|||
</div> |
|||
@ -0,0 +1,151 @@ |
|||
///
|
|||
/// 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, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { ChartType, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; |
|||
import { TbFlot } from '@home/components/widget/lib/flot-widget'; |
|||
import { |
|||
defaultLegendConfig, |
|||
LegendConfig, |
|||
LegendData, |
|||
LegendPosition, |
|||
widgetType |
|||
} from '@shared/models/widget.models'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-flot-widget', |
|||
templateUrl: './flot-widget.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class FlotWidgetComponent implements OnInit { |
|||
|
|||
@ViewChild('flotElement', {static: true}) flotElement: ElementRef; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
chartType: ChartType; |
|||
|
|||
displayLegend: boolean; |
|||
legendConfig: LegendConfig; |
|||
legendData: LegendData; |
|||
isLegendFirst: boolean; |
|||
legendContainerLayoutType: string; |
|||
legendStyle: {[klass: string]: any}; |
|||
|
|||
public settings: TbFlotSettings; |
|||
private flot: TbFlot; |
|||
|
|||
constructor() { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.ctx.$scope.flotWidget = this; |
|||
this.settings = this.ctx.settings; |
|||
this.chartType = this.chartType || 'line'; |
|||
this.configureLegend(); |
|||
this.flot = new TbFlot(this.ctx, this.chartType, $(this.flotElement.nativeElement)); |
|||
} |
|||
|
|||
private configureLegend(): void { |
|||
|
|||
this.displayLegend = isDefinedAndNotNull(this.settings.showLegend) ? this.settings.showLegend |
|||
: false; |
|||
|
|||
this.legendContainerLayoutType = 'column'; |
|||
|
|||
if (this.displayLegend) { |
|||
this.legendConfig = this.settings.legendConfig || defaultLegendConfig(widgetType.timeseries); |
|||
if (this.ctx.defaultSubscription) { |
|||
this.legendData = this.ctx.defaultSubscription.legendData; |
|||
} else { |
|||
this.legendData = { |
|||
keys: [], |
|||
data: [] |
|||
}; |
|||
} |
|||
if (this.legendConfig.position === LegendPosition.top || |
|||
this.legendConfig.position === LegendPosition.bottom) { |
|||
this.legendContainerLayoutType = 'column'; |
|||
this.isLegendFirst = this.legendConfig.position === LegendPosition.top; |
|||
} else { |
|||
this.legendContainerLayoutType = 'row'; |
|||
this.isLegendFirst = this.legendConfig.position === LegendPosition.left; |
|||
} |
|||
switch (this.legendConfig.position) { |
|||
case LegendPosition.top: |
|||
this.legendStyle = { |
|||
paddingBottom: '8px', |
|||
maxHeight: '50%', |
|||
overflowY: 'auto' |
|||
}; |
|||
break; |
|||
case LegendPosition.bottom: |
|||
this.legendStyle = { |
|||
paddingTop: '8px', |
|||
maxHeight: '50%', |
|||
overflowY: 'auto' |
|||
}; |
|||
break; |
|||
case LegendPosition.left: |
|||
this.legendStyle = { |
|||
paddingRight: '0px', |
|||
maxWidth: '50%', |
|||
overflowY: 'auto' |
|||
}; |
|||
break; |
|||
case LegendPosition.right: |
|||
this.legendStyle = { |
|||
paddingLeft: '0px', |
|||
maxWidth: '50%', |
|||
overflowY: 'auto' |
|||
}; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public onLegendKeyHiddenChange(index: number) { |
|||
for (const id of Object.keys(this.ctx.subscriptions)) { |
|||
const subscription = this.ctx.subscriptions[id]; |
|||
subscription.updateDataVisibility(index); |
|||
} |
|||
} |
|||
|
|||
public onDataUpdated() { |
|||
this.flot.update(); |
|||
} |
|||
|
|||
public onLatestDataUpdated() { |
|||
this.flot.latestDataUpdate(); |
|||
} |
|||
|
|||
public onResize() { |
|||
this.flot.resize(); |
|||
} |
|||
|
|||
public onEditModeChanged() { |
|||
this.flot.checkMouseEvents(); |
|||
} |
|||
|
|||
public onDestroy() { |
|||
this.flot.destroy(); |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,31 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<tb-dashboard class="tb-preview-dashboard" |
|||
[aliasController]="aliasController" |
|||
[stateController]="stateController" |
|||
[widgets]="widgets" |
|||
[autofillHeight]="true" |
|||
[columns]="24" |
|||
[isEdit]="false" |
|||
[isMobileDisabled]="true" |
|||
[isEditActionEnabled]="false" |
|||
[isRemoveActionEnabled]="false"> |
|||
</tb-dashboard> |
|||
<div class="tb-preview-panel"> |
|||
<ng-content select=".tb-preview-panel-content"></ng-content> |
|||
</div> |
|||
@ -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. |
|||
*/ |
|||
:host { |
|||
z-index: 10; |
|||
background: #F3F6FA; |
|||
.tb-preview-dashboard { |
|||
position: absolute; |
|||
top: 15%; |
|||
bottom: 15%; |
|||
left: 0; |
|||
right: 0; |
|||
} |
|||
.tb-preview-panel { |
|||
position: absolute; |
|||
top: 16px; |
|||
left: 24px; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
tb-dashboard.tb-preview-dashboard { |
|||
.tb-dashboard-content { |
|||
background-color: transparent !important; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
///
|
|||
/// 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, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { IAliasController, IStateController } from '@core/api/widget-api.models'; |
|||
import { Widget, WidgetConfig } from '@shared/models/widget.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { deepClone } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-preview', |
|||
templateUrl: './widget-preview.component.html', |
|||
styleUrls: ['./widget-preview.component.scss'] |
|||
}) |
|||
export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges { |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
stateController: IStateController; |
|||
|
|||
@Input() |
|||
widget: Widget; |
|||
|
|||
@Input() |
|||
widgetConfig: WidgetConfig; |
|||
|
|||
widgets: Widget[]; |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.loadPreviewWidget(); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
let reloadPreviewWidget = false; |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (['widget', 'widgetConfig'].includes(propName)) { |
|||
reloadPreviewWidget = true; |
|||
} |
|||
} |
|||
} |
|||
if (reloadPreviewWidget) { |
|||
this.loadPreviewWidget(); |
|||
} |
|||
} |
|||
|
|||
private loadPreviewWidget() { |
|||
const widget = deepClone(this.widget); |
|||
widget.sizeX = 24; |
|||
widget.sizeY = this.widget.sizeY * 2; |
|||
widget.row = 0; |
|||
widget.col = 0; |
|||
widget.config = this.widgetConfig; |
|||
this.widgets = [widget]; |
|||
} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue