52 changed files with 2611 additions and 313 deletions
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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 #widgetForm="ngForm" [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 900px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>widget.add</h2> |
|||
<span fxFlex></span> |
|||
<div [tb-help]="helpLinkIdForWidgetType()"></div> |
|||
<button mat-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 mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;"> |
|||
<tb-widget-config |
|||
[aliasController]="aliasController" |
|||
[functionsOnly]="false" |
|||
[entityAliases]="dashboard.configuration.entityAliases" |
|||
[dashboardStates]="dashboard.configuration.states" |
|||
formControlName="widgetConfig"> |
|||
</tb-widget-config> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,137 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { Widget, widgetTypesData } from '@shared/models/widget.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { Dashboard } from '@app/shared/models/dashboard.models'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; |
|||
import { isDefined, isString } from '@core/utils'; |
|||
|
|||
export interface AddWidgetDialogData { |
|||
dashboard: Dashboard; |
|||
aliasController: IAliasController; |
|||
widget: Widget; |
|||
widgetInfo: WidgetInfo; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-add-widget-dialog', |
|||
templateUrl: './add-widget-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
widgetFormGroup: FormGroup; |
|||
|
|||
dashboard: Dashboard; |
|||
aliasController: IAliasController; |
|||
widget: Widget; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
private entityService: EntityService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.dashboard = this.data.dashboard; |
|||
this.aliasController = this.data.aliasController; |
|||
this.widget = this.data.widget; |
|||
|
|||
const widgetInfo = this.data.widgetInfo; |
|||
|
|||
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; |
|||
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; |
|||
const typeParameters = widgetInfo.typeParameters; |
|||
const actionSources = widgetInfo.actionSources; |
|||
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; |
|||
let settingsSchema; |
|||
if (!rawSettingsSchema || rawSettingsSchema === '') { |
|||
settingsSchema = {}; |
|||
} else { |
|||
settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema; |
|||
} |
|||
let dataKeySettingsSchema; |
|||
if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') { |
|||
dataKeySettingsSchema = {}; |
|||
} else { |
|||
dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema; |
|||
} |
|||
const widgetConfig: WidgetConfigComponentData = { |
|||
config: this.widget.config, |
|||
layout: {}, |
|||
widgetType: this.widget.type, |
|||
typeParameters, |
|||
actionSources, |
|||
isDataEnabled, |
|||
settingsSchema, |
|||
dataKeySettingsSchema |
|||
}; |
|||
|
|||
this.widgetFormGroup = this.fb.group({ |
|||
widgetConfig: [widgetConfig, []] |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
helpLinkIdForWidgetType(): string { |
|||
let link = 'widgetsConfig'; |
|||
if (this.widget && this.widget.type) { |
|||
link = widgetTypesData.get(this.widget.type).configHelpLinkId; |
|||
} |
|||
return link; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
add(): void { |
|||
this.submitted = true; |
|||
const widgetConfig: WidgetConfigComponentData = this.widgetFormGroup.get('widgetConfig').value; |
|||
this.widget.config = widgetConfig.config; |
|||
this.widget.config.mobileOrder = widgetConfig.layout.mobileOrder; |
|||
this.widget.config.mobileHeight = widgetConfig.layout.mobileHeight; |
|||
this.dialogRef.close(this.widget); |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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 #settingsForm="ngForm" (ngSubmit)="save()"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>{{settings ? 'dashboard.settings' : 'layout.settings'}}</h2> |
|||
<span fxFlex></span> |
|||
<button mat-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 mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<div *ngIf="settings" [formGroup]="settingsFormGroup"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>dashboard.state-controller</mat-label> |
|||
<mat-select required matInput formControlName="stateControllerId"> |
|||
<mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId"> |
|||
{{stateControllerId}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-checkbox fxFlex formControlName="toolbarAlwaysOpen"> |
|||
{{ 'dashboard.toolbar-always-open' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showTitle"> |
|||
{{ 'dashboard.display-title' | translate }} |
|||
</mat-checkbox> |
|||
<tb-color-input fxFlex |
|||
label="{{'dashboard.title-color' | translate}}" |
|||
icon="format_color_fill" |
|||
openOnInput |
|||
formControlName="titleColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-checkbox fxFlex formControlName="showDashboardsSelect"> |
|||
{{ 'dashboard.display-dashboards-selection' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showEntitiesSelect"> |
|||
{{ 'dashboard.display-entities-selection' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showDashboardTimewindow"> |
|||
{{ 'dashboard.display-dashboard-timewindow' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="showDashboardExport"> |
|||
{{ 'dashboard.display-dashboard-export' | translate }} |
|||
</mat-checkbox> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="gridSettings" [formGroup]="gridSettingsFormGroup"> |
|||
<tb-color-input fxFlex |
|||
label="{{'layout.color' | translate}}" |
|||
icon="format_color_fill" |
|||
openOnInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>dashboard.columns-count</mat-label> |
|||
<input matInput formControlName="columns" type="number" step="any" min="10" |
|||
max="1000" required> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('required')"> |
|||
{{ 'dashboard.columns-count-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('min')"> |
|||
{{ 'dashboard.min-columns-count-message' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('max')"> |
|||
{{ 'dashboard.max-columns-count-message' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>dashboard.widgets-margins</mat-label> |
|||
<input matInput formControlName="margin" type="number" step="any" min="0" |
|||
max="50" required> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('required')"> |
|||
{{ 'dashboard.margin-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('min')"> |
|||
{{ 'dashboard.min-margin-message' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('max')"> |
|||
{{ 'dashboard.max-margin-message' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-checkbox fxFlex formControlName="autoFillHeight" style="display: block; padding-bottom: 12px;"> |
|||
{{ 'dashboard.autofill-height' | translate }} |
|||
</mat-checkbox> |
|||
<tb-color-input fxFlex |
|||
label="{{'dashboard.background-color' | translate}}" |
|||
icon="format_color_fill" |
|||
openOnInput |
|||
formControlName="backgroundColor"> |
|||
</tb-color-input> |
|||
<tb-image-input fxFlex |
|||
label="{{'dashboard.background-image' | translate}}" |
|||
formControlName="backgroundImageUrl"> |
|||
</tb-image-input> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>dashboard.background-size-mode</mat-label> |
|||
<mat-select matInput formControlName="backgroundSizeMode"> |
|||
<mat-option value="100%">Fit width</mat-option> |
|||
<mat-option value="auto 100%">Fit height</mat-option> |
|||
<mat-option value="cover">Cover</mat-option> |
|||
<mat-option value="contain">Contain</mat-option> |
|||
<mat-option value="auto">Original size</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<small translate>dashboard.mobile-layout</small> |
|||
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-checkbox fxFlex formControlName="mobileAutoFillHeight"> |
|||
{{ 'dashboard.autofill-height' | translate }} |
|||
</mat-checkbox> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>dashboard.mobile-row-height</mat-label> |
|||
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5" |
|||
max="200" required> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')"> |
|||
{{ 'dashboard.mobile-row-height-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')"> |
|||
{{ 'dashboard.min-mobile-row-height-message' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')"> |
|||
{{ 'dashboard.max-mobile-row-height-message' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || settingsFormGroup.invalid || gridSettingsFormGroup.invalid |
|||
|| (!settingsFormGroup.dirty && !gridSettingsFormGroup.dirty)"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,137 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models'; |
|||
import { isUndefined } from '@core/utils'; |
|||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
|||
import { StatesControllerService } from './states/states-controller.service'; |
|||
|
|||
export interface DashboardSettingsDialogData { |
|||
settings?: DashboardSettings; |
|||
gridSettings?: GridSettings; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-dashboard-settings-dialog', |
|||
templateUrl: './dashboard-settings-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: DashboardSettingsDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
settings: DashboardSettings; |
|||
gridSettings: GridSettings; |
|||
|
|||
settingsFormGroup: FormGroup; |
|||
gridSettingsFormGroup: FormGroup; |
|||
|
|||
stateControllerIds: string[]; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private dashboardUtils: DashboardUtilsService, |
|||
private translate: TranslateService, |
|||
private statesControllerService: StatesControllerService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers()); |
|||
|
|||
this.settings = this.data.settings; |
|||
this.gridSettings = this.data.gridSettings; |
|||
|
|||
if (this.settings) { |
|||
this.settingsFormGroup = this.fb.group({ |
|||
stateControllerId: [isUndefined(this.settings.stateControllerId) ? 'entity' : this.settings.stateControllerId, []], |
|||
toolbarAlwaysOpen: [isUndefined(this.settings.toolbarAlwaysOpen) ? true : this.settings.toolbarAlwaysOpen, []], |
|||
showTitle: [isUndefined(this.settings.showTitle) ? true : this.settings.showTitle, []], |
|||
titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []], |
|||
showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []], |
|||
showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []], |
|||
showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []], |
|||
showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []] |
|||
}); |
|||
this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe( |
|||
(stateControllerId: StateControllerId) => { |
|||
if (stateControllerId !== 'default') { |
|||
this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true); |
|||
} |
|||
} |
|||
); |
|||
} else { |
|||
this.settingsFormGroup = this.fb.group({}); |
|||
} |
|||
|
|||
if (this.gridSettings) { |
|||
this.gridSettingsFormGroup = this.fb.group({ |
|||
color: [this.gridSettings.color || 'rgba(0,0,0,0.870588)', []], |
|||
columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1000)]], |
|||
margin: [this.gridSettings.margin || 10, [Validators.required, Validators.min(0), Validators.max(50)]], |
|||
autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []], |
|||
backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []], |
|||
backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []], |
|||
backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []], |
|||
mobileAutoFillHeight: [isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight, []], |
|||
mobileRowHeight: [isUndefined(this.gridSettings.mobileRowHeight) ? 70 : this.gridSettings.mobileRowHeight, |
|||
[Validators.required, Validators.min(5), Validators.max(200)]] |
|||
}); |
|||
} else { |
|||
this.gridSettingsFormGroup = this.fb.group({}); |
|||
} |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
let settings: DashboardSettings = null; |
|||
let gridSettings: GridSettings = null; |
|||
if (this.settings) { |
|||
settings = {...this.settings, ...this.settingsFormGroup.value}; |
|||
} |
|||
if (this.gridSettings) { |
|||
gridSettings = {...this.gridSettings, ...this.gridSettingsFormGroup.value}; |
|||
} |
|||
this.dialogRef.close({settings, gridSettings}); |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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> |
|||
<mat-tab-group *ngIf="hasWidgetTypes()" class="tb-absolute-fill" fxFlex> |
|||
<mat-tab *ngIf="timeseriesWidgetTypes.length" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> |
|||
<tb-dashboard [aliasController]="aliasController" |
|||
[widgets]="timeseriesWidgetTypes" |
|||
[widgetLayouts]="{}" |
|||
[isEdit]="false" |
|||
[isMobile]="true" |
|||
[isEditActionEnabled]="false" |
|||
[isExportActionEnabled]="false" |
|||
[isRemoveActionEnabled]="false" |
|||
[callbacks]="callbacks"></tb-dashboard> |
|||
</mat-tab> |
|||
<mat-tab *ngIf="latestWidgetTypes.length" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}"> |
|||
<tb-dashboard [aliasController]="aliasController" |
|||
[widgets]="latestWidgetTypes" |
|||
[widgetLayouts]="{}" |
|||
[isEdit]="false" |
|||
[isMobile]="true" |
|||
[isEditActionEnabled]="false" |
|||
[isExportActionEnabled]="false" |
|||
[isRemoveActionEnabled]="false" |
|||
[callbacks]="callbacks"></tb-dashboard> |
|||
</mat-tab> |
|||
<mat-tab *ngIf="rpcWidgetTypes.length" style="height: 100%;" label="{{ 'widget.rpc' | translate }}"> |
|||
<tb-dashboard [aliasController]="aliasController" |
|||
[widgets]="rpcWidgetTypes" |
|||
[widgetLayouts]="{}" |
|||
[isEdit]="false" |
|||
[isMobile]="true" |
|||
[isEditActionEnabled]="false" |
|||
[isExportActionEnabled]="false" |
|||
[isRemoveActionEnabled]="false" |
|||
[callbacks]="callbacks"></tb-dashboard> |
|||
</mat-tab> |
|||
<mat-tab *ngIf="alarmWidgetTypes.length" style="height: 100%;" label="{{ 'widget.alarm' | translate }}"> |
|||
<tb-dashboard [aliasController]="aliasController" |
|||
[widgets]="alarmWidgetTypes" |
|||
[widgetLayouts]="{}" |
|||
[isEdit]="false" |
|||
[isMobile]="true" |
|||
[isEditActionEnabled]="false" |
|||
[isExportActionEnabled]="false" |
|||
[isRemoveActionEnabled]="false" |
|||
[callbacks]="callbacks"></tb-dashboard> |
|||
</mat-tab> |
|||
<mat-tab *ngIf="staticWidgetTypes.length" style="height: 100%;" label="{{ 'widget.static' | translate }}"> |
|||
<tb-dashboard [aliasController]="aliasController" |
|||
[widgets]="staticWidgetTypes" |
|||
[widgetLayouts]="{}" |
|||
[isEdit]="false" |
|||
[isMobile]="true" |
|||
[isEditActionEnabled]="false" |
|||
[isExportActionEnabled]="false" |
|||
[isRemoveActionEnabled]="false" |
|||
[callbacks]="callbacks"></tb-dashboard> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
<span translate *ngIf="widgetsBundle && !hasWidgetTypes()" |
|||
style="text-transform: uppercase; display: flex;" |
|||
fxLayoutAlign="center center" |
|||
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> |
|||
<span translate *ngIf="!widgetsBundle" |
|||
style="text-transform: uppercase; display: flex;" |
|||
fxLayoutAlign="center center" |
|||
class="mat-headline tb-absolute-fill">widget.select-widgets-bundle</span> |
|||
</div> |
|||
@ -0,0 +1,23 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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 ::ng-deep { |
|||
.mat-tab-group { |
|||
.mat-tab-body-wrapper { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,144 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewEncapsulation, |
|||
Input, |
|||
Output, |
|||
EventEmitter, |
|||
OnChanges, |
|||
SimpleChanges |
|||
} from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { WidgetService } from '@core/http/widget.service'; |
|||
import { widgetType, Widget } from '@shared/models/widget.models'; |
|||
import { toWidgetInfo } from '@home/models/widget-component.models'; |
|||
import { DashboardCallbacks } from '../../models/dashboard-component.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-dashboard-widget-select', |
|||
templateUrl: './dashboard-widget-select.component.html', |
|||
styleUrls: ['./dashboard-widget-select.component.scss'] |
|||
}) |
|||
export class DashboardWidgetSelectComponent implements OnInit, OnChanges { |
|||
|
|||
@Input() |
|||
widgetsBundle: WidgetsBundle; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Output() |
|||
widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); |
|||
|
|||
timeseriesWidgetTypes: Array<Widget> = []; |
|||
latestWidgetTypes: Array<Widget> = []; |
|||
rpcWidgetTypes: Array<Widget> = []; |
|||
alarmWidgetTypes: Array<Widget> = []; |
|||
staticWidgetTypes: Array<Widget> = []; |
|||
|
|||
callbacks: DashboardCallbacks = { |
|||
onWidgetClicked: this.onWidgetClicked.bind(this) |
|||
}; |
|||
|
|||
constructor(private widgetsService: WidgetService) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (change.currentValue !== change.previousValue && change.currentValue) { |
|||
if (propName === 'widgetsBundle') { |
|||
this.loadLibrary(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private loadLibrary() { |
|||
this.timeseriesWidgetTypes.length = 0; |
|||
this.latestWidgetTypes.length = 0; |
|||
this.rpcWidgetTypes.length = 0; |
|||
this.alarmWidgetTypes.length = 0; |
|||
this.staticWidgetTypes.length = 0; |
|||
const bundleAlias = this.widgetsBundle.alias; |
|||
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; |
|||
this.widgetsService.getBundleWidgetTypes(bundleAlias, |
|||
isSystem).subscribe( |
|||
(types) => { |
|||
types = types.sort((a, b) => b.createdTime - a.createdTime); |
|||
let top = 0; |
|||
types.forEach((type) => { |
|||
const widgetTypeInfo = toWidgetInfo(type); |
|||
const widget: Widget = { |
|||
typeId: type.id, |
|||
isSystemType: isSystem, |
|||
bundleAlias, |
|||
typeAlias: widgetTypeInfo.alias, |
|||
type: widgetTypeInfo.type, |
|||
title: widgetTypeInfo.widgetName, |
|||
sizeX: widgetTypeInfo.sizeX, |
|||
sizeY: widgetTypeInfo.sizeY, |
|||
row: top, |
|||
col: 0, |
|||
config: JSON.parse(widgetTypeInfo.defaultConfig) |
|||
}; |
|||
widget.config.title = widgetTypeInfo.widgetName; |
|||
switch (widgetTypeInfo.type) { |
|||
case widgetType.timeseries: |
|||
this.timeseriesWidgetTypes.push(widget); |
|||
break; |
|||
case widgetType.latest: |
|||
this.latestWidgetTypes.push(widget); |
|||
break; |
|||
case widgetType.rpc: |
|||
this.rpcWidgetTypes.push(widget); |
|||
break; |
|||
case widgetType.alarm: |
|||
this.alarmWidgetTypes.push(widget); |
|||
break; |
|||
case widgetType.static: |
|||
this.staticWidgetTypes.push(widget); |
|||
break; |
|||
} |
|||
top += widget.sizeY; |
|||
}); |
|||
} |
|||
); |
|||
} |
|||
|
|||
hasWidgetTypes() { |
|||
return this.timeseriesWidgetTypes.length > 0 || |
|||
this.latestWidgetTypes.length > 0 || |
|||
this.rpcWidgetTypes.length > 0 || |
|||
this.alarmWidgetTypes.length > 0 || |
|||
this.staticWidgetTypes.length > 0; |
|||
} |
|||
|
|||
private onWidgetClicked($event: Event, widget: Widget, index: number): void { |
|||
this.widgetSelected.emit(widget); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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 ::ng-deep { |
|||
button.tb-layout-button { |
|||
width: 100%; |
|||
max-width: 240px; |
|||
height: 100%; |
|||
.mat-button-wrapper { |
|||
padding: 40px; |
|||
line-height: 18px; |
|||
span { |
|||
font-size: 18px; |
|||
font-weight: 400; |
|||
white-space: normal; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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 #widgetForm="ngForm" [formGroup]="layoutsFormGroup" (ngSubmit)="save()"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>layout.manage</h2> |
|||
<span fxFlex></span> |
|||
<button mat-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 mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-checkbox fxFlex formControlName="main"> |
|||
{{ 'layout.main' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox fxFlex formControlName="right"> |
|||
{{ 'layout.right' | translate }} |
|||
</mat-checkbox> |
|||
</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<button fxFlex fxLayout="column" |
|||
type="button" mat-button mat-raised-button color="primary" |
|||
class="tb-layout-button" |
|||
(click)="openLayoutSettings('main')"> |
|||
<span translate>layout.main</span> |
|||
</button> |
|||
<button fxFlex fxLayout="column" [fxShow]="layoutsFormGroup.get('right').value" |
|||
type="button" mat-button mat-raised-button color="primary" |
|||
class="tb-layout-button" |
|||
(click)="openLayoutSettings('right')"> |
|||
<span translate>layout.right</span> |
|||
</button> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || layoutsFormGroup.invalid || !layoutsFormGroup.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,125 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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, Inject, OnInit, SkipSelf } from '@angular/core'; |
|||
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { Widget, widgetTypesData } from '@shared/models/widget.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { Dashboard, DashboardLayoutId, DashboardStateLayouts } from '@app/shared/models/dashboard.models'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; |
|||
import { deepClone, isDefined, isString } from '@core/utils'; |
|||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
DashboardSettingsDialogComponent, |
|||
DashboardSettingsDialogData |
|||
} from '@home/pages/dashboard/dashboard-settings-dialog.component'; |
|||
|
|||
export interface ManageDashboardLayoutsDialogData { |
|||
layouts: DashboardStateLayouts; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-manage-dashboard-layouts-dialog', |
|||
templateUrl: './manage-dashboard-layouts-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardLayoutsDialogComponent}], |
|||
styleUrls: ['./layout-button.scss'] |
|||
}) |
|||
export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
layoutsFormGroup: FormGroup; |
|||
|
|||
layouts: DashboardStateLayouts; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardLayoutsDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private dashboardUtils: DashboardUtilsService, |
|||
private translate: TranslateService, |
|||
private dialog: MatDialog) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.layouts = this.data.layouts; |
|||
this.layoutsFormGroup = this.fb.group({ |
|||
main: [{value: isDefined(this.layouts.main), disabled: true}, []], |
|||
right: [isDefined(this.layouts.right), []], |
|||
} |
|||
); |
|||
for (const l of Object.keys(this.layoutsFormGroup.controls)) { |
|||
const control = this.layoutsFormGroup.controls[l]; |
|||
if (!this.layouts[l]) { |
|||
this.layouts[l] = this.dashboardUtils.createDefaultLayoutData(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
openLayoutSettings(layoutId: DashboardLayoutId) { |
|||
const gridSettings = deepClone(this.layouts[layoutId].gridSettings); |
|||
this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData, |
|||
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
settings: null, |
|||
gridSettings |
|||
} |
|||
}).afterClosed().subscribe((data) => { |
|||
if (data && data.gridSettings) { |
|||
this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings); |
|||
this.layoutsFormGroup.markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
for (const l of Object.keys(this.layoutsFormGroup.controls)) { |
|||
const control = this.layoutsFormGroup.controls[l]; |
|||
if (!control.value) { |
|||
delete this.layouts[l]; |
|||
} |
|||
} |
|||
this.dialogRef.close(this.layouts); |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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 #widgetForm="ngForm"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>layout.select</h2> |
|||
<span fxFlex></span> |
|||
<button mat-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 mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<button fxFlex fxLayout="column" |
|||
type="button" mat-button mat-raised-button color="primary" |
|||
class="tb-layout-button" |
|||
(click)="selectLayout('main')"> |
|||
<span translate>layout.main</span> |
|||
</button> |
|||
<button fxFlex fxLayout="column" |
|||
type="button" mat-button mat-raised-button color="primary" |
|||
class="tb-layout-button" |
|||
(click)="selectLayout('right')"> |
|||
<span translate>layout.right</span> |
|||
</button> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,50 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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, OnInit } from '@angular/core'; |
|||
import { MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { DashboardLayoutId } from '@app/shared/models/dashboard.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-select-target-layout-dialog', |
|||
templateUrl: './select-target-layout-dialog.component.html', |
|||
styleUrls: ['./layout-button.scss'] |
|||
}) |
|||
export class SelectTargetLayoutDialogComponent extends DialogComponent<SelectTargetLayoutDialogComponent, DashboardLayoutId> |
|||
implements OnInit { |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
public dialogRef: MatDialogRef<SelectTargetLayoutDialogComponent, DashboardLayoutId>) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
selectLayout(layoutId: DashboardLayoutId) { |
|||
this.dialogRef.close(layoutId); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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-container"> |
|||
<label class="tb-title">{{label}}</label> |
|||
<ng-container #flow="flow" |
|||
[flowConfig]="{singleFile: true}"> |
|||
<div class="tb-image-select-container"> |
|||
<div class="tb-image-preview-container"> |
|||
<div *ngIf="!safeImageUrl" translate>dashboard.no-image</div> |
|||
<img *ngIf="safeImageUrl" class="tb-image-preview" [src]="safeImageUrl" /> |
|||
</div> |
|||
<div class="tb-image-clear-container"> |
|||
<button mat-button mat-icon-button color="primary" |
|||
type="button" |
|||
(click)="clearImage()" |
|||
class="tb-image-clear-btn" |
|||
matTooltip="{{ 'action.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div class="drop-area tb-flow-drop" |
|||
flowDrop |
|||
[flow]="flow.flowJs"> |
|||
<label for="select" translate>dashboard.drop-image</label> |
|||
<input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="select"> |
|||
</div> |
|||
</div> |
|||
</ng-container> |
|||
</div> |
|||
@ -0,0 +1,104 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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"; |
|||
|
|||
$previewSize: 100px !default; |
|||
|
|||
:host { |
|||
|
|||
.tb-container { |
|||
margin-top: 0px; |
|||
label.tb-title { |
|||
display: block; |
|||
padding-bottom: 8px; |
|||
} |
|||
} |
|||
|
|||
.tb-image-select-container { |
|||
position: relative; |
|||
width: 100%; |
|||
height: $previewSize; |
|||
} |
|||
|
|||
.tb-image-preview { |
|||
width: auto; |
|||
max-width: $previewSize; |
|||
height: auto; |
|||
max-height: $previewSize; |
|||
} |
|||
|
|||
.tb-image-preview-container { |
|||
position: relative; |
|||
float: left; |
|||
width: $previewSize; |
|||
height: $previewSize; |
|||
margin-right: 12px; |
|||
vertical-align: top; |
|||
border: solid 1px; |
|||
|
|||
div { |
|||
width: 100%; |
|||
font-size: 18px; |
|||
text-align: center; |
|||
} |
|||
|
|||
div, |
|||
.tb-image-preview { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
} |
|||
} |
|||
|
|||
.tb-image-clear-container { |
|||
position: relative; |
|||
float: right; |
|||
width: 48px; |
|||
height: $previewSize; |
|||
} |
|||
|
|||
.tb-image-clear-btn { |
|||
position: absolute !important; |
|||
top: 50%; |
|||
transform: translate(0%, -50%) !important; |
|||
} |
|||
|
|||
.file-input { |
|||
display: none; |
|||
} |
|||
|
|||
.tb-flow-drop { |
|||
position: relative; |
|||
height: $previewSize; |
|||
overflow: hidden; |
|||
border: dashed 2px; |
|||
|
|||
label { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
width: 100%; |
|||
height: 100%; |
|||
font-size: 16px; |
|||
text-align: center; |
|||
|
|||
@media #{$mat-gt-sm} { |
|||
font-size: 24px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 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, forwardRef, Input, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { DataKey, DatasourceType } from '@shared/models/widget.models'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { Observable, of, Subscription } from 'rxjs'; |
|||
import { map, mergeMap, tap } from 'rxjs/operators'; |
|||
import { alarmFields } from '@shared/models/alarm.models'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { FlowDirective } from '@flowjs/ngx-flow'; |
|||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-image-input', |
|||
templateUrl: './image-input.component.html', |
|||
styleUrls: ['./image-input.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ImageInputComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { |
|||
|
|||
@Input() |
|||
label: string; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.requiredValue !== newVal) { |
|||
this.requiredValue = newVal; |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
imageUrl: string; |
|||
safeImageUrl: SafeUrl; |
|||
|
|||
@ViewChild('flow', {static: true}) |
|||
flow: FlowDirective; |
|||
|
|||
autoUploadSubscription: Subscription; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private sanitizer: DomSanitizer) { |
|||
super(store); |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
this.autoUploadSubscription = this.flow.events$.subscribe(event => { |
|||
if (event.type === 'fileAdded') { |
|||
const file = (event.event[0] as flowjs.FlowFile).file; |
|||
const reader = new FileReader(); |
|||
reader.onload = (loadEvent) => { |
|||
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { |
|||
this.imageUrl = reader.result; |
|||
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); |
|||
this.updateModel(); |
|||
} |
|||
}; |
|||
reader.readAsDataURL(file); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.autoUploadSubscription.unsubscribe(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(value: string): void { |
|||
this.imageUrl = value; |
|||
if (this.imageUrl) { |
|||
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); |
|||
} else { |
|||
this.safeImageUrl = null; |
|||
} |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.propagateChange(this.imageUrl); |
|||
} |
|||
|
|||
clearImage() { |
|||
this.imageUrl = null; |
|||
this.safeImageUrl = null; |
|||
this.updateModel(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue