26 changed files with 1077 additions and 195 deletions
@ -0,0 +1,69 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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()" style="min-width: 600px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>{{ isAdd ? 'dashboard.add-state' : 'dashboard.edit-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-name</mat-label> |
||||
|
<input required matInput formControlName="name"> |
||||
|
<mat-error *ngIf="stateFormGroup.get('name').hasError('required')"> |
||||
|
{{ 'dashboard.state-name-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>dashboard.state-id</mat-label> |
||||
|
<input required matInput formControlName="id"> |
||||
|
<mat-error *ngIf="stateFormGroup.get('id').hasError('required')"> |
||||
|
{{ 'dashboard.state-id-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="stateFormGroup.get('id').hasError('stateExists')"> |
||||
|
{{ 'dashboard.state-id-exists' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-checkbox formControlName="root"> |
||||
|
{{ 'dashboard.is-root-state' | translate }} |
||||
|
</mat-checkbox> |
||||
|
</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 || !stateFormGroup.dirty"> |
||||
|
{{ (isAdd ? 'action.add' : '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,145 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
ValidatorFn, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { DashboardState } from '@app/shared/models/dashboard.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { DashboardStateInfo } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
||||
|
|
||||
|
export interface DashboardStateDialogData { |
||||
|
states: {[id: string]: DashboardState }; |
||||
|
state: DashboardStateInfo; |
||||
|
isAdd: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dashboard-state-dialog', |
||||
|
templateUrl: './dashboard-state-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class DashboardStateDialogComponent extends |
||||
|
DialogComponent<DashboardStateDialogComponent, DashboardStateInfo> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
stateFormGroup: FormGroup; |
||||
|
|
||||
|
states: {[id: string]: DashboardState }; |
||||
|
state: DashboardStateInfo; |
||||
|
prevStateId: string; |
||||
|
|
||||
|
stateIdTouched: boolean; |
||||
|
|
||||
|
isAdd: boolean; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: DashboardStateDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<DashboardStateDialogComponent, DashboardStateInfo>, |
||||
|
private fb: FormBuilder, |
||||
|
private translate: TranslateService, |
||||
|
private dashboardUtils: DashboardUtilsService, |
||||
|
private dialog: MatDialog) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.states = this.data.states; |
||||
|
this.isAdd = this.data.isAdd; |
||||
|
if (this.isAdd) { |
||||
|
this.state = {id: '', ...this.dashboardUtils.createDefaultState('', false)}; |
||||
|
this.prevStateId = ''; |
||||
|
} else { |
||||
|
this.state = this.data.state; |
||||
|
this.prevStateId = this.state.id; |
||||
|
} |
||||
|
|
||||
|
this.stateFormGroup = this.fb.group({ |
||||
|
name: [this.state.name, [Validators.required]], |
||||
|
id: [this.state.id, [Validators.required, this.validateDuplicateStateId()]], |
||||
|
root: [this.state.root, []], |
||||
|
}); |
||||
|
|
||||
|
this.stateFormGroup.get('name').valueChanges.subscribe((name: string) => { |
||||
|
this.checkStateName(name); |
||||
|
}); |
||||
|
|
||||
|
this.stateFormGroup.get('id').valueChanges.subscribe((id: string) => { |
||||
|
this.stateIdTouched = true; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private checkStateName(name: string) { |
||||
|
if (name && !this.stateIdTouched && this.isAdd) { |
||||
|
this.stateFormGroup.get('id').setValue( |
||||
|
name.toLowerCase().replace(/\W/g, '_'), |
||||
|
{ emitEvent: false } |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private validateDuplicateStateId(): ValidatorFn { |
||||
|
return (c: FormControl) => { |
||||
|
const newStateId: string = c.value; |
||||
|
if (newStateId) { |
||||
|
const existing = this.states[newStateId]; |
||||
|
if (existing && newStateId !== this.prevStateId) { |
||||
|
return { |
||||
|
stateExists: true |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
this.state = {...this.state, ...this.stateFormGroup.value}; |
||||
|
this.state.id = this.state.id.trim(); |
||||
|
this.dialogRef.close(this.state); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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 #statesForm="ngForm" [formGroup]="statesFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>dashboard.manage-states</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"> |
||||
|
<div class="manage-dashboard-states" fxLayout="column"> |
||||
|
<div class="tb-entity-table"> |
||||
|
<div fxLayout="column" class="tb-entity-table-content"> |
||||
|
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<span class="tb-entity-table-title" translate>dashboard.states</span> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
type="button" |
||||
|
(click)="addState($event)" |
||||
|
matTooltip="{{ 'dashboard.add-state' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>add</mat-icon> |
||||
|
</button> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'action.search' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<button mat-button mat-icon-button |
||||
|
type="button" |
||||
|
matTooltip="{{ 'dashboard.search-states' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label> </mat-label> |
||||
|
<input #searchInput matInput |
||||
|
[(ngModel)]="pageLink.textSearch" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
placeholder="{{ 'dashboard.search-states' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<button mat-button mat-icon-button (click)="exitFilterMode()" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'action.close' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<div class="table-container"> |
||||
|
<mat-table [dataSource]="dataSource" |
||||
|
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
||||
|
<ng-container matColumnDef="name"> |
||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.state-name' | translate }} </mat-header-cell> |
||||
|
<mat-cell *matCellDef="let state"> |
||||
|
{{ state.name }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="id"> |
||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.state-id' | translate }} </mat-header-cell> |
||||
|
<mat-cell *matCellDef="let state"> |
||||
|
{{ state.id }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="root"> |
||||
|
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.is-root-state' | translate }} </mat-header-cell> |
||||
|
<mat-cell *matCellDef="let state"> |
||||
|
<mat-icon class="material-icons mat-icon">{{state.root ? 'check_box' : 'check_box_outline_blank'}}</mat-icon> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let state" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> |
||||
|
<div fxFlex fxLayout="row"> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'dashboard.edit-state' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="editState($event, state)"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button [fxShow]="!state.root" mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'dashboard.delete-state' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="deleteState($event, state)"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row class="mat-row-select" |
||||
|
*matRowDef="let state; columns: displayedColumns;"></mat-row> |
||||
|
</mat-table> |
||||
|
<span [fxShow]="dataSource.isEmpty() | async" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found" translate>{{ 'dashboard.no-states-text' }}</span> |
||||
|
</div> |
||||
|
<mat-divider></mat-divider> |
||||
|
<mat-paginator [length]="dataSource.total() | async" |
||||
|
[pageIndex]="pageLink.page" |
||||
|
[pageSize]="pageLink.pageSize" |
||||
|
[pageSizeOptions]="[5, 10, 15]"></mat-paginator> |
||||
|
</div> |
||||
|
</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) || statesFormGroup.invalid || !statesFormGroup.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,100 @@ |
|||||
|
///
|
||||
|
/// 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 { DashboardState } from '@shared/models/dashboard.models'; |
||||
|
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; |
||||
|
import { WidgetActionDescriptorInfo } from '@home/components/widget/action/manage-widget-actions.component.models'; |
||||
|
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { catchError, map, publishReplay, refCount } from 'rxjs/operators'; |
||||
|
|
||||
|
export interface DashboardStateInfo extends DashboardState { |
||||
|
id: string; |
||||
|
} |
||||
|
|
||||
|
export class DashboardStatesDatasource implements DataSource<DashboardStateInfo> { |
||||
|
|
||||
|
private statesSubject = new BehaviorSubject<DashboardStateInfo[]>([]); |
||||
|
private pageDataSubject = new BehaviorSubject<PageData<DashboardStateInfo>>(emptyPageData<DashboardStateInfo>()); |
||||
|
|
||||
|
public pageData$ = this.pageDataSubject.asObservable(); |
||||
|
|
||||
|
private allStates: Observable<Array<DashboardStateInfo>>; |
||||
|
|
||||
|
constructor(private states: {[id: string]: DashboardState }) { |
||||
|
} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<DashboardStateInfo[] | ReadonlyArray<DashboardStateInfo>> { |
||||
|
return this.statesSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.statesSubject.complete(); |
||||
|
this.pageDataSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadStates(pageLink: PageLink, reload: boolean = false): Observable<PageData<DashboardStateInfo>> { |
||||
|
if (reload) { |
||||
|
this.allStates = null; |
||||
|
} |
||||
|
const result = new ReplaySubject<PageData<DashboardStateInfo>>(); |
||||
|
this.fetchStates(pageLink).pipe( |
||||
|
catchError(() => of(emptyPageData<DashboardStateInfo>())), |
||||
|
).subscribe( |
||||
|
(pageData) => { |
||||
|
this.statesSubject.next(pageData.data); |
||||
|
this.pageDataSubject.next(pageData); |
||||
|
result.next(pageData); |
||||
|
} |
||||
|
); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
fetchStates(pageLink: PageLink): Observable<PageData<DashboardStateInfo>> { |
||||
|
return this.getAllStates().pipe( |
||||
|
map((data) => pageLink.filterData(data)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getAllStates(): Observable<Array<DashboardStateInfo>> { |
||||
|
if (!this.allStates) { |
||||
|
const states: DashboardStateInfo[] = []; |
||||
|
for (const id of Object.keys(this.states)) { |
||||
|
const state = this.states[id]; |
||||
|
states.push({id, ...state}); |
||||
|
} |
||||
|
this.allStates = of(states).pipe( |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} |
||||
|
return this.allStates; |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.statesSubject.pipe( |
||||
|
map((states) => !states.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.pageDataSubject.pipe( |
||||
|
map((pageData) => pageData.totalElements) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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. |
||||
|
*/ |
||||
|
:host { |
||||
|
.manage-dashboard-states { |
||||
|
.tb-entity-table { |
||||
|
.tb-entity-table-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: #fff; |
||||
|
|
||||
|
.tb-entity-table-title { |
||||
|
padding-right: 20px; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.table-container { |
||||
|
overflow: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.manage-dashboard-states { |
||||
|
.mat-sort-header-sorted .mat-sort-header-arrow { |
||||
|
opacity: 1 !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,244 @@ |
|||||
|
///
|
||||
|
/// 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 { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } 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 { DashboardState } from '@app/shared/models/dashboard.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { |
||||
|
WidgetActionDescriptorInfo, |
||||
|
WidgetActionsDatasource |
||||
|
} from '@home/components/widget/action/manage-widget-actions.component.models'; |
||||
|
import { |
||||
|
DashboardStateInfo, |
||||
|
DashboardStatesDatasource |
||||
|
} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { MatPaginator } from '@angular/material/paginator'; |
||||
|
import { MatSort } from '@angular/material/sort'; |
||||
|
import { fromEvent, merge } from 'rxjs'; |
||||
|
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { |
||||
|
WidgetActionDialogComponent, |
||||
|
WidgetActionDialogData |
||||
|
} from '@home/components/widget/action/widget-action-dialog.component'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
import { |
||||
|
DashboardStateDialogComponent, |
||||
|
DashboardStateDialogData |
||||
|
} from '@home/pages/dashboard/states/dashboard-state-dialog.component'; |
||||
|
|
||||
|
export interface ManageDashboardStatesDialogData { |
||||
|
states: {[id: string]: DashboardState }; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-manage-dashboard-states-dialog', |
||||
|
templateUrl: './manage-dashboard-states-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}], |
||||
|
styleUrls: ['./manage-dashboard-states-dialog.component.scss'] |
||||
|
}) |
||||
|
export class ManageDashboardStatesDialogComponent extends |
||||
|
DialogComponent<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }> |
||||
|
implements OnInit, ErrorStateMatcher, AfterViewInit { |
||||
|
|
||||
|
statesFormGroup: FormGroup; |
||||
|
|
||||
|
states: {[id: string]: DashboardState }; |
||||
|
|
||||
|
displayedColumns: string[]; |
||||
|
pageLink: PageLink; |
||||
|
textSearchMode = false; |
||||
|
dataSource: DashboardStatesDatasource; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
||||
|
|
||||
|
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; |
||||
|
@ViewChild(MatSort, {static: false}) sort: MatSort; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }>, |
||||
|
private fb: FormBuilder, |
||||
|
private translate: TranslateService, |
||||
|
private dialogs: DialogService, |
||||
|
private dialog: MatDialog) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.states = this.data.states; |
||||
|
this.statesFormGroup = this.fb.group({}); |
||||
|
|
||||
|
const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; |
||||
|
this.pageLink = new PageLink(5, 0, null, sortOrder); |
||||
|
this.displayedColumns = ['name', 'id', 'root', 'actions']; |
||||
|
this.dataSource = new DashboardStatesDatasource(this.states); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.dataSource.loadStates(this.pageLink); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
fromEvent(this.searchInputField.nativeElement, 'keyup') |
||||
|
.pipe( |
||||
|
debounceTime(150), |
||||
|
distinctUntilChanged(), |
||||
|
tap(() => { |
||||
|
this.paginator.pageIndex = 0; |
||||
|
this.updateData(); |
||||
|
}) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
|
||||
|
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); |
||||
|
|
||||
|
merge(this.sort.sortChange, this.paginator.page) |
||||
|
.pipe( |
||||
|
tap(() => this.updateData()) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
} |
||||
|
|
||||
|
updateData(reload: boolean = false) { |
||||
|
this.pageLink.page = this.paginator.pageIndex; |
||||
|
this.pageLink.pageSize = this.paginator.pageSize; |
||||
|
this.pageLink.sortOrder.property = this.sort.active; |
||||
|
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
||||
|
this.dataSource.loadStates(this.pageLink, reload); |
||||
|
} |
||||
|
|
||||
|
addState($event: Event) { |
||||
|
this.openStateDialog($event); |
||||
|
} |
||||
|
|
||||
|
editState($event: Event, state: DashboardStateInfo) { |
||||
|
this.openStateDialog($event, state); |
||||
|
} |
||||
|
|
||||
|
deleteState($event: Event, state: DashboardStateInfo) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const title = this.translate.instant('dashboard.delete-state-title'); |
||||
|
const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name}); |
||||
|
this.dialogs.confirm(title, content, this.translate.instant('action.no'), |
||||
|
this.translate.instant('action.yes')).subscribe( |
||||
|
(res) => { |
||||
|
if (res) { |
||||
|
delete this.states[state.id]; |
||||
|
this.onStatesUpdated(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
enterFilterMode() { |
||||
|
this.textSearchMode = true; |
||||
|
this.pageLink.textSearch = ''; |
||||
|
setTimeout(() => { |
||||
|
this.searchInputField.nativeElement.focus(); |
||||
|
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
||||
|
}, 10); |
||||
|
} |
||||
|
|
||||
|
exitFilterMode() { |
||||
|
this.textSearchMode = false; |
||||
|
this.pageLink.textSearch = null; |
||||
|
this.paginator.pageIndex = 0; |
||||
|
this.updateData(); |
||||
|
} |
||||
|
|
||||
|
openStateDialog($event: Event, state: DashboardStateInfo = null) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const isAdd = state === null; |
||||
|
let prevStateId = null; |
||||
|
if (!isAdd) { |
||||
|
prevStateId = state.id; |
||||
|
} |
||||
|
this.dialog.open<DashboardStateDialogComponent, DashboardStateDialogData, |
||||
|
DashboardStateInfo>(DashboardStateDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
isAdd, |
||||
|
states: this.states, |
||||
|
state: deepClone(state) |
||||
|
} |
||||
|
}).afterClosed().subscribe( |
||||
|
(res) => { |
||||
|
if (res) { |
||||
|
this.saveState(res, prevStateId); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
saveState(state: DashboardStateInfo, prevStateId: string) { |
||||
|
const newState: DashboardState = { |
||||
|
name: state.name, |
||||
|
root: state.root, |
||||
|
layouts: state.layouts |
||||
|
}; |
||||
|
if (prevStateId) { |
||||
|
this.states[prevStateId] = newState; |
||||
|
} else { |
||||
|
this.states[state.id] = newState; |
||||
|
} |
||||
|
if (state.root) { |
||||
|
for (const id of Object.keys(this.states)) { |
||||
|
const otherState = this.states[id]; |
||||
|
if (id !== state.id) { |
||||
|
otherState.root = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.onStatesUpdated(); |
||||
|
} |
||||
|
|
||||
|
private onStatesUpdated() { |
||||
|
this.statesFormGroup.markAsDirty(); |
||||
|
this.updateData(true); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
this.dialogRef.close(this.states); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue