89 changed files with 2775 additions and 579 deletions
@ -0,0 +1,77 @@ |
|||
<!-- |
|||
|
|||
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 #addWidgetForm="ngForm" [formGroup]="addWidgetFormGroup" (ngSubmit)="add()" style="min-width: 400px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>attribute.add-widget-to-dashboard</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 fxLayout="column"> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<mat-radio-group formControlName="addToDashboardType"> |
|||
<mat-radio-button [value]="0" color="primary"> |
|||
<section fxLayout="column" style="width: 300px;"> |
|||
<span translate>dashboard.select-existing</span> |
|||
<tb-dashboard-autocomplete formControlName="dashboardId" |
|||
[required]="addWidgetFormGroup.get('addToDashboardType').value === 0" |
|||
[selectFirstDashboard]="false"> |
|||
</tb-dashboard-autocomplete> |
|||
</section> |
|||
</mat-radio-button> |
|||
<mat-radio-button [value]="1" color="primary"> |
|||
<section fxLayout="column" style="width: 300px;"> |
|||
<span translate>dashboard.create-new</span> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>dashboard.new-dashboard-title</mat-label> |
|||
<input matInput formControlName="newDashboardTitle" |
|||
[required]="addWidgetFormGroup.get('addToDashboardType').value === 1"> |
|||
<mat-error *ngIf="addWidgetFormGroup.get('newDashboardTitle').hasError('required')"> |
|||
{{ 'dashboard.title-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</section> |
|||
</mat-radio-button> |
|||
</mat-radio-group> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<mat-checkbox formControlName="openDashboard" |
|||
style="margin-bottom: 0px; padding-right: 20px;"> |
|||
{{ 'dashboard.open-dashboard' | translate }} |
|||
</mat-checkbox> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || addWidgetFormGroup.invalid || !addWidgetFormGroup.dirty"> |
|||
{{ '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,29 @@ |
|||
/** |
|||
* 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-radio-button { |
|||
display: block; |
|||
margin-bottom: 16px; |
|||
.mat-radio-label { |
|||
width: 100%; |
|||
align-items: start; |
|||
.mat-radio-label-content { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,222 @@ |
|||
///
|
|||
/// 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 { |
|||
DashboardLayoutId, |
|||
DashboardSettings, |
|||
GridSettings, |
|||
StateControllerId, |
|||
Dashboard |
|||
} from '@app/shared/models/dashboard.models'; |
|||
import { isUndefined, objToBase64 } from '@core/utils'; |
|||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
|||
import { EntityId } from '@app/shared/models/id/entity-id'; |
|||
import { Widget } from '@app/shared/models/widget.models'; |
|||
import { DashboardService } from '@core/http/dashboard.service'; |
|||
import { forkJoin, Observable, of } from 'rxjs'; |
|||
import { SelectTargetLayoutDialogComponent } from '@home/pages/dashboard/layout/select-target-layout-dialog.component'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
SelectTargetStateDialogComponent, |
|||
SelectTargetStateDialogData |
|||
} from '@home/pages/dashboard/states/select-target-state-dialog.component'; |
|||
import { mergeMap, map } from 'rxjs/operators'; |
|||
import { AliasesInfo } from '@shared/models/alias.models'; |
|||
import { ItemBufferService } from '@core/services/item-buffer.service'; |
|||
import { StateObject } from '@core/api/widget-api.models'; |
|||
|
|||
export interface AddWidgetToDashboardDialogData { |
|||
entityId: EntityId; |
|||
entityName: string; |
|||
widget: Widget; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-add-widget-to-dashboard-dialog', |
|||
templateUrl: './add-widget-to-dashboard-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetToDashboardDialogComponent}], |
|||
styleUrls: ['./add-widget-to-dashboard-dialog.component.scss'] |
|||
}) |
|||
export class AddWidgetToDashboardDialogComponent extends |
|||
DialogComponent<AddWidgetToDashboardDialogComponent, void> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
addWidgetFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: AddWidgetToDashboardDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<AddWidgetToDashboardDialogComponent, void>, |
|||
private fb: FormBuilder, |
|||
private utils: UtilsService, |
|||
private dashboardUtils: DashboardUtilsService, |
|||
private dashboardService: DashboardService, |
|||
private itembuffer: ItemBufferService, |
|||
private dialog: MatDialog) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.addWidgetFormGroup = this.fb.group( |
|||
{ |
|||
addToDashboardType: [0, []], |
|||
dashboardId: [null, [Validators.required]], |
|||
newDashboardTitle: [{value: null, disabled: true}, []], |
|||
openDashboard: [false, []] |
|||
} |
|||
); |
|||
|
|||
this.addWidgetFormGroup.get('addToDashboardType').valueChanges.subscribe( |
|||
(addToDashboardType: number) => { |
|||
if (addToDashboardType === 0) { |
|||
this.addWidgetFormGroup.get('dashboardId').setValidators([Validators.required]); |
|||
this.addWidgetFormGroup.get('dashboardId').enable(); |
|||
this.addWidgetFormGroup.get('newDashboardTitle').setValidators([]); |
|||
this.addWidgetFormGroup.get('newDashboardTitle').disable(); |
|||
this.addWidgetFormGroup.get('dashboardId').updateValueAndValidity(); |
|||
this.addWidgetFormGroup.get('newDashboardTitle').updateValueAndValidity(); |
|||
} else { |
|||
this.addWidgetFormGroup.get('dashboardId').setValidators([]); |
|||
this.addWidgetFormGroup.get('dashboardId').disable(); |
|||
this.addWidgetFormGroup.get('newDashboardTitle').setValidators([Validators.required]); |
|||
this.addWidgetFormGroup.get('newDashboardTitle').enable(); |
|||
this.addWidgetFormGroup.get('dashboardId').updateValueAndValidity(); |
|||
this.addWidgetFormGroup.get('newDashboardTitle').updateValueAndValidity(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
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); |
|||
} |
|||
|
|||
add(): void { |
|||
this.submitted = true; |
|||
const addToDashboardType: number = this.addWidgetFormGroup.get('addToDashboardType').value; |
|||
if (addToDashboardType === 0) { |
|||
const dashboardId: string = this.addWidgetFormGroup.get('dashboardId').value; |
|||
this.dashboardService.getDashboard(dashboardId).pipe( |
|||
mergeMap((dashboard) => { |
|||
dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); |
|||
return this.selectTargetState(dashboard).pipe( |
|||
mergeMap((targetState) => { |
|||
return forkJoin([of(dashboard), of(targetState), this.selectTargetLayout(dashboard, targetState)]); |
|||
}) |
|||
); |
|||
}) |
|||
).subscribe((res) => { |
|||
this.addWidgetToDashboard(res[0], res[1], res[2]); |
|||
}); |
|||
} else { |
|||
const dashboardTitle: string = this.addWidgetFormGroup.get('newDashboardTitle').value; |
|||
const newDashboard: Dashboard = { |
|||
title: dashboardTitle |
|||
}; |
|||
this.addWidgetToDashboard(newDashboard, 'default', 'main'); |
|||
} |
|||
} |
|||
|
|||
private selectTargetState(dashboard: Dashboard): Observable<string> { |
|||
const states = dashboard.configuration.states; |
|||
const stateIds = Object.keys(states); |
|||
if (stateIds.length > 1) { |
|||
return this.dialog.open<SelectTargetStateDialogComponent, SelectTargetStateDialogData, |
|||
string>(SelectTargetStateDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
states |
|||
} |
|||
}).afterClosed(); |
|||
} else { |
|||
return of(stateIds[0]); |
|||
} |
|||
} |
|||
|
|||
private selectTargetLayout(dashboard: Dashboard, targetState: string): Observable<DashboardLayoutId> { |
|||
const layouts = dashboard.configuration.states[targetState].layouts; |
|||
const layoutIds = Object.keys(layouts); |
|||
if (layoutIds.length > 1) { |
|||
return this.dialog.open<SelectTargetLayoutDialogComponent, any, |
|||
DashboardLayoutId>(SelectTargetLayoutDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed(); |
|||
} else { |
|||
return of(layoutIds[0] as DashboardLayoutId); |
|||
} |
|||
} |
|||
|
|||
private addWidgetToDashboard(dashboard: Dashboard, targetState: string, targetLayout: DashboardLayoutId) { |
|||
const aliasesInfo: AliasesInfo = { |
|||
datasourceAliases: {}, |
|||
targetDeviceAliases: {} |
|||
}; |
|||
aliasesInfo.datasourceAliases[0] = { |
|||
alias: this.data.entityName, |
|||
filter: this.dashboardUtils.createSingleEntityFilter(this.data.entityId) |
|||
}; |
|||
this.itembuffer.addWidgetToDashboard(dashboard, targetState, |
|||
targetLayout, this.data.widget, aliasesInfo, null, |
|||
48, null, -1, -1).pipe( |
|||
mergeMap((theDashboard) => { |
|||
return this.dashboardService.saveDashboard(theDashboard); |
|||
}) |
|||
).subscribe( |
|||
(theDashboard) => { |
|||
const openDashboard: boolean = this.addWidgetFormGroup.get('openDashboard').value; |
|||
this.dialogRef.close(); |
|||
if (openDashboard) { |
|||
let url; |
|||
const stateIds = Object.keys(dashboard.configuration.states); |
|||
const stateIndex = stateIds.indexOf(targetState); |
|||
if (stateIndex > 0) { |
|||
const stateObject: StateObject = { |
|||
id: targetState, |
|||
params: {} |
|||
}; |
|||
const state = objToBase64([ stateObject ]); |
|||
url = `/dashboards/${theDashboard.id.id}?state=${state}`; |
|||
} else { |
|||
url = `/dashboards/${theDashboard.id.id}`; |
|||
} |
|||
const urlTree = this.router.parseUrl(url); |
|||
this.router.navigateByUrl(url); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<form> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>{{ importTitle }}</h2> |
|||
<span fxFlex></span> |
|||
<div [tb-help]="'entitiesImport'"></div> |
|||
<button mat-button mat-icon-button |
|||
(click)="cancel()" |
|||
[disabled]="isImportData" |
|||
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> |
|||
<mat-vertical-stepper [linear]="true" [selectedIndex]="selectedIndex" #importStepper> |
|||
<mat-step [stepControl]="selectFileFormGroup"> |
|||
<form [formGroup]="selectFileFormGroup"> |
|||
<ng-template matStepLabel>{{ 'import.stepper-text.select-file' | translate }}</ng-template> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<tb-file-input formControlName="importData" |
|||
required |
|||
label="{{importFileLabel | translate}}" |
|||
[allowedExtensions]="'csv'" |
|||
[accept]="'.csv,application/csv,text/csv'" |
|||
dropLabel="{{'import.drop-file-csv' | translate}}"> |
|||
</tb-file-input> |
|||
</fieldset> |
|||
</form> |
|||
<div fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button |
|||
style="margin-right: 20px;" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()">{{ 'action.cancel' | translate }}</button> |
|||
<button mat-button mat-raised-button |
|||
[disabled]="(isLoading$ | async) || selectFileFormGroup.invalid || !selectFileFormGroup.dirty" |
|||
color="primary" |
|||
(click)="nextStep(2)">{{ 'action.continue' | translate }}</button> |
|||
</div> |
|||
</mat-step> |
|||
<mat-step [stepControl]="importParametersFormGroup"> |
|||
<form [formGroup]="importParametersFormGroup"> |
|||
<ng-template matStepLabel>{{ 'import.stepper-text.configuration' | translate }}</ng-template> |
|||
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>import.csv-delimiter</mat-label> |
|||
<mat-select required matInput formControlName="delim"> |
|||
<mat-option *ngFor="let delimiter of delimiters" [value]="delimiter.key"> |
|||
{{ delimiter.value }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<div fxLayout="column" fxLayoutGap="12px"> |
|||
<mat-checkbox formControlName="isHeader"> |
|||
{{ 'import.csv-first-line-header' | translate }} |
|||
</mat-checkbox> |
|||
<mat-checkbox formControlName="isUpdate"> |
|||
{{ 'import.csv-update-data' | translate }} |
|||
</mat-checkbox> |
|||
</div> |
|||
</fieldset> |
|||
</form> |
|||
<div fxLayout="row"> |
|||
<button mat-button |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="previousStep()">{{ 'action.back' | translate }}</button> |
|||
<span fxFlex></span> |
|||
<button mat-button |
|||
style="margin-right: 20px;" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()">{{ 'action.cancel' | translate }}</button> |
|||
<button mat-button mat-raised-button |
|||
[disabled]="(isLoading$ | async)" |
|||
color="primary" |
|||
(click)="nextStep(3)">{{ 'action.continue' | translate }}</button> |
|||
</div> |
|||
</mat-step> |
|||
<mat-step [stepControl]="columnTypesFormGroup"> |
|||
<form [formGroup]="columnTypesFormGroup"> |
|||
<ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template> |
|||
<tb-table-columns-assignment formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment> |
|||
</form> |
|||
<div fxLayout="row"> |
|||
<button mat-button |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="previousStep()">{{ 'action.back' | translate }}</button> |
|||
<span fxFlex></span> |
|||
<button mat-button |
|||
style="margin-right: 20px;" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()">{{ 'action.cancel' | translate }}</button> |
|||
<button mat-button mat-raised-button |
|||
[disabled]="(isLoading$ | async) || columnTypesFormGroup.invalid || !columnTypesFormGroup.dirty" |
|||
color="primary" |
|||
(click)="nextStep(4)">{{ 'action.continue' | translate }}</button> |
|||
</div> |
|||
</mat-step> |
|||
<mat-step> |
|||
<ng-template matStepLabel>{{ 'import.stepper-text.creat-entities' | translate }}</ng-template> |
|||
<mat-progress-bar color="warn" class="tb-import-progress" mode="determinate" [value]="progressCreate"> |
|||
</mat-progress-bar> |
|||
</mat-step> |
|||
<mat-step> |
|||
<ng-template matStepLabel>{{ 'import.stepper-text.done' | translate }}</ng-template> |
|||
<div fxLayout="column"> |
|||
<p class="mat-body-1" *ngIf="this.statistical?.create && this.statistical?.create.entity"> |
|||
{{ translate.instant('import.message.create-entities', {count: this.statistical.create.entity}) }} |
|||
</p> |
|||
<p class="mat-body-1" *ngIf="this.statistical?.update && this.statistical?.update.entity"> |
|||
{{ translate.instant('import.message.update-entities', {count: this.statistical.update.entity}) }} |
|||
</p> |
|||
<p class="mat-body-1" *ngIf="this.statistical?.error && this.statistical?.error.entity"> |
|||
{{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }} |
|||
</p> |
|||
</div> |
|||
<div fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button |
|||
[disabled]="(isLoading$ | async)" |
|||
color="primary" |
|||
(click)="nextStep(6)">{{ 'action.ok' | translate }}</button> |
|||
</div> |
|||
</mat-step> |
|||
</mat-vertical-stepper> |
|||
</div> |
|||
</form> |
|||
@ -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 { |
|||
.mat-dialog-content { |
|||
.mat-vertical-content { |
|||
form { |
|||
overflow: hidden !important; |
|||
padding-bottom: 12px; |
|||
} |
|||
} |
|||
.mat-vertical-stepper-header { |
|||
pointer-events: none !important; |
|||
} |
|||
.tb-import-progress{ |
|||
margin: 7px 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,256 @@ |
|||
///
|
|||
/// 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, ViewChild } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { MatVerticalStepper } from '@angular/material/stepper'; |
|||
import { |
|||
convertCSVToJson, |
|||
CsvColumnParam, |
|||
CsvToJsonConfig, |
|||
CsvToJsonResult, |
|||
ImportEntityColumnType |
|||
} from '@home/components/import-export/import-export.models'; |
|||
import { ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models'; |
|||
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
|||
|
|||
export interface ImportDialogCsvData { |
|||
entityType: EntityType; |
|||
importTitle: string; |
|||
importFileLabel: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-import-csv-dialog', |
|||
templateUrl: './import-dialog-csv.component.html', |
|||
providers: [], |
|||
styleUrls: ['./import-dialog-csv.component.scss'] |
|||
}) |
|||
export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean> |
|||
implements OnInit { |
|||
|
|||
@ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper; |
|||
|
|||
entityType: EntityType; |
|||
importTitle: string; |
|||
importFileLabel: string; |
|||
|
|||
delimiters: {key: string, value: string}[] = [{ |
|||
key: ',', |
|||
value: ',' |
|||
}, { |
|||
key: ';', |
|||
value: ';' |
|||
}, { |
|||
key: '|', |
|||
value: '|' |
|||
}, { |
|||
key: '\t', |
|||
value: 'Tab' |
|||
}]; |
|||
|
|||
selectedIndex = 0; |
|||
|
|||
selectFileFormGroup: FormGroup; |
|||
importParametersFormGroup: FormGroup; |
|||
columnTypesFormGroup: FormGroup; |
|||
|
|||
isImportData = false; |
|||
progressCreate = 0; |
|||
statistical: ImportEntitiesResultInfo; |
|||
|
|||
private parseData: CsvToJsonResult; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: ImportDialogCsvData, |
|||
public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>, |
|||
public translate: TranslateService, |
|||
private importExport: ImportExportService, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
this.entityType = data.entityType; |
|||
this.importTitle = data.importTitle; |
|||
this.importFileLabel = data.importFileLabel; |
|||
|
|||
this.selectFileFormGroup = this.fb.group( |
|||
{ |
|||
importData: [null, [Validators.required]] |
|||
} |
|||
); |
|||
this.importParametersFormGroup = this.fb.group({ |
|||
delim: [',', [Validators.required]], |
|||
isHeader: [true, []], |
|||
isUpdate: [true, []], |
|||
}); |
|||
this.columnTypesFormGroup = this.fb.group({ |
|||
columnsParam: [[], []] |
|||
}); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(false); |
|||
} |
|||
|
|||
previousStep() { |
|||
this.importStepper.previous(); |
|||
} |
|||
|
|||
nextStep(step: number) { |
|||
switch (step) { |
|||
case 2: |
|||
this.importStepper.next(); |
|||
break; |
|||
case 3: |
|||
const importData: string = this.selectFileFormGroup.get('importData').value; |
|||
const parseData = this.parseCSV(importData); |
|||
if (parseData === -1) { |
|||
this.importStepper.previous(); |
|||
this.importStepper.selected.reset(); |
|||
} else { |
|||
this.parseData = parseData as CsvToJsonResult; |
|||
const columnsParam = this.createColumnsData(); |
|||
this.columnTypesFormGroup.patchValue({columnsParam}, {emitEvent: true}); |
|||
this.importStepper.next(); |
|||
} |
|||
break; |
|||
case 4: |
|||
this.importStepper.next(); |
|||
this.isImportData = true; |
|||
this.addEntities(); |
|||
break; |
|||
case 6: |
|||
this.dialogRef.close(true); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private parseCSV(importData: string): CsvToJsonResult | number { |
|||
const config: CsvToJsonConfig = { |
|||
delim: this.importParametersFormGroup.get('delim').value, |
|||
header: this.importParametersFormGroup.get('isHeader').value |
|||
}; |
|||
return convertCSVToJson(importData, config, |
|||
(messageId, params) => { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{message: this.translate.instant(messageId, params), |
|||
type: 'error'})); |
|||
} |
|||
); |
|||
} |
|||
|
|||
private createColumnsData(): CsvColumnParam[] { |
|||
const columnsParam: CsvColumnParam[] = []; |
|||
const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value; |
|||
for (let i = 0; i < this.parseData.headers.length; i++) { |
|||
let columnParam: CsvColumnParam; |
|||
if (isHeader && this.parseData.headers[i].search(/^(name|type)$/im) === 0) { |
|||
columnParam = { |
|||
type: ImportEntityColumnType[this.parseData.headers[i].toLowerCase()], |
|||
key: this.parseData.headers[i].toLowerCase(), |
|||
sampleData: this.parseData.rows[0][i] |
|||
}; |
|||
} else { |
|||
columnParam = { |
|||
type: ImportEntityColumnType.serverAttribute, |
|||
key: isHeader ? this.parseData.headers[i] : '', |
|||
sampleData: this.parseData.rows[0][i] |
|||
}; |
|||
} |
|||
columnsParam.push(columnParam); |
|||
} |
|||
return columnsParam; |
|||
} |
|||
|
|||
private addEntities() { |
|||
const importData = this.parseData; |
|||
const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value; |
|||
const entitiesData: ImportEntityData[] = []; |
|||
let sentDataLength = 0; |
|||
for (let row = 0; row < importData.rows.length; row++) { |
|||
const entityData: ImportEntityData = { |
|||
name: '', |
|||
type: '', |
|||
accessToken: '', |
|||
attributes: { |
|||
server: [], |
|||
shared: [] |
|||
}, |
|||
timeseries: [] |
|||
}; |
|||
const i = row; |
|||
for (let j = 0; j < parameterColumns.length; j++) { |
|||
switch (parameterColumns[j].type) { |
|||
case ImportEntityColumnType.serverAttribute: |
|||
entityData.attributes.server.push({ |
|||
key: parameterColumns[j].key, |
|||
value: importData.rows[i][j] |
|||
}); |
|||
break; |
|||
case ImportEntityColumnType.timeseries: |
|||
entityData.timeseries.push({ |
|||
key: parameterColumns[j].key, |
|||
value: importData.rows[i][j] |
|||
}); |
|||
break; |
|||
case ImportEntityColumnType.sharedAttribute: |
|||
entityData.attributes.shared.push({ |
|||
key: parameterColumns[j].key, |
|||
value: importData.rows[i][j] |
|||
}); |
|||
break; |
|||
case ImportEntityColumnType.accessToken: |
|||
entityData.accessToken = importData.rows[i][j]; |
|||
break; |
|||
case ImportEntityColumnType.name: |
|||
entityData.name = importData.rows[i][j]; |
|||
break; |
|||
case ImportEntityColumnType.type: |
|||
entityData.type = importData.rows[i][j]; |
|||
break; |
|||
} |
|||
} |
|||
entitiesData.push(entityData); |
|||
} |
|||
const createImportEntityCompleted = () => { |
|||
sentDataLength++; |
|||
this.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100); |
|||
}; |
|||
|
|||
const isUpdate: boolean = this.importParametersFormGroup.get('isUpdate').value; |
|||
|
|||
this.importExport.importEntities(entitiesData, this.entityType, isUpdate, |
|||
createImportEntityCompleted, {ignoreErrors: true, resendRequest: true}).subscribe( |
|||
(result) => { |
|||
this.statistical = result; |
|||
this.isImportData = false; |
|||
this.importStepper.next(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<mat-table [dataSource]="dataSource"> |
|||
<ng-container matColumnDef="order"> |
|||
<mat-header-cell *matHeaderCellDef></mat-header-cell> |
|||
<mat-cell *matCellDef="let column; let i = index"> |
|||
{{ (i+1) }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="sampleData"> |
|||
<mat-header-cell *matHeaderCellDef> {{ 'import.column-example' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
{{column.sampleData}} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="type"> |
|||
<mat-header-cell *matHeaderCellDef> {{ 'import.column-type.column-type' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
<mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()"> |
|||
<mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled"> |
|||
{{ columnTypesTranslations.get(type.value) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="key"> |
|||
<mat-header-cell *matHeaderCellDef> {{ 'import.column-key' | translate }} </mat-header-cell> |
|||
<mat-cell *matCellDef="let column"> |
|||
<mat-form-field floatLabel="always" hideRequiredMarker |
|||
*ngIf="column.type !== importEntityColumnType.name && |
|||
column.type !== importEntityColumnType.type && |
|||
column.type !== importEntityColumnType.accessToken"> |
|||
<mat-label></mat-label> |
|||
<input matInput required |
|||
[(ngModel)]="column.key" (ngModelChange)="columnsUpdated()" |
|||
placeholder="{{ 'import.column-value' | translate }}"/> |
|||
</mat-form-field> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> |
|||
<mat-row *matRowDef="let column; columns: displayedColumns;"></mat-row> |
|||
</mat-table> |
|||
<mat-divider></mat-divider> |
|||
@ -0,0 +1,26 @@ |
|||
/** |
|||
* 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 { |
|||
.mat-column-order { |
|||
flex: 0 0 40px; |
|||
} |
|||
.mat-column-sampleData { |
|||
flex: 0 0 120px; |
|||
} |
|||
.mat-column-type { |
|||
flex: 0 0 120px; |
|||
} |
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
///
|
|||
/// 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 } from '@angular/core'; |
|||
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { CsvColumnParam, ImportEntityColumnType, importEntityColumnTypeTranslations, |
|||
importEntityObjectColumns } from '@home/components/import-export/import-export.models'; |
|||
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; |
|||
import { BehaviorSubject, Observable } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-table-columns-assignment', |
|||
templateUrl: './table-columns-assignment.component.html', |
|||
styleUrls: ['./table-columns-assignment.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => TableColumnsAssignmentComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => TableColumnsAssignmentComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class TableColumnsAssignmentComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
@Input() entityType: EntityType; |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
dataSource = new CsvColumnsDatasource(); |
|||
|
|||
displayedColumns = ['order', 'sampleData', 'type', 'key']; |
|||
|
|||
importEntityColumnType = ImportEntityColumnType; |
|||
|
|||
columnTypes: AssignmentColumnType[] = []; |
|||
|
|||
columnTypesTranslations = importEntityColumnTypeTranslations; |
|||
|
|||
private columns: CsvColumnParam[]; |
|||
|
|||
private valid = true; |
|||
|
|||
private propagateChangePending = false; |
|||
private propagateChange = null; |
|||
|
|||
constructor(public elementRef: ElementRef, |
|||
protected store: Store<AppState>) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.columnTypes.push( |
|||
{ value: ImportEntityColumnType.name }, |
|||
{ value: ImportEntityColumnType.type }, |
|||
); |
|||
switch (this.entityType) { |
|||
case EntityType.DEVICE: |
|||
this.columnTypes.push( |
|||
{ value: ImportEntityColumnType.sharedAttribute }, |
|||
{ value: ImportEntityColumnType.serverAttribute }, |
|||
{ value: ImportEntityColumnType.timeseries }, |
|||
{ value: ImportEntityColumnType.accessToken } |
|||
); |
|||
break; |
|||
case EntityType.ASSET: |
|||
this.columnTypes.push( |
|||
{ value: ImportEntityColumnType.serverAttribute }, |
|||
{ value: ImportEntityColumnType.timeseries } |
|||
); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
if (this.propagateChangePending) { |
|||
this.propagateChange(this.columns); |
|||
this.propagateChangePending = false; |
|||
} |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
columnsUpdated() { |
|||
const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1; |
|||
const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1; |
|||
const isSelectCredentials = this.columns.findIndex((column) => column.type === ImportEntityColumnType.accessToken) > -1; |
|||
const hasInvalidColumn = this.columns.findIndex((column) => !this.columnValid(column)) > -1; |
|||
|
|||
this.valid = isSelectName && isSelectType && !hasInvalidColumn; |
|||
|
|||
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.name).disabled = isSelectName; |
|||
this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.type).disabled = isSelectType; |
|||
const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken); |
|||
if (accessTokenColumnType) { |
|||
accessTokenColumnType.disabled = isSelectCredentials; |
|||
} |
|||
if (this.propagateChange) { |
|||
this.propagateChange(this.columns); |
|||
} else { |
|||
this.propagateChangePending = true; |
|||
} |
|||
} |
|||
|
|||
private columnValid(column: CsvColumnParam): boolean { |
|||
if (!importEntityObjectColumns.includes(column.type)) { |
|||
return column.key && column.key.trim().length > 0; |
|||
} else { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
public validate(c: FormControl) { |
|||
return (this.valid) ? null : { |
|||
columnsInvalid: true |
|||
}; |
|||
return null; |
|||
} |
|||
|
|||
writeValue(value: CsvColumnParam[]): void { |
|||
this.columns = value; |
|||
this.dataSource.setColumns(this.columns); |
|||
this.columnsUpdated(); |
|||
} |
|||
} |
|||
|
|||
interface AssignmentColumnType { |
|||
value: ImportEntityColumnType; |
|||
disabled?: boolean; |
|||
} |
|||
|
|||
class CsvColumnsDatasource implements DataSource<CsvColumnParam> { |
|||
|
|||
private columnsSubject = new BehaviorSubject<CsvColumnParam[]>([]); |
|||
|
|||
constructor() {} |
|||
|
|||
connect(collectionViewer: CollectionViewer): Observable<CsvColumnParam[] | ReadonlyArray<CsvColumnParam>> { |
|||
return this.columnsSubject.asObservable(); |
|||
} |
|||
|
|||
disconnect(collectionViewer: CollectionViewer): void { |
|||
this.columnsSubject.complete(); |
|||
} |
|||
|
|||
setColumns(columns: CsvColumnParam[]) { |
|||
this.columnsSubject.next(columns); |
|||
} |
|||
|
|||
} |
|||
@ -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.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { AuthService } from '@core/auth/auth.service'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { Observable } from 'rxjs'; |
|||
import { |
|||
ImportDialogCsvComponent, |
|||
ImportDialogCsvData |
|||
} from '@home/components/import-export/import-dialog-csv.component'; |
|||
|
|||
@Injectable() |
|||
export class HomeDialogsService { |
|||
constructor( |
|||
private dialog: MatDialog |
|||
) { |
|||
} |
|||
|
|||
public importEntities(entityType: EntityType): Observable<boolean> { |
|||
switch (entityType) { |
|||
case EntityType.DEVICE: |
|||
return this.openImportDialogCSV(entityType, 'device.import', 'device.device-file'); |
|||
case EntityType.ASSET: |
|||
return this.openImportDialogCSV(entityType, 'asset.import', 'asset.asset-file'); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private openImportDialogCSV(entityType: EntityType, importTitle: string, importFileLabel: string): Observable<boolean> { |
|||
return this.dialog.open<ImportDialogCsvComponent, ImportDialogCsvData, |
|||
any>(ImportDialogCsvComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
entityType, |
|||
importTitle, |
|||
importFileLabel |
|||
} |
|||
}).afterClosed(); |
|||
} |
|||
} |
|||
@ -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 #stateForm="ngForm" [formGroup]="stateFormGroup" (ngSubmit)="save()"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2 translate>dashboard.select-state</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"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>dashboard.state</mat-label> |
|||
<mat-select required matInput formControlName="stateId"> |
|||
<mat-option *ngFor="let stateItem of states | keyvalue" [value]="stateItem.key"> |
|||
{{ stateItem.value.name }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || stateFormGroup.invalid"> |
|||
{{ 'action.select' | 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,85 @@ |
|||
///
|
|||
/// 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 { DashboardState } from '@app/shared/models/dashboard.models'; |
|||
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
|||
import { DashboardService } from '@core/http/dashboard.service'; |
|||
|
|||
export interface SelectTargetStateDialogData { |
|||
states: {[id: string]: DashboardState }; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-select-target-state-dialog', |
|||
templateUrl: './select-target-state-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: SelectTargetStateDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class SelectTargetStateDialogComponent extends |
|||
DialogComponent<SelectTargetStateDialogComponent, string> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
states: {[id: string]: DashboardState }; |
|||
stateFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: SelectTargetStateDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<SelectTargetStateDialogComponent, string>, |
|||
private fb: FormBuilder, |
|||
private dashboardUtils: DashboardUtilsService) { |
|||
super(store, router, dialogRef); |
|||
|
|||
this.states = this.data.states; |
|||
|
|||
this.stateFormGroup = this.fb.group( |
|||
{ |
|||
stateId: [this.dashboardUtils.getRootStateId(this.states), [Validators.required]] |
|||
} |
|||
); |
|||
} |
|||
|
|||
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; |
|||
const stateId: string = this.stateFormGroup.get('stateId').value; |
|||
this.dialogRef.close(stateId); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue