146 changed files with 23290 additions and 1178 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.alarm; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.TimePageLink; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@Builder |
||||
|
@AllArgsConstructor |
||||
|
public class AlarmQueryV2 { |
||||
|
|
||||
|
private EntityId affectedEntityId; |
||||
|
private TimePageLink pageLink; |
||||
|
private List<String> typeList; |
||||
|
private List<AlarmSearchStatus> statusList; |
||||
|
private List<AlarmSeverity> severityList; |
||||
|
private UserId assigneeId; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
||||
|
import { UsageInfo } from '@shared/models/usage.models'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class UsageInfoService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) {} |
||||
|
|
||||
|
public getUsageInfo(config?: RequestConfig): Observable<UsageInfo> { |
||||
|
return this.http.get<UsageInfo>('/api/usage', |
||||
|
defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,203 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
Inject, |
||||
|
InjectionToken, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { FormBuilder, FormGroup } from '@angular/forms'; |
||||
|
import { Observable, of, Subject } from 'rxjs'; |
||||
|
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, takeUntil, } from 'rxjs/operators'; |
||||
|
import { User, UserEmailInfo } from '@shared/models/user.model'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { UserService } from '@core/http/user.service'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Direction } from '@shared/models/page/sort-order'; |
||||
|
import { emptyPageData } from '@shared/models/page/page-data'; |
||||
|
import { OverlayRef } from '@angular/cdk/overlay'; |
||||
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
|
||||
|
export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData'); |
||||
|
|
||||
|
export interface AlarmAssigneeSelectPanelData { |
||||
|
assigneeId?: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-assignee-select-panel', |
||||
|
templateUrl: './alarm-assignee-panel.component.html', |
||||
|
styleUrls: ['./alarm-assignee-panel.component.scss'] |
||||
|
}) |
||||
|
export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
assigneeId?: string; |
||||
|
|
||||
|
assigneeNotSetText = 'alarm.assignee-not-set'; |
||||
|
|
||||
|
selectUserFormGroup: FormGroup; |
||||
|
|
||||
|
@ViewChild('userInput', {static: true}) userInput: ElementRef; |
||||
|
|
||||
|
filteredUsers: Observable<Array<UserEmailInfo>>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
userSelected = false; |
||||
|
|
||||
|
result?: UserEmailInfo; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
|
||||
|
constructor(@Inject(ALARM_ASSIGNEE_SELECT_PANEL_DATA) public data: AlarmAssigneeSelectPanelData, |
||||
|
public overlayRef: OverlayRef, |
||||
|
public translate: TranslateService, |
||||
|
private userService: UserService, |
||||
|
private fb: FormBuilder, |
||||
|
private utilsService: UtilsService) { |
||||
|
this.assigneeId = data.assigneeId; |
||||
|
this.selectUserFormGroup = this.fb.group({ |
||||
|
user: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges |
||||
|
.pipe( |
||||
|
debounceTime(150), |
||||
|
map(value => value ? (typeof value === 'string' ? value : '') : ''), |
||||
|
distinctUntilChanged(), |
||||
|
switchMap(name => this.fetchUsers(name)), |
||||
|
share(), |
||||
|
takeUntil(this.destroy$) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
setTimeout(() => { |
||||
|
this.userInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
displayUserFn(user?: User): string | undefined { |
||||
|
return user ? user.email : undefined; |
||||
|
} |
||||
|
|
||||
|
selected(event: MatAutocompleteSelectedEvent): void { |
||||
|
this.clear(); |
||||
|
this.userSelected = true; |
||||
|
this.result = event.option.value; |
||||
|
this.overlayRef.dispose(); |
||||
|
} |
||||
|
|
||||
|
fetchUsers(searchText?: string): Observable<Array<UserEmailInfo>> { |
||||
|
this.searchText = searchText; |
||||
|
const pageLink = new PageLink(50, 0, searchText, { |
||||
|
property: 'email', |
||||
|
direction: Direction.ASC |
||||
|
}); |
||||
|
return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) |
||||
|
.pipe( |
||||
|
catchError(() => of(emptyPageData<UserEmailInfo>())), |
||||
|
map(pageData => pageData.data) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onFocus(): void { |
||||
|
if (!this.dirty) { |
||||
|
this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.userInput.nativeElement.blur(); |
||||
|
this.userInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
getUserDisplayName(entity: UserEmailInfo) { |
||||
|
let displayName = ''; |
||||
|
if ((entity.firstName && entity.firstName.length > 0) || |
||||
|
(entity.lastName && entity.lastName.length > 0)) { |
||||
|
if (entity.firstName) { |
||||
|
displayName += entity.firstName; |
||||
|
} |
||||
|
if (entity.lastName) { |
||||
|
if (displayName.length > 0) { |
||||
|
displayName += ' '; |
||||
|
} |
||||
|
displayName += entity.lastName; |
||||
|
} |
||||
|
} else { |
||||
|
displayName = entity.email; |
||||
|
} |
||||
|
return displayName; |
||||
|
} |
||||
|
|
||||
|
getUserInitials(entity: UserEmailInfo): string { |
||||
|
let initials = ''; |
||||
|
if (entity.firstName && entity.firstName.length || |
||||
|
entity.lastName && entity.lastName.length) { |
||||
|
if (entity.firstName) { |
||||
|
initials += entity.firstName.charAt(0); |
||||
|
} |
||||
|
if (entity.lastName) { |
||||
|
initials += entity.lastName.charAt(0); |
||||
|
} |
||||
|
} else { |
||||
|
initials += entity.email.charAt(0); |
||||
|
} |
||||
|
return initials.toUpperCase(); |
||||
|
} |
||||
|
|
||||
|
getFullName(entity: UserEmailInfo): string { |
||||
|
let fullName = ''; |
||||
|
if ((entity.firstName && entity.firstName.length > 0) || |
||||
|
(entity.lastName && entity.lastName.length > 0)) { |
||||
|
if (entity.firstName) { |
||||
|
fullName += entity.firstName; |
||||
|
} |
||||
|
if (entity.lastName) { |
||||
|
if (fullName.length > 0) { |
||||
|
fullName += ' '; |
||||
|
} |
||||
|
fullName += entity.lastName; |
||||
|
} |
||||
|
} |
||||
|
return fullName; |
||||
|
} |
||||
|
|
||||
|
getAvatarBgColor(entity: UserEmailInfo) { |
||||
|
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<ng-container [formGroup]="assigneeFormGroup"> |
||||
|
<mat-form-field fxFlex class="mat-block" style="margin-bottom: 25px" |
||||
|
(click)="openAlarmAssigneeSelectPanel($event)" |
||||
|
subscriptSizing="dynamic"> |
||||
|
<mat-label translate>alarm.assignee</mat-label> |
||||
|
<input matInput readonly formControlName="assignee"> |
||||
|
<span *ngIf="assignee" matPrefix class="user-avatar" |
||||
|
[style.backgroundColor]="getAvatarBgColor()"> |
||||
|
{{ getUserInitials() }} |
||||
|
</span> |
||||
|
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon">account_circle</mat-icon> |
||||
|
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</ng-container> |
||||
@ -0,0 +1,193 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Injector, Input, OnInit, StaticProvider, ViewContainerRef } from '@angular/core'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
||||
|
import { ComponentPortal } from '@angular/cdk/portal'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { UserId } from '@shared/models/id/user-id'; |
||||
|
import { UserService } from '@core/http/user.service'; |
||||
|
import { User, UserEmailInfo } from '@shared/models/user.model'; |
||||
|
import { catchError, map, of } from 'rxjs'; |
||||
|
import { tap } from 'rxjs/operators'; |
||||
|
import { |
||||
|
ALARM_ASSIGNEE_SELECT_PANEL_DATA, |
||||
|
AlarmAssigneeSelectPanelComponent, |
||||
|
AlarmAssigneeSelectPanelData |
||||
|
} from '@home/components/alarm/alarm-assignee-select-panel.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-assignee-select', |
||||
|
templateUrl: './alarm-assignee-select.component.html', |
||||
|
styleUrls: ['./alarm-assignee.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmAssigneeSelectComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
assigneeFormGroup: UntypedFormGroup; |
||||
|
assignee?: User | UserEmailInfo; |
||||
|
|
||||
|
private propagateChange = (_: any) => {}; |
||||
|
|
||||
|
constructor(private utilsService: UtilsService, |
||||
|
private overlay: Overlay, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private userService: UserService, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private translateService: TranslateService) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.assigneeFormGroup = this.fb.group({ |
||||
|
assignee: [null, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.assigneeFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.assigneeFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(userId?: UserId): void { |
||||
|
const userObservable = userId ? this.userService.getUser(userId.id, {ignoreErrors: true}).pipe( |
||||
|
catchError(() => of(null)) |
||||
|
) : of(null); |
||||
|
userObservable.pipe( |
||||
|
tap((user) => { |
||||
|
this.assignee = user; |
||||
|
}), |
||||
|
map((user) => this.getAssignee(user)) |
||||
|
).subscribe((assignee) => { |
||||
|
this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private getAssignee(user?: User| UserEmailInfo): string { |
||||
|
if (user) { |
||||
|
return this.getUserDisplayName(user); |
||||
|
} else { |
||||
|
return this.translateService.instant('alarm.assignee-not-set'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private getUserDisplayName(user?: User | UserEmailInfo): string { |
||||
|
let displayName = ''; |
||||
|
if ((user?.firstName && user?.firstName.length > 0) || |
||||
|
(user?.lastName && user?.lastName.length > 0)) { |
||||
|
if (user?.firstName) { |
||||
|
displayName += user?.firstName; |
||||
|
} |
||||
|
if (user?.lastName) { |
||||
|
if (displayName.length > 0) { |
||||
|
displayName += ' '; |
||||
|
} |
||||
|
displayName += user?.lastName; |
||||
|
} |
||||
|
} else { |
||||
|
displayName = user?.email; |
||||
|
} |
||||
|
return displayName; |
||||
|
} |
||||
|
|
||||
|
getUserInitials(): string { |
||||
|
let initials = ''; |
||||
|
if (this.assignee?.firstName && this.assignee?.firstName.length || |
||||
|
this.assignee?.lastName && this.assignee?.lastName.length) { |
||||
|
if (this.assignee?.firstName) { |
||||
|
initials += this.assignee?.firstName.charAt(0); |
||||
|
} |
||||
|
if (this.assignee?.lastName) { |
||||
|
initials += this.assignee?.lastName.charAt(0); |
||||
|
} |
||||
|
} else { |
||||
|
initials += this.assignee?.email.charAt(0); |
||||
|
} |
||||
|
return initials.toUpperCase(); |
||||
|
} |
||||
|
|
||||
|
getAvatarBgColor(): string { |
||||
|
return this.utilsService.stringToHslColor(this.getUserDisplayName(this.assignee), 40, 60); |
||||
|
} |
||||
|
|
||||
|
openAlarmAssigneeSelectPanel($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
if (!this.disabled) { |
||||
|
const target = $event.currentTarget; |
||||
|
const config = new OverlayConfig(); |
||||
|
config.backdropClass = 'cdk-overlay-transparent-backdrop'; |
||||
|
config.hasBackdrop = true; |
||||
|
const connectedPosition: ConnectedPosition = { |
||||
|
originX: 'center', |
||||
|
originY: 'bottom', |
||||
|
overlayX: 'center', |
||||
|
overlayY: 'top' |
||||
|
}; |
||||
|
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) |
||||
|
.withPositions([connectedPosition]); |
||||
|
config.width = (target as HTMLElement).offsetWidth; |
||||
|
const overlayRef = this.overlay.create(config); |
||||
|
overlayRef.backdropClick().subscribe(() => { |
||||
|
overlayRef.dispose(); |
||||
|
}); |
||||
|
const providers: StaticProvider[] = [ |
||||
|
{ |
||||
|
provide: ALARM_ASSIGNEE_SELECT_PANEL_DATA, |
||||
|
useValue: { |
||||
|
assigneeId: this.assignee?.id?.id |
||||
|
} as AlarmAssigneeSelectPanelData |
||||
|
}, |
||||
|
{ |
||||
|
provide: OverlayRef, |
||||
|
useValue: overlayRef |
||||
|
} |
||||
|
]; |
||||
|
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); |
||||
|
const component = overlayRef.attach(new ComponentPortal(AlarmAssigneeSelectPanelComponent, |
||||
|
this.viewContainerRef, injector)); |
||||
|
component.onDestroy(() => { |
||||
|
if (component.instance.userSelected) { |
||||
|
this.assignee = component.instance.result; |
||||
|
this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false}); |
||||
|
this.propagateChange(this.assignee?.id); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,103 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<ng-container *ngIf="panelMode; else componentMode"> |
||||
|
<ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container> |
||||
|
</ng-container> |
||||
|
<ng-template #componentMode> |
||||
|
<ng-container *ngIf="buttonMode; else alarmFilter"> |
||||
|
<button color="primary" |
||||
|
matTooltip="{{buttonDisplayValue}}" |
||||
|
matTooltipPosition="above" |
||||
|
mat-stroked-button |
||||
|
(click)="toggleAlarmFilterPanel($event)"> |
||||
|
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}} |
||||
|
</button> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template #alarmFilterPanel> |
||||
|
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()"> |
||||
|
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container> |
||||
|
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> |
||||
|
<button type="button" |
||||
|
mat-button |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button type="submit" |
||||
|
mat-raised-button |
||||
|
color="primary" |
||||
|
[disabled]="alarmFilterConfigForm.invalid || !alarmFilterConfigForm.dirty"> |
||||
|
{{ 'action.update' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
<ng-template #alarmFilter> |
||||
|
<div fxLayout="column" fxLayoutAlign="center" [formGroup]="alarmFilterConfigForm"> |
||||
|
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start" |
||||
|
fxLayoutGap.gt-xs="8px"> |
||||
|
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
||||
|
<mat-label translate>alarm.alarm-status-list</mat-label> |
||||
|
<mat-select formControlName="statusList" multiple |
||||
|
placeholder="{{ !alarmFilterConfigForm.get('statusList').value?.length ? ('alarm.any-status' | translate) : '' }}"> |
||||
|
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus"> |
||||
|
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
||||
|
<mat-label translate>alarm.alarm-severity-list</mat-label> |
||||
|
<mat-select formControlName="severityList" multiple |
||||
|
placeholder="{{ !alarmFilterConfigForm.get('severityList').value?.length ? ('alarm.any-severity' | translate) : '' }}"> |
||||
|
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> |
||||
|
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start" |
||||
|
fxLayoutGap.gt-xs="8px"> |
||||
|
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
||||
|
<mat-label translate>alarm.alarm-type-list</mat-label> |
||||
|
<mat-chip-grid #alarmTypeChipList formControlName="typeList"> |
||||
|
<mat-chip-row *ngFor="let type of alarmTypeList()" |
||||
|
[removable]="true" (removed)="removeAlarmType(type)"> |
||||
|
{{type}} |
||||
|
<mat-icon matChipRemove>cancel</mat-icon> |
||||
|
</mat-chip-row> |
||||
|
<input placeholder="{{ !alarmFilterConfigForm.get('typeList').value?.length ? ('alarm.any-type' | translate) : '' }}" |
||||
|
[matChipInputFor]="alarmTypeChipList" |
||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
||||
|
matChipInputAddOnBlur |
||||
|
(matChipInputTokenEnd)="addAlarmType($event)"> |
||||
|
</mat-chip-grid> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div fxLayout="column" fxLayoutGap="16px"> |
||||
|
<mat-slide-toggle *ngIf="propagatedFilter" formControlName="searchPropagatedAlarms"> |
||||
|
{{ 'alarm.search-propagated-alarms' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<mat-slide-toggle formControlName="assignedToCurrentUser"> |
||||
|
{{ (userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user') | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<tb-alarm-assignee-select |
||||
|
*ngIf="!alarmFilterConfigForm.get('assignedToCurrentUser').value" formControlName="assigneeId"> |
||||
|
</tb-alarm-assignee-select> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -0,0 +1,318 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Inject, |
||||
|
InjectionToken, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Optional, |
||||
|
TemplateRef, |
||||
|
ViewChild, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
||||
|
import { AlarmFilterConfig } from '@shared/models/query/query.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coerce-boolean'; |
||||
|
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; |
||||
|
import { TemplatePortal } from '@angular/cdk/portal'; |
||||
|
import { |
||||
|
AlarmSearchStatus, |
||||
|
alarmSearchStatusTranslations, |
||||
|
AlarmSeverity, |
||||
|
alarmSeverityTranslations |
||||
|
} from '@shared/models/alarm.models'; |
||||
|
import { MatChipInputEvent } from '@angular/material/chips'; |
||||
|
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
export const ALARM_FILTER_CONFIG_DATA = new InjectionToken<any>('AlarmFilterConfigData'); |
||||
|
|
||||
|
export interface AlarmFilterConfigData { |
||||
|
panelMode: boolean; |
||||
|
userMode: boolean; |
||||
|
alarmFilterConfig: AlarmFilterConfig; |
||||
|
} |
||||
|
|
||||
|
// @dynamic
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-filter-config', |
||||
|
templateUrl: './alarm-filter-config.component.html', |
||||
|
styleUrls: ['./alarm-filter-config.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmFilterConfigComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { |
||||
|
|
||||
|
@ViewChild('alarmFilterPanel') |
||||
|
alarmFilterPanel: TemplateRef<any>; |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() |
||||
|
buttonMode = true; |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() |
||||
|
userMode = false; |
||||
|
|
||||
|
@coerceBoolean() |
||||
|
@Input() |
||||
|
propagatedFilter = true; |
||||
|
|
||||
|
panelMode = false; |
||||
|
|
||||
|
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
||||
|
|
||||
|
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, |
||||
|
AlarmSearchStatus.CLEARED, |
||||
|
AlarmSearchStatus.ACK, |
||||
|
AlarmSearchStatus.UNACK]; |
||||
|
|
||||
|
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; |
||||
|
|
||||
|
alarmSeverities = Object.keys(AlarmSeverity); |
||||
|
alarmSeverityEnum = AlarmSeverity; |
||||
|
|
||||
|
alarmSeverityTranslationMap = alarmSeverityTranslations; |
||||
|
|
||||
|
buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); |
||||
|
|
||||
|
alarmFilterConfigForm: UntypedFormGroup; |
||||
|
|
||||
|
alarmFilterOverlayRef: OverlayRef; |
||||
|
|
||||
|
panelResult: AlarmFilterConfig = null; |
||||
|
|
||||
|
private alarmFilterConfig: AlarmFilterConfig; |
||||
|
|
||||
|
private propagateChange = (_: any) => {}; |
||||
|
|
||||
|
constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA) |
||||
|
private data: AlarmFilterConfigData | undefined, |
||||
|
@Optional() |
||||
|
private overlayRef: OverlayRef, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private translate: TranslateService, |
||||
|
private overlay: Overlay, |
||||
|
private nativeElement: ElementRef, |
||||
|
private viewContainerRef: ViewContainerRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
if (this.data) { |
||||
|
this.panelMode = this.data.panelMode; |
||||
|
this.userMode = this.data.userMode; |
||||
|
this.alarmFilterConfig = this.data.alarmFilterConfig; |
||||
|
} |
||||
|
this.alarmFilterConfigForm = this.fb.group({ |
||||
|
statusList: [null, []], |
||||
|
severityList: [null, []], |
||||
|
typeList: [null, []], |
||||
|
searchPropagatedAlarms: [false, []], |
||||
|
assignedToCurrentUser: [false, []], |
||||
|
assigneeId: [null, []] |
||||
|
}); |
||||
|
this.alarmFilterConfigForm.valueChanges.subscribe( |
||||
|
() => { |
||||
|
this.updateValidators(); |
||||
|
if (!this.buttonMode) { |
||||
|
this.alarmConfigUpdated(this.alarmFilterConfigForm.value); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
if (this.panelMode) { |
||||
|
this.updateAlarmConfigForm(this.alarmFilterConfig); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.alarmFilterConfigForm.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.alarmFilterConfigForm.enable({emitEvent: false}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(alarmFilterConfig?: AlarmFilterConfig): void { |
||||
|
this.alarmFilterConfig = alarmFilterConfig; |
||||
|
this.updateButtonDisplayValue(); |
||||
|
this.updateAlarmConfigForm(alarmFilterConfig); |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const assignedToCurrentUser = this.alarmFilterConfigForm.get('assignedToCurrentUser').value; |
||||
|
if (assignedToCurrentUser) { |
||||
|
this.alarmFilterConfigForm.get('assigneeId').disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.alarmFilterConfigForm.get('assigneeId').enable({emitEvent: false}); |
||||
|
} |
||||
|
this.alarmFilterConfigForm.get('assigneeId').updateValueAndValidity({emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
toggleAlarmFilterPanel($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
const config = new OverlayConfig({ |
||||
|
panelClass: 'tb-alarm-filter-panel', |
||||
|
backdropClass: 'cdk-overlay-transparent-backdrop', |
||||
|
hasBackdrop: true, |
||||
|
maxHeight: '80vh', |
||||
|
height: 'min-content', |
||||
|
minWidth: '' |
||||
|
}); |
||||
|
config.hasBackdrop = true; |
||||
|
const connectedPosition: ConnectedPosition = { |
||||
|
originX: 'start', |
||||
|
originY: 'bottom', |
||||
|
overlayX: 'start', |
||||
|
overlayY: 'top' |
||||
|
}; |
||||
|
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement) |
||||
|
.withPositions([connectedPosition]); |
||||
|
|
||||
|
this.alarmFilterOverlayRef = this.overlay.create(config); |
||||
|
this.alarmFilterOverlayRef.backdropClick().subscribe(() => { |
||||
|
this.alarmFilterOverlayRef.dispose(); |
||||
|
}); |
||||
|
this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmFilterPanel, |
||||
|
this.viewContainerRef)); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.updateAlarmConfigForm(this.alarmFilterConfig); |
||||
|
if (this.overlayRef) { |
||||
|
this.overlayRef.dispose(); |
||||
|
} else { |
||||
|
this.alarmFilterOverlayRef.dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
this.alarmConfigUpdated(this.alarmFilterConfigForm.value); |
||||
|
if (this.panelMode) { |
||||
|
this.panelResult = this.alarmFilterConfig; |
||||
|
} |
||||
|
if (this.overlayRef) { |
||||
|
this.overlayRef.dispose(); |
||||
|
} else { |
||||
|
this.alarmFilterOverlayRef.dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public alarmTypeList(): string[] { |
||||
|
return this.alarmFilterConfigForm.get('typeList').value; |
||||
|
} |
||||
|
|
||||
|
public removeAlarmType(type: string): void { |
||||
|
const types: string[] = this.alarmFilterConfigForm.get('typeList').value; |
||||
|
const index = types.indexOf(type); |
||||
|
if (index >= 0) { |
||||
|
types.splice(index, 1); |
||||
|
this.alarmFilterConfigForm.get('typeList').setValue(types); |
||||
|
this.alarmFilterConfigForm.get('typeList').markAsDirty(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public addAlarmType(event: MatChipInputEvent): void { |
||||
|
const input = event.chipInput.inputElement; |
||||
|
const value = event.value; |
||||
|
|
||||
|
let types: string[] = this.alarmFilterConfigForm.get('typeList').value; |
||||
|
|
||||
|
if ((value || '').trim()) { |
||||
|
if (!types) { |
||||
|
types = []; |
||||
|
} |
||||
|
types.push(value.trim()); |
||||
|
this.alarmFilterConfigForm.get('typeList').setValue(types); |
||||
|
this.alarmFilterConfigForm.get('typeList').markAsDirty(); |
||||
|
} |
||||
|
|
||||
|
if (input) { |
||||
|
input.value = ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateAlarmConfigForm(alarmFilterConfig?: AlarmFilterConfig) { |
||||
|
this.alarmFilterConfigForm.patchValue({ |
||||
|
statusList: alarmFilterConfig?.statusList, |
||||
|
severityList: alarmFilterConfig?.severityList, |
||||
|
typeList: alarmFilterConfig?.typeList, |
||||
|
searchPropagatedAlarms: alarmFilterConfig?.searchPropagatedAlarms, |
||||
|
assignedToCurrentUser: alarmFilterConfig?.assignedToCurrentUser, |
||||
|
assigneeId: alarmFilterConfig?.assigneeId |
||||
|
}, {emitEvent: false}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
|
||||
|
private alarmConfigUpdated(alarmFilterConfig: AlarmFilterConfig) { |
||||
|
this.alarmFilterConfig = alarmFilterConfig; |
||||
|
this.updateButtonDisplayValue(); |
||||
|
this.propagateChange(this.alarmFilterConfig); |
||||
|
} |
||||
|
|
||||
|
private updateButtonDisplayValue() { |
||||
|
if (this.buttonMode) { |
||||
|
const filterTextParts: string[] = []; |
||||
|
if (this.alarmFilterConfig?.statusList?.length) { |
||||
|
filterTextParts.push(this.alarmFilterConfig.statusList.map(s => |
||||
|
this.translate.instant(alarmSearchStatusTranslations.get(s))).join(', ')); |
||||
|
} |
||||
|
if (this.alarmFilterConfig?.severityList?.length) { |
||||
|
filterTextParts.push(this.alarmFilterConfig.severityList.map(s => |
||||
|
this.translate.instant(alarmSeverityTranslations.get(s))).join(', ')); |
||||
|
} |
||||
|
if (this.alarmFilterConfig?.typeList?.length) { |
||||
|
filterTextParts.push(this.alarmFilterConfig.typeList.join(', ')); |
||||
|
} |
||||
|
if (this.alarmFilterConfig?.assignedToCurrentUser) { |
||||
|
filterTextParts.push(this.translate.instant('alarm.assigned-to-me')); |
||||
|
} else if (this.alarmFilterConfig?.assigneeId) { |
||||
|
filterTextParts.push(this.translate.instant('alarm.assigned')); |
||||
|
} |
||||
|
if (!filterTextParts.length) { |
||||
|
this.buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); |
||||
|
} else { |
||||
|
this.buttonDisplayValue = this.translate.instant('alarm.filter-title') + `: ${filterTextParts.join(', ')}`; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,65 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
Copyright © 2016-2023 The Thingsboard Authors |
|
||||
|
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
you may not use this file except in compliance with the License. |
|
||||
You may obtain a copy of the License at |
|
||||
|
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
|
||||
|
|
||||
Unless required by applicable law or agreed to in writing, software |
|
||||
distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
See the License for the specific language governing permissions and |
|
||||
limitations under the License. |
|
||||
|
|
||||
--> |
|
||||
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="alarmFilterFormGroup" (ngSubmit)="update()"> |
|
||||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|
||||
<mat-label translate>alarm.alarm-status-list</mat-label> |
|
||||
<mat-select formControlName="alarmStatusList" multiple |
|
||||
placeholder="{{ !alarmFilterFormGroup.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}"> |
|
||||
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus"> |
|
||||
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|
||||
<mat-label translate>alarm.alarm-severity-list</mat-label> |
|
||||
<mat-select formControlName="alarmSeverityList" multiple |
|
||||
placeholder="{{ !alarmFilterFormGroup.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}"> |
|
||||
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> |
|
||||
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|
||||
<mat-label translate>alarm.alarm-type-list</mat-label> |
|
||||
<mat-chip-grid #alarmTypeChipList formControlName="alarmTypeList"> |
|
||||
<mat-chip-row *ngFor="let type of alarmTypeList()" |
|
||||
[removable]="true" (removed)="removeAlarmType(type)"> |
|
||||
{{type}} |
|
||||
<mat-icon matChipRemove>cancel</mat-icon> |
|
||||
</mat-chip-row> |
|
||||
<input placeholder="{{ !alarmFilterFormGroup.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}" |
|
||||
[matChipInputFor]="alarmTypeChipList" |
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|
||||
matChipInputAddOnBlur |
|
||||
(matChipInputTokenEnd)="addAlarmType($event)"> |
|
||||
</mat-chip-grid> |
|
||||
</mat-form-field> |
|
||||
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> |
|
||||
<button type="button" |
|
||||
mat-button |
|
||||
(click)="cancel()"> |
|
||||
{{ 'action.cancel' | translate }} |
|
||||
</button> |
|
||||
<button type="submit" |
|
||||
mat-raised-button |
|
||||
color="primary" |
|
||||
[disabled]="alarmFilterFormGroup.invalid || !alarmFilterFormGroup.dirty"> |
|
||||
{{ 'action.update' | translate }} |
|
||||
</button> |
|
||||
</div> |
|
||||
</form> |
|
||||
@ -1,119 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2023 The Thingsboard Authors
|
|
||||
///
|
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
/// you may not use this file except in compliance with the License.
|
|
||||
/// You may obtain a copy of the License at
|
|
||||
///
|
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
///
|
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
/// See the License for the specific language governing permissions and
|
|
||||
/// limitations under the License.
|
|
||||
///
|
|
||||
|
|
||||
import { Component, Inject, InjectionToken } from '@angular/core'; |
|
||||
import { |
|
||||
AlarmSearchStatus, |
|
||||
alarmSearchStatusTranslations, |
|
||||
AlarmSeverity, |
|
||||
alarmSeverityTranslations |
|
||||
} from '@shared/models/alarm.models'; |
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|
||||
import { MatChipInputEvent } from '@angular/material/chips'; |
|
||||
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
|
||||
import { OverlayRef } from '@angular/cdk/overlay'; |
|
||||
|
|
||||
export const ALARM_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmFilterPanelData'); |
|
||||
|
|
||||
export interface AlarmFilterPanelData { |
|
||||
statusList: AlarmSearchStatus[]; |
|
||||
severityList: AlarmSeverity[]; |
|
||||
typeList: string[]; |
|
||||
} |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-alarm-filter-panel', |
|
||||
templateUrl: './alarm-filter-panel.component.html', |
|
||||
styleUrls: ['./alarm-filter-panel.component.scss'] |
|
||||
}) |
|
||||
export class AlarmFilterPanelComponent { |
|
||||
|
|
||||
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
|
||||
|
|
||||
alarmFilterFormGroup: UntypedFormGroup; |
|
||||
|
|
||||
result: AlarmFilterPanelData; |
|
||||
|
|
||||
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, |
|
||||
AlarmSearchStatus.CLEARED, |
|
||||
AlarmSearchStatus.ACK, |
|
||||
AlarmSearchStatus.UNACK]; |
|
||||
|
|
||||
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; |
|
||||
|
|
||||
alarmSeverities = Object.keys(AlarmSeverity); |
|
||||
alarmSeverityEnum = AlarmSeverity; |
|
||||
|
|
||||
alarmSeverityTranslationMap = alarmSeverityTranslations; |
|
||||
|
|
||||
constructor(@Inject(ALARM_FILTER_PANEL_DATA) |
|
||||
public data: AlarmFilterPanelData, |
|
||||
public overlayRef: OverlayRef, |
|
||||
private fb: UntypedFormBuilder) { |
|
||||
this.alarmFilterFormGroup = this.fb.group( |
|
||||
{ |
|
||||
alarmStatusList: [this.data.statusList], |
|
||||
alarmSeverityList: [this.data.severityList], |
|
||||
alarmTypeList: [this.data.typeList] |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
public alarmTypeList(): string[] { |
|
||||
return this.alarmFilterFormGroup.get('alarmTypeList').value; |
|
||||
} |
|
||||
|
|
||||
public removeAlarmType(type: string): void { |
|
||||
const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; |
|
||||
const index = types.indexOf(type); |
|
||||
if (index >= 0) { |
|
||||
types.splice(index, 1); |
|
||||
this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); |
|
||||
this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public addAlarmType(event: MatChipInputEvent): void { |
|
||||
const input = event.chipInput.inputElement; |
|
||||
const value = event.value; |
|
||||
|
|
||||
const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value; |
|
||||
|
|
||||
if ((value || '').trim()) { |
|
||||
types.push(value.trim()); |
|
||||
this.alarmFilterFormGroup.get('alarmTypeList').setValue(types); |
|
||||
this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty(); |
|
||||
} |
|
||||
|
|
||||
if (input) { |
|
||||
input.value = ''; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
update() { |
|
||||
this.result = { |
|
||||
statusList: this.alarmFilterFormGroup.get('alarmStatusList').value, |
|
||||
severityList: this.alarmFilterFormGroup.get('alarmSeverityList').value, |
|
||||
typeList: this.alarmFilterFormGroup.get('alarmTypeList').value |
|
||||
}; |
|
||||
this.overlayRef.dispose(); |
|
||||
} |
|
||||
|
|
||||
cancel() { |
|
||||
this.overlayRef.dispose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -0,0 +1,33 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form class="tb-add-quick-link-dialog" [formGroup]="addQuickLinkFormGroup"> |
||||
|
<mat-toolbar style="background: transparent;"> |
||||
|
<h2 translate>widgets.quick-links.add-link-title</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<tb-quick-link formControlName="link" [addOnly]="true" |
||||
|
(quickLinkAdded)="add($event)"> |
||||
|
</tb-quick-link> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
.tb-add-quick-link-dialog { |
||||
|
width: 480px; |
||||
|
.mat-toolbar-single-row { |
||||
|
padding: 0 24px; |
||||
|
} |
||||
|
h2 { |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
.mat-icon { |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit, SkipSelf } from '@angular/core'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-add-quick-link-dialog', |
||||
|
templateUrl: './add-quick-link-dialog.component.html', |
||||
|
styleUrls: ['./add-quick-link-dialog.component.scss'] |
||||
|
}) |
||||
|
export class AddQuickLinkDialogComponent extends |
||||
|
DialogComponent<AddQuickLinkDialogComponent, string> implements OnInit { |
||||
|
|
||||
|
addQuickLinkFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<AddQuickLinkDialogComponent, string>, |
||||
|
public fb: UntypedFormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.addQuickLinkFormGroup = this.fb.group({ |
||||
|
link: [null, [Validators.required]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(link: string): void { |
||||
|
this.dialogRef.close(link); |
||||
|
} |
||||
|
} |
||||
@ -1,63 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
Copyright © 2016-2023 The Thingsboard Authors |
|
||||
|
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
you may not use this file except in compliance with the License. |
|
||||
You may obtain a copy of the License at |
|
||||
|
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
|
||||
|
|
||||
Unless required by applicable law or agreed to in writing, software |
|
||||
distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
See the License for the specific language governing permissions and |
|
||||
limitations under the License. |
|
||||
|
|
||||
--> |
|
||||
<form class="tb-edit-doc-links-dialog" [formGroup]="editDocLinksFormGroup"> |
|
||||
<mat-toolbar style="background: transparent;"> |
|
||||
<h2 translate>widgets.documentation.title</h2> |
|
||||
<span fxFlex></span> |
|
||||
<button mat-icon-button |
|
||||
(click)="close()" |
|
||||
type="button"> |
|
||||
<mat-icon class="material-icons">close</mat-icon> |
|
||||
</button> |
|
||||
</mat-toolbar> |
|
||||
<div mat-dialog-content> |
|
||||
<div class="tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
|
||||
(cdkDropListDropped)="docLinkDrop($event)" [cdkDropListDisabled]="editMode || addMode"> |
|
||||
<div cdkDrag class="tb-draggable" *ngFor="let docLinkControl of docLinksFormArray().controls; trackBy: trackByDocLink; |
|
||||
let $index = index; last as isLast;" |
|
||||
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
|
||||
<tb-doc-link fxFlex #docLinkComponent [formControl]="docLinkControl" |
|
||||
[disableEdit]="editMode || addMode" |
|
||||
(editModeChanged)="editMode = $event" |
|
||||
(docLinkUpdated)="update()" |
|
||||
(docLinkDeleted)="deleteLink($index)"> |
|
||||
</tb-doc-link> |
|
||||
<div *ngIf="!docLinkComponent.isEditing()" |
|
||||
cdkDragHandle |
|
||||
matTooltip="{{ 'action.drag' | translate }}" |
|
||||
matTooltipPosition="above" |
|
||||
class="tb-drag-handle"> |
|
||||
<mat-icon>drag_indicator</mat-icon> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div mat-dialog-actions> |
|
||||
<div *ngIf="!editMode && !addMode" fxFlex class="tb-add-doc-button" |
|
||||
matTooltip="{{ 'widgets.documentation.add-link' | translate }}" |
|
||||
matTooltipPosition="above" |
|
||||
(click)="addLink()"> |
|
||||
<mat-icon class="tb-add-icon">add</mat-icon> |
|
||||
</div> |
|
||||
<tb-doc-link *ngIf="addMode" fxFlex [(ngModel)]="addingDocLink" |
|
||||
[ngModelOptions]="{standalone: true}" |
|
||||
(docLinkAdded)="linkAdded($event)" |
|
||||
(docLinkAddCanceled)="addMode = false"> |
|
||||
</tb-doc-link> |
|
||||
</div> |
|
||||
</form> |
|
||||
@ -1,114 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2023 The Thingsboard Authors
|
|
||||
///
|
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
/// you may not use this file except in compliance with the License.
|
|
||||
/// You may obtain a copy of the License at
|
|
||||
///
|
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
///
|
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
/// See the License for the specific language governing permissions and
|
|
||||
/// limitations under the License.
|
|
||||
///
|
|
||||
|
|
||||
import { Component, Inject, OnInit } from '@angular/core'; |
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|
||||
import { Store } from '@ngrx/store'; |
|
||||
import { AppState } from '@core/core.state'; |
|
||||
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|
||||
import { DialogComponent } from '@shared/components/dialog.component'; |
|
||||
import { Router } from '@angular/router'; |
|
||||
import { DocumentationLink, DocumentationLinks } from '@shared/models/user-settings.models'; |
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
|
||||
import { UserSettingsService } from '@core/http/user-settings.service'; |
|
||||
|
|
||||
export interface EditDocLinksDialogData { |
|
||||
docLinks: DocumentationLinks; |
|
||||
} |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-edit-doc-links-dialog', |
|
||||
templateUrl: './edit-doc-links-dialog.component.html', |
|
||||
styleUrls: ['./edit-doc-links-dialog.component.scss'] |
|
||||
}) |
|
||||
export class EditDocLinksDialogComponent extends |
|
||||
DialogComponent<EditDocLinksDialogComponent, boolean> implements OnInit { |
|
||||
|
|
||||
updated = false; |
|
||||
addMode = false; |
|
||||
editMode = false; |
|
||||
|
|
||||
docLinks = this.data.docLinks; |
|
||||
addingDocLink: Partial<DocumentationLink>; |
|
||||
|
|
||||
editDocLinksFormGroup: UntypedFormGroup; |
|
||||
|
|
||||
constructor(protected store: Store<AppState>, |
|
||||
protected router: Router, |
|
||||
@Inject(MAT_DIALOG_DATA) public data: EditDocLinksDialogData, |
|
||||
public dialogRef: MatDialogRef<EditDocLinksDialogComponent, boolean>, |
|
||||
public fb: UntypedFormBuilder, |
|
||||
private userSettingsService: UserSettingsService) { |
|
||||
super(store, router, dialogRef); |
|
||||
} |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
const docLinksControls: Array<AbstractControl> = []; |
|
||||
for (const docLink of this.docLinks.links) { |
|
||||
docLinksControls.push(this.fb.control(docLink, [Validators.required])); |
|
||||
} |
|
||||
this.editDocLinksFormGroup = this.fb.group({ |
|
||||
links: this.fb.array(docLinksControls) |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
docLinksFormArray(): UntypedFormArray { |
|
||||
return this.editDocLinksFormGroup.get('links') as UntypedFormArray; |
|
||||
} |
|
||||
|
|
||||
trackByDocLink(index: number, docLinkControl: AbstractControl): any { |
|
||||
return docLinkControl; |
|
||||
} |
|
||||
|
|
||||
docLinkDrop(event: CdkDragDrop<string[]>) { |
|
||||
const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray; |
|
||||
const docLink = docLinksArray.at(event.previousIndex); |
|
||||
docLinksArray.removeAt(event.previousIndex); |
|
||||
docLinksArray.insert(event.currentIndex, docLink); |
|
||||
this.update(); |
|
||||
} |
|
||||
|
|
||||
addLink() { |
|
||||
this.addingDocLink = { icon: 'notifications' }; |
|
||||
this.addMode = true; |
|
||||
} |
|
||||
|
|
||||
linkAdded(docLink: DocumentationLink) { |
|
||||
this.addMode = false; |
|
||||
const docLinksArray = this.editDocLinksFormGroup.get('links') as UntypedFormArray; |
|
||||
const docLinkControl = this.fb.control(docLink, [Validators.required]); |
|
||||
docLinksArray.push(docLinkControl); |
|
||||
this.update(); |
|
||||
} |
|
||||
|
|
||||
deleteLink(index: number) { |
|
||||
(this.editDocLinksFormGroup.get('links') as UntypedFormArray).removeAt(index); |
|
||||
this.update(); |
|
||||
} |
|
||||
|
|
||||
update() { |
|
||||
if (this.editDocLinksFormGroup.valid) { |
|
||||
const docLinks: DocumentationLinks = this.editDocLinksFormGroup.value; |
|
||||
this.userSettingsService.updateDocumentationLinks(docLinks).subscribe(() => { |
|
||||
this.updated = true; |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
close(): void { |
|
||||
this.dialogRef.close(this.updated); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,95 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form class="tb-edit-links-dialog" [formGroup]="editLinksFormGroup"> |
||||
|
<mat-toolbar style="background: transparent;"> |
||||
|
<h2>{{ (mode === 'docs' ? 'widgets.documentation.title' : 'widgets.quick-links.title') | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="close()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<div class="tb-drop-list" cdkDropList cdkDropListOrientation="vertical" |
||||
|
(cdkDropListDropped)="linkDrop($event)" [cdkDropListDisabled]="editMode || addMode"> |
||||
|
<div cdkDrag class="tb-draggable" *ngFor="let linkControl of linksFormArray().controls; trackBy: trackByLink; |
||||
|
let $index = index; last as isLast;" |
||||
|
fxLayout="row" fxLayoutAlign="start center" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> |
||||
|
<ng-container [ngSwitch]="mode"> |
||||
|
<ng-template [ngSwitchCase]="'docs'"> |
||||
|
<tb-doc-link fxFlex #docLink [formControl]="linkControl" |
||||
|
[disableEdit]="editMode || addMode" |
||||
|
(editModeChanged)="editMode = $event" |
||||
|
(docLinkUpdated)="update()" |
||||
|
(docLinkDeleted)="deleteLink($index)"> |
||||
|
</tb-doc-link> |
||||
|
<ng-container *ngIf="!docLink.isEditing()"> |
||||
|
<ng-container *ngTemplateOutlet="dragHandle"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="'quickLinks'"> |
||||
|
<tb-quick-link fxFlex #quickLink [formControl]="linkControl" |
||||
|
[disableEdit]="editMode || addMode" |
||||
|
(editModeChanged)="editMode = $event" |
||||
|
(quickLinkUpdated)="update()" |
||||
|
(quickLinkDeleted)="deleteLink($index)"> |
||||
|
</tb-quick-link> |
||||
|
<ng-container *ngIf="!quickLink.isEditing()"> |
||||
|
<ng-container *ngTemplateOutlet="dragHandle"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
<ng-template #dragHandle> |
||||
|
<div cdkDragHandle |
||||
|
matTooltip="{{ 'action.drag' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
class="tb-drag-handle"> |
||||
|
<mat-icon>drag_indicator</mat-icon> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions> |
||||
|
<div *ngIf="!editMode && !addMode" fxFlex class="tb-add-link-button" |
||||
|
matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="addLink()"> |
||||
|
<mat-icon class="tb-add-icon">add</mat-icon> |
||||
|
</div> |
||||
|
<ng-container *ngIf="addMode"> |
||||
|
<ng-container [ngSwitch]="mode"> |
||||
|
<ng-template [ngSwitchCase]="'docs'"> |
||||
|
<tb-doc-link fxFlex [(ngModel)]="addingLink" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
(docLinkAdded)="linkAdded($event)" |
||||
|
(docLinkAddCanceled)="addMode = false"> |
||||
|
</tb-doc-link> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="'quickLinks'"> |
||||
|
<tb-quick-link fxFlex [(ngModel)]="addingLink" |
||||
|
[ngModelOptions]="{standalone: true}" |
||||
|
(quickLinkAdded)="linkAdded($event)" |
||||
|
(quickLinkAddCanceled)="addMode = false"> |
||||
|
</tb-quick-link> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,122 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, Inject, OnInit } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DocumentationLink, DocumentationLinks, QuickLinks } from '@shared/models/user-settings.models'; |
||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
||||
|
import { UserSettingsService } from '@core/http/user-settings.service'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
export interface EditLinksDialogData { |
||||
|
mode: 'docs' | 'quickLinks'; |
||||
|
links: DocumentationLinks | QuickLinks; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-edit-links-dialog', |
||||
|
templateUrl: './edit-links-dialog.component.html', |
||||
|
styleUrls: ['./edit-links-dialog.component.scss'] |
||||
|
}) |
||||
|
export class EditLinksDialogComponent extends |
||||
|
DialogComponent<EditLinksDialogComponent, boolean> implements OnInit { |
||||
|
|
||||
|
updated = false; |
||||
|
addMode = false; |
||||
|
editMode = false; |
||||
|
|
||||
|
links = this.data.links; |
||||
|
mode = this.data.mode; |
||||
|
addingLink: Partial<DocumentationLink> | string; |
||||
|
|
||||
|
editLinksFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EditLinksDialogData, |
||||
|
public dialogRef: MatDialogRef<EditLinksDialogComponent, boolean>, |
||||
|
public fb: UntypedFormBuilder, |
||||
|
private userSettingsService: UserSettingsService) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
const linksControls: Array<AbstractControl> = []; |
||||
|
for (const link of this.links.links) { |
||||
|
linksControls.push(this.fb.control(link, [Validators.required])); |
||||
|
} |
||||
|
this.editLinksFormGroup = this.fb.group({ |
||||
|
links: this.fb.array(linksControls) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
linksFormArray(): UntypedFormArray { |
||||
|
return this.editLinksFormGroup.get('links') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
trackByLink(index: number, linkControl: AbstractControl): any { |
||||
|
return linkControl; |
||||
|
} |
||||
|
|
||||
|
linkDrop(event: CdkDragDrop<string[]>) { |
||||
|
const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray; |
||||
|
const link = linksArray.at(event.previousIndex); |
||||
|
linksArray.removeAt(event.previousIndex); |
||||
|
linksArray.insert(event.currentIndex, link); |
||||
|
this.update(); |
||||
|
} |
||||
|
|
||||
|
addLink() { |
||||
|
this.addingLink = this.mode === 'docs' ? { icon: 'notifications' } : null; |
||||
|
this.addMode = true; |
||||
|
} |
||||
|
|
||||
|
linkAdded(link: DocumentationLink | string) { |
||||
|
this.addMode = false; |
||||
|
const linksArray = this.editLinksFormGroup.get('links') as UntypedFormArray; |
||||
|
const linkControl = this.fb.control(link, [Validators.required]); |
||||
|
linksArray.push(linkControl); |
||||
|
this.update(); |
||||
|
} |
||||
|
|
||||
|
deleteLink(index: number) { |
||||
|
(this.editLinksFormGroup.get('links') as UntypedFormArray).removeAt(index); |
||||
|
this.update(); |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
if (this.editLinksFormGroup.valid) { |
||||
|
let updateObservable: Observable<void>; |
||||
|
if (this.mode === 'docs') { |
||||
|
updateObservable = this.userSettingsService.updateDocumentationLinks(this.editLinksFormGroup.value); |
||||
|
} else { |
||||
|
updateObservable = this.userSettingsService.updateQuickLinks(this.editLinksFormGroup.value); |
||||
|
} |
||||
|
updateObservable.subscribe(() => { |
||||
|
this.updated = true; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(this.updated); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import "../../../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
|
||||
|
.tb-card-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.tb-title { |
||||
|
font-style: normal; |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 12px; |
||||
|
line-height: 16px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-title-link { |
||||
|
font-style: normal; |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
position: relative; |
||||
|
border-bottom: none; |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 12px; |
||||
|
line-height: 16px; |
||||
|
} |
||||
|
&:hover, &:focus { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
&::after { |
||||
|
content: 'arrow_forward'; |
||||
|
display: inline-block; |
||||
|
transform: rotate(315deg); |
||||
|
font-family: 'Material Icons'; |
||||
|
font-weight: normal; |
||||
|
font-style: normal; |
||||
|
font-size: 18px; |
||||
|
color: rgba(0, 0, 0, 0.12); |
||||
|
vertical-align: bottom; |
||||
|
margin-left: 6px; |
||||
|
} |
||||
|
&:hover::after { |
||||
|
color: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
.tb-autocomplete.tb-quick-links { |
||||
|
.mat-mdc-option { |
||||
|
border-bottom: none; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
.mat-icon { |
||||
|
margin-right: 10px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
font-size: 20px; |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="row" *ngIf="addMode || editMode; else quickLinkTemplate"> |
||||
|
<div fxFlex class="tb-edit-link" [ngClass]="{'edit-mode': editMode}" [formGroup]="editQuickLinkFormGroup" fxLayout="column"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label translate>widgets.quick-links.quick-link</mat-label> |
||||
|
<input matInput type="text" |
||||
|
#linkInput |
||||
|
formControlName="link" |
||||
|
(focusin)="onFocus()" |
||||
|
required |
||||
|
[matAutocomplete]="linkAutocomplete"> |
||||
|
<mat-icon matPrefix *ngIf="quickLink && !quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon> |
||||
|
<mat-icon matPrefix *ngIf="quickLink && quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon> |
||||
|
<button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled" |
||||
|
type="button" |
||||
|
matSuffix mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete |
||||
|
class="tb-autocomplete tb-quick-links" |
||||
|
#linkAutocomplete="matAutocomplete" |
||||
|
[displayWith]="displayLinkFn"> |
||||
|
<mat-option *ngFor="let link of filteredLinks | async" [value]="link"> |
||||
|
<mat-icon *ngIf="!link.isMdiIcon">{{ link.icon }}</mat-icon> |
||||
|
<mat-icon *ngIf="link.isMdiIcon" [svgIcon]="link.icon"></mat-icon> |
||||
|
<span [innerHTML]="link.name | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get('widgets.quick-links.no-links-matching', {name: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="editQuickLinkFormGroup.get('link').hasError('required')"> |
||||
|
{{ 'widgets.quick-links.quick-link-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div *ngIf="addMode" fxLayout="row" fxLayoutAlign="end" fxLayoutGap="8px"> |
||||
|
<button *ngIf="!addOnly" mat-button color="primary" (click)="cancelAdd()">{{ 'action.cancel' | translate }}</button> |
||||
|
<button mat-raised-button color="primary" (click)="add()">{{ 'action.add' | translate }}</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons"> |
||||
|
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button> |
||||
|
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-template #quickLinkTemplate> |
||||
|
<div fxLayout="row"> |
||||
|
<div fxFlex class="tb-link"> |
||||
|
<div class="tb-link-container"> |
||||
|
<div class="tb-link-icon-container"> |
||||
|
<mat-icon *ngIf="!quickLink?.isMdiIcon" color="primary">{{ quickLink?.icon }}</mat-icon> |
||||
|
<mat-icon *ngIf="quickLink?.isMdiIcon" color="primary" [svgIcon]="quickLink?.icon"></mat-icon> |
||||
|
</div> |
||||
|
<div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" class="tb-edit-buttons"> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.edit' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="switchToEditMode()"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
matTooltip="{{ 'action.delete' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="delete()"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -0,0 +1,318 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
EventEmitter, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
SkipSelf, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
FormGroupDirective, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
NgForm, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors |
||||
|
} from '@angular/forms'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { ErrorStateMatcher } from '@angular/material/core'; |
||||
|
import { MenuService } from '@core/services/menu.service'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { MenuSection } from '@core/services/menu.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { catchError, distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { Direction } from '@shared/models/page/sort-order'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { deepClone } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-quick-link', |
||||
|
templateUrl: './quick-link.component.html', |
||||
|
styleUrls: ['./link.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => QuickLinkComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{provide: ErrorStateMatcher, useExisting: QuickLinkComponent} |
||||
|
] |
||||
|
}) |
||||
|
export class QuickLinkComponent extends PageComponent implements OnInit, ControlValueAccessor, ErrorStateMatcher { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
addOnly = false; |
||||
|
|
||||
|
@Input() |
||||
|
disableEdit = false; |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkAdded = new EventEmitter<string>(); |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkAddCanceled = new EventEmitter<void>(); |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkUpdated = new EventEmitter<string>(); |
||||
|
|
||||
|
@Output() |
||||
|
quickLinkDeleted = new EventEmitter<void>(); |
||||
|
|
||||
|
@Output() |
||||
|
editModeChanged = new EventEmitter<boolean>(); |
||||
|
|
||||
|
@ViewChild('linkInput', {static: false}) linkInput: ElementRef; |
||||
|
|
||||
|
filteredLinks: Observable<Array<MenuSection>>; |
||||
|
|
||||
|
private allLinksObservable$: Observable<Array<MenuSection>> = null; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
editMode = false; |
||||
|
addMode = false; |
||||
|
|
||||
|
quickLink: MenuSection; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
public editQuickLinkFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private submitted = false; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private menuService: MenuService, |
||||
|
public translate: TranslateService, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.addMode = this.addOnly; |
||||
|
this.editQuickLinkFormGroup = this.fb.group({ |
||||
|
link: [null, [this.requiredLinkValidator]] |
||||
|
}); |
||||
|
this.filteredLinks = this.editQuickLinkFormGroup.get('link').valueChanges |
||||
|
.pipe( |
||||
|
tap(value => { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = value; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : |
||||
|
((value as any).translated ? value.name : this.translate.instant(value.name))) : ''), |
||||
|
distinctUntilChanged(), |
||||
|
switchMap(name => this.fetchLinks(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
requiredLinkValidator(control: AbstractControl): ValidationErrors | null { |
||||
|
const value = control.value; |
||||
|
if (!value || typeof value === 'string') { |
||||
|
return {required: true}; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.editQuickLinkFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.editQuickLinkFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string): void { |
||||
|
if (value) { |
||||
|
this.menuService.menuLinkById(value).subscribe( |
||||
|
(link) => { |
||||
|
this.quickLink = link; |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue( |
||||
|
link, {emitEvent: false} |
||||
|
); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.quickLink = null; |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue( |
||||
|
value, {emitEvent: false} |
||||
|
); |
||||
|
if (!this.editQuickLinkFormGroup.valid) { |
||||
|
this.addMode = true; |
||||
|
this.editModeChanged.emit(true); |
||||
|
} |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
updateView(value: MenuSection | null) { |
||||
|
if (this.quickLink !== value) { |
||||
|
this.quickLink = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayLinkFn = (link?: MenuSection): string | undefined => |
||||
|
link ? ((link as any).translated ? link.name : this.translate.instant(link.fullName || link.name)) : undefined; |
||||
|
|
||||
|
fetchLinks(searchText?: string): Observable<Array<MenuSection>> { |
||||
|
this.searchText = searchText; |
||||
|
const pageLink = new PageLink(100, 0, searchText, { |
||||
|
property: 'name', |
||||
|
direction: Direction.ASC |
||||
|
}); |
||||
|
return this.getLinks(pageLink).pipe( |
||||
|
catchError(() => of(emptyPageData<MenuSection>())), |
||||
|
map(pageData => pageData.data) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getLinks(pageLink: PageLink): Observable<PageData<MenuSection>> { |
||||
|
return this.allLinks().pipe( |
||||
|
map((links) => pageLink.filterData(links)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
allLinks(): Observable<Array<MenuSection>> { |
||||
|
if (this.allLinksObservable$ === null) { |
||||
|
this.allLinksObservable$ = this.menuService.availableMenuLinks().pipe( |
||||
|
map((links) => { |
||||
|
const result = deepClone(links); |
||||
|
for (const link of result) { |
||||
|
link.name = this.translate.instant(link.fullName || link.name); |
||||
|
(link as any).translated = true; |
||||
|
} |
||||
|
return result; |
||||
|
}), |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} |
||||
|
return this.allLinksObservable$; |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.editQuickLinkFormGroup.get('link').updateValueAndValidity({onlySelf: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue(''); |
||||
|
setTimeout(() => { |
||||
|
this.linkInput.nativeElement.blur(); |
||||
|
this.linkInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
switchToEditMode() { |
||||
|
if (!this.disableEdit && !this.editMode) { |
||||
|
this.submitted = false; |
||||
|
this.editQuickLinkFormGroup.get('link').patchValue( |
||||
|
this.quickLink, {emitEvent: false} |
||||
|
); |
||||
|
this.editMode = true; |
||||
|
this.editModeChanged.emit(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
apply() { |
||||
|
this.submitted = true; |
||||
|
this.updateModel(); |
||||
|
if (this.quickLink) { |
||||
|
this.editMode = false; |
||||
|
this.editModeChanged.emit(false); |
||||
|
this.quickLinkUpdated.next(this.quickLink.id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancelEdit() { |
||||
|
this.submitted = false; |
||||
|
this.editMode = false; |
||||
|
this.editModeChanged.emit(false); |
||||
|
} |
||||
|
|
||||
|
add() { |
||||
|
this.submitted = true; |
||||
|
this.updateModel(); |
||||
|
if (this.quickLink) { |
||||
|
if (!this.addOnly) { |
||||
|
this.addMode = false; |
||||
|
this.editModeChanged.emit(false); |
||||
|
} |
||||
|
this.quickLinkAdded.next(this.quickLink.id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancelAdd() { |
||||
|
this.editModeChanged.emit(false); |
||||
|
this.quickLinkAddCanceled.emit(); |
||||
|
} |
||||
|
|
||||
|
delete() { |
||||
|
this.quickLinkDeleted.emit(); |
||||
|
} |
||||
|
|
||||
|
isEditing() { |
||||
|
return this.editMode || this.addMode; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
if (this.quickLink) { |
||||
|
this.propagateChange(this.quickLink.id); |
||||
|
} else { |
||||
|
this.propagateChange(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> |
||||
|
<div class="tb-card-header"> |
||||
|
<div class="tb-title">{{ 'widgets.quick-links.title' | translate }}</div> |
||||
|
<button class="tb-title-icon" |
||||
|
matTooltip="{{ 'action.edit' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
mat-icon-button |
||||
|
(click)="edit()"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize"> |
||||
|
<mat-grid-tile class="tb-links-tile" *ngFor="let quickLink of menuLinks$() | async"> |
||||
|
<a fxFlex class="tb-link-button" |
||||
|
[routerLink]="quickLink.path"> |
||||
|
<div class="tb-link-container"> |
||||
|
<div class="tb-link-icon-container"> |
||||
|
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon> |
||||
|
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon> |
||||
|
</div> |
||||
|
<div class="tb-link-text">{{ (quickLink.fullName || quickLink.name) | translate }}</div> |
||||
|
</div> |
||||
|
</a> |
||||
|
</mat-grid-tile> |
||||
|
<mat-grid-tile class="tb-links-tile"> |
||||
|
<div fxFlex class="tb-add-link-button" |
||||
|
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="addLink()"> |
||||
|
<mat-icon class="tb-add-icon">add</mat-icon> |
||||
|
</div> |
||||
|
</mat-grid-tile> |
||||
|
</mat-grid-list> |
||||
|
</div> |
||||
@ -0,0 +1,168 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { map, Observable, of, Subscription } from 'rxjs'; |
||||
|
import { QuickLinks } from '@shared/models/user-settings.models'; |
||||
|
import { UserSettingsService } from '@core/http/user-settings.service'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; |
||||
|
import { MediaBreakpoints } from '@shared/models/constants'; |
||||
|
import { MenuService } from '@core/services/menu.service'; |
||||
|
import { MenuSection } from '@core/services/menu.models'; |
||||
|
import { AddQuickLinkDialogComponent } from '@home/components/widget/lib/home-page/add-quick-link-dialog.component'; |
||||
|
import { |
||||
|
EditLinksDialogComponent, |
||||
|
EditLinksDialogData |
||||
|
} from '@home/components/widget/lib/home-page/edit-links-dialog.component'; |
||||
|
|
||||
|
const defaultQuickLinksMap = new Map<Authority, QuickLinks>( |
||||
|
[ |
||||
|
[Authority.SYS_ADMIN, { |
||||
|
links: ['tenants', 'tenant_profiles'] |
||||
|
}], |
||||
|
[Authority.TENANT_ADMIN, { |
||||
|
links: ['alarms', 'dashboards', 'devices'] |
||||
|
}], |
||||
|
[Authority.CUSTOMER_USER, { |
||||
|
links: ['alarms', 'dashboards', 'devices'] |
||||
|
}] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
interface QuickLinksWidgetSettings { |
||||
|
columns: number; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-quick-links-widget', |
||||
|
templateUrl: './quick-links-widget.component.html', |
||||
|
styleUrls: ['./home-page-widget.scss', './links-widget.component.scss'] |
||||
|
}) |
||||
|
export class QuickLinksWidgetComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
settings: QuickLinksWidgetSettings; |
||||
|
columns: number; |
||||
|
rowHeight = '55px'; |
||||
|
gutterSize = '12px'; |
||||
|
|
||||
|
quickLinks: QuickLinks; |
||||
|
authUser = getCurrentAuthUser(this.store); |
||||
|
|
||||
|
private observeBreakpointSubscription: Subscription; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private userSettingsService: UserSettingsService, |
||||
|
private dialog: MatDialog, |
||||
|
private menuService: MenuService, |
||||
|
private breakpointObserver: BreakpointObserver) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.settings = this.ctx.settings; |
||||
|
this.columns = this.settings.columns || 3; |
||||
|
const isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); |
||||
|
this.rowHeight = isMdLg ? '18px' : '55px'; |
||||
|
this.gutterSize = isMdLg ? '8px' : '12px'; |
||||
|
this.observeBreakpointSubscription = this.breakpointObserver |
||||
|
.observe(MediaBreakpoints['md-lg']) |
||||
|
.subscribe((state: BreakpointState) => { |
||||
|
if (state.matches) { |
||||
|
this.rowHeight = '18px'; |
||||
|
this.gutterSize = '8px'; |
||||
|
} else { |
||||
|
this.rowHeight = '55px'; |
||||
|
this.gutterSize = '12px'; |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
); |
||||
|
this.loadQuickLinks(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy() { |
||||
|
if (this.observeBreakpointSubscription) { |
||||
|
this.observeBreakpointSubscription.unsubscribe(); |
||||
|
} |
||||
|
super.ngOnDestroy(); |
||||
|
} |
||||
|
|
||||
|
menuLinks$(): Observable<Array<MenuSection>> { |
||||
|
return this.quickLinks ? this.menuService.menuLinksByIds(this.quickLinks.links) : of([]); |
||||
|
} |
||||
|
|
||||
|
private loadQuickLinks() { |
||||
|
this.userSettingsService.getQuickLinks().pipe( |
||||
|
map((quickLinks) => { |
||||
|
if (!quickLinks || !quickLinks.links) { |
||||
|
return defaultQuickLinksMap.get(this.authUser.authority); |
||||
|
} else { |
||||
|
return quickLinks; |
||||
|
} |
||||
|
}) |
||||
|
).subscribe( |
||||
|
(quickLinks) => { |
||||
|
this.quickLinks = quickLinks; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
edit() { |
||||
|
this.dialog.open<EditLinksDialogComponent, EditLinksDialogData, |
||||
|
boolean>(EditLinksDialogComponent, { |
||||
|
disableClose: true, |
||||
|
autoFocus: false, |
||||
|
data: { |
||||
|
mode: 'quickLinks', |
||||
|
links: this.quickLinks |
||||
|
}, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
||||
|
}).afterClosed().subscribe( |
||||
|
(result) => { |
||||
|
if (result) { |
||||
|
this.loadQuickLinks(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
addLink() { |
||||
|
this.dialog.open<AddQuickLinkDialogComponent, any, |
||||
|
string>(AddQuickLinkDialogComponent, { |
||||
|
disableClose: true, |
||||
|
autoFocus: false, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
||||
|
}).afterClosed().subscribe( |
||||
|
(link) => { |
||||
|
if (link) { |
||||
|
this.quickLinks.links.push(link); |
||||
|
this.cd.markForCheck(); |
||||
|
this.userSettingsService.updateQuickLinks(this.quickLinks).subscribe(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> |
||||
|
<div class="tb-card-header"> |
||||
|
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px"> |
||||
|
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[ |
||||
|
{ |
||||
|
name: ctx.translate.instant('widgets.recent-dashboards.last'), |
||||
|
value: 'last' |
||||
|
}, |
||||
|
{ |
||||
|
name: ctx.translate.instant('widgets.recent-dashboards.starred'), |
||||
|
value: 'starred' |
||||
|
} |
||||
|
]"> |
||||
|
</tb-toggle-header> |
||||
|
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md |
||||
|
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value"> |
||||
|
<ng-template [ngSwitchCase]="'last'"> |
||||
|
<div *ngIf="hasLastVisitedDashboards(); else noLastVisitedDashboards" style="overflow-y: auto;"> |
||||
|
<table mat-table |
||||
|
[dataSource]="lastVisitedDashboardsDataSource" matSort |
||||
|
[matSortActive]="lastVisitedDashboardsPageLink.sortOrder?.property" |
||||
|
[matSortDirection]="lastVisitedDashboardsPageLink.sortDirection()" matSortDisableClear> |
||||
|
<ng-container matColumnDef="starred"> |
||||
|
<mat-header-cell class="star-cell" *matHeaderCellDef> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell class="star-cell" *matCellDef="let lastVisitedDashboard"> |
||||
|
<mat-icon (click)="toggleDashboardStar(lastVisitedDashboard)" |
||||
|
class="star" [ngClass]="{'starred': lastVisitedDashboard.starred}">{{ lastVisitedDashboard.starred ? 'star' : 'star_border' }}</mat-icon> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="title"> |
||||
|
<mat-header-cell class="title" *matHeaderCellDef mat-sort-header> |
||||
|
{{ 'widgets.recent-dashboards.name' | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell class="title" *matCellDef="let lastVisitedDashboard"> |
||||
|
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="lastVisited"> |
||||
|
<mat-header-cell class="last-visited" *matHeaderCellDef mat-sort-header> |
||||
|
{{ 'widgets.recent-dashboards.last-viewed' | translate }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell class="last-visited" *matCellDef="let lastVisitedDashboard"> |
||||
|
{{ lastVisitedDashboard.lastVisited | dateAgo:{applyAgo: true} }} |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row *matHeaderRowDef="lastVisitedDashboardsColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row *matRowDef="let lastVisitedDashboard; columns: lastVisitedDashboardsColumns;"></mat-row> |
||||
|
</table> |
||||
|
</div> |
||||
|
<ng-template #noLastVisitedDashboards> |
||||
|
<div class="tb-no-dashboards"> |
||||
|
<div class="tb-no-last-visited-bg"></div> |
||||
|
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="'starred'"> |
||||
|
<div style="overflow-y: auto;"> |
||||
|
<div class="tb-starred-dashboard-row" *ngFor="let dashboard of userDashboardsInfo?.starred"> |
||||
|
<div class="tb-cell star-cell"> |
||||
|
<mat-icon (click)="toggleDashboardStar(dashboard)" |
||||
|
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon> |
||||
|
</div> |
||||
|
<div class="tb-cell title"> |
||||
|
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-dashboard-autocomplete class="tb-star-dashboard-autocomplete" |
||||
|
#starDashboardAutocomplete |
||||
|
subscriptSizing="dynamic" |
||||
|
appearance="outline" |
||||
|
[useIdValue]="false" |
||||
|
label="" |
||||
|
placeholder="{{ 'dashboard.select-dashboard' | translate }}" |
||||
|
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
<ng-template #loading> |
||||
|
<div class="tb-no-dashboards"> |
||||
|
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</div> |
||||
@ -0,0 +1,240 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import "../../../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
.tb-card-header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
@media #{$mat-lt-md} { |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
} |
||||
|
.tb-no-dashboards { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
.tb-no-last-visited-bg { |
||||
|
width: 60px; |
||||
|
height: 60px; |
||||
|
margin: 10px; |
||||
|
position: relative; |
||||
|
&:before { |
||||
|
content: ""; |
||||
|
border-radius: 8px; |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
background: #305680; |
||||
|
-webkit-mask-image: url(/assets/home/no_data_bg.svg); |
||||
|
-webkit-mask-repeat: no-repeat; |
||||
|
mask-image: url(/assets/home/no_data_bg.svg); |
||||
|
mask-repeat: no-repeat; |
||||
|
} |
||||
|
} |
||||
|
.tb-no-last-visited-dashboards-text { |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
|
||||
|
.tb-starred-dashboard-row { |
||||
|
height: 40px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
@media #{$mat-md-lg} { |
||||
|
height: 34px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-cell { |
||||
|
box-sizing: content-box; |
||||
|
&:first-child { |
||||
|
padding: 0 12px; |
||||
|
} |
||||
|
&:nth-child(n+2):nth-last-child(n+2) { |
||||
|
padding: 0 28px 0 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-star-dashboard-autocomplete { |
||||
|
margin-left: 64px; |
||||
|
margin-right: 64px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
margin-left: 48px; |
||||
|
margin-right: 48px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-mdc-cell, .mat-mdc-header-cell, .mdc-data-table__row:last-child .mdc-data-table__cell { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-table { |
||||
|
.mdc-data-table__row, .mdc-data-table__header-row { |
||||
|
height: 40px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
height: 34px; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: inherit; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-cell:nth-child(n+2):nth-last-child(n+2) { |
||||
|
padding: 0 28px 0 0; |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-row:not(.mat-row-select) .mat-mdc-cell:nth-child(n+2):nth-last-child(n+2) { |
||||
|
@media #{$mat-md-lg} { |
||||
|
padding: 0 12px 0 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-header-cell, .mat-mdc-cell, .tb-cell { |
||||
|
&.star-cell { |
||||
|
width: 40px; |
||||
|
min-width: 40px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
width: 24px; |
||||
|
min-width: 24px; |
||||
|
max-width: 24px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.title { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
&.last-visited { |
||||
|
width: 80px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
width: 50px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.mat-mdc-header-cell { |
||||
|
font-weight: 400; |
||||
|
font-size: 12px; |
||||
|
line-height: 16px; |
||||
|
letter-spacing: 0.25px; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 11px; |
||||
|
} |
||||
|
} |
||||
|
.mat-mdc-cell.title { |
||||
|
max-width: 0; |
||||
|
} |
||||
|
.mat-mdc-cell, .tb-cell { |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 11px; |
||||
|
line-height: 16px; |
||||
|
} |
||||
|
&.title { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
a { |
||||
|
color: rgba(0, 0, 0, 0.87); |
||||
|
border-bottom: none; |
||||
|
&:hover, &:focus { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
&.last-visited { |
||||
|
cursor: default; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
.star { |
||||
|
cursor: pointer; |
||||
|
vertical-align: middle; |
||||
|
color: rgba(0, 0, 0, 0.12); |
||||
|
&:hover { |
||||
|
color: rgba(0, 0, 0, 0.18); |
||||
|
} |
||||
|
&.starred { |
||||
|
color: #FAE205; |
||||
|
&:hover { |
||||
|
color: #CDBD2C; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-star-dashboard-autocomplete { |
||||
|
.mat-mdc-form-field { |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 11px; |
||||
|
line-height: 16px; |
||||
|
} |
||||
|
} |
||||
|
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { |
||||
|
padding-top: 4px; |
||||
|
padding-bottom: 4px; |
||||
|
min-height: 28px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
min-height: 24px; |
||||
|
} |
||||
|
} |
||||
|
.mat-mdc-form-field-hint-wrapper { |
||||
|
height: 0; |
||||
|
} |
||||
|
.mdc-text-field--outlined .mdc-notched-outline { |
||||
|
.mdc-notched-outline__leading, .mdc-notched-outline__trailing { |
||||
|
border-top-style: dashed; |
||||
|
border-bottom-style: dashed; |
||||
|
} |
||||
|
.mdc-notched-outline__leading { |
||||
|
border-left-style: dashed; |
||||
|
} |
||||
|
.mdc-notched-outline__trailing { |
||||
|
border-right-style: dashed; |
||||
|
} |
||||
|
} |
||||
|
.mat-mdc-icon-button.mat-mdc-button-base { |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
padding: 2px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,222 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
AfterViewInit, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
QueryList, ViewChild, |
||||
|
ViewChildren |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { BehaviorSubject, Observable, of } from 'rxjs'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { |
||||
|
AbstractUserDashboardInfo, |
||||
|
LastVisitedDashboardInfo, StarredDashboardInfo, |
||||
|
UserDashboardAction, |
||||
|
UserDashboardsInfo |
||||
|
} from '@shared/models/user-settings.models'; |
||||
|
import { UserSettingsService } from '@core/http/user-settings.service'; |
||||
|
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; |
||||
|
import { map, share } from 'rxjs/operators'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { MatSort } from '@angular/material/sort'; |
||||
|
import { DashboardInfo } from '@shared/models/dashboard.models'; |
||||
|
import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-recent-dashboards-widget', |
||||
|
templateUrl: './recent-dashboards-widget.component.html', |
||||
|
styleUrls: ['./home-page-widget.scss', './recent-dashboards-widget.component.scss'] |
||||
|
}) |
||||
|
export class RecentDashboardsWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
@ViewChildren(MatSort) lastVisitedDashboardsSort: QueryList<MatSort>; |
||||
|
|
||||
|
@ViewChild('starDashboardAutocomplete', {static: false}) |
||||
|
starDashboardAutocomplete: DashboardAutocompleteComponent; |
||||
|
|
||||
|
authority = Authority; |
||||
|
|
||||
|
userDashboardsInfo: UserDashboardsInfo; |
||||
|
authUser = getCurrentAuthUser(this.store); |
||||
|
|
||||
|
toggleValue: 'last' | 'starred' = 'last'; |
||||
|
|
||||
|
lastVisitedDashboardsColumns = ['starred', 'title', 'lastVisited']; |
||||
|
lastVisitedDashboardsDataSource: LastVisitedDashboardsDataSource; |
||||
|
lastVisitedDashboardsPageLink: PageLink; |
||||
|
|
||||
|
starredDashboardValue = null; |
||||
|
|
||||
|
dirty = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private userSettingService: UserSettingsService) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.reload(); |
||||
|
} |
||||
|
|
||||
|
reload() { |
||||
|
this.userDashboardsInfo = null; |
||||
|
this.cd.markForCheck(); |
||||
|
(this.authUser.authority !== Authority.SYS_ADMIN ? |
||||
|
this.userSettingService.getUserDashboardsInfo() : of({last: [], starred: []})).subscribe( |
||||
|
(userDashboardsInfo) => { |
||||
|
this.userDashboardsInfo = userDashboardsInfo; |
||||
|
for (const starredDashboard of this.userDashboardsInfo?.starred) { |
||||
|
starredDashboard.starred = true; |
||||
|
} |
||||
|
this.userDashboardsInfo?.starred.sort((a, b) => a.starredAt - b.starredAt); |
||||
|
if (this.hasLastVisitedDashboards()) { |
||||
|
this.initLastVisitedDashboardsDataSource(); |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
toggleValueChange(value: 'last' | 'starred') { |
||||
|
this.toggleValue = value; |
||||
|
if (this.dirty) { |
||||
|
this.dirty = false; |
||||
|
this.reload(); |
||||
|
} else { |
||||
|
if (value === 'last' && this.hasLastVisitedDashboards()) { |
||||
|
this.initLastVisitedDashboardsDataSource(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private initLastVisitedDashboardsDataSource() { |
||||
|
this.lastVisitedDashboardsDataSource = new LastVisitedDashboardsDataSource(this.userDashboardsInfo.last); |
||||
|
const sortOrder: SortOrder = { |
||||
|
property: 'lastVisited', |
||||
|
direction: Direction.DESC |
||||
|
}; |
||||
|
this.lastVisitedDashboardsPageLink = new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, sortOrder); |
||||
|
this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.lastVisitedDashboardsSort.changes.subscribe(() => { |
||||
|
if (this.lastVisitedDashboardsSort.length) { |
||||
|
this.lastVisitedDashboardsSort.get(0).sortChange.subscribe(() => |
||||
|
this.updateLastVisitedDashboardsData(this.lastVisitedDashboardsSort.get(0))); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updateLastVisitedDashboardsData(sort: MatSort) { |
||||
|
this.lastVisitedDashboardsPageLink.sortOrder.property = sort.active; |
||||
|
this.lastVisitedDashboardsPageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; |
||||
|
this.lastVisitedDashboardsDataSource.loadData(this.lastVisitedDashboardsPageLink); |
||||
|
} |
||||
|
|
||||
|
hasLastVisitedDashboards(): boolean { |
||||
|
return !!(this.userDashboardsInfo && this.userDashboardsInfo.last && this.userDashboardsInfo.last.length); |
||||
|
} |
||||
|
|
||||
|
toggleDashboardStar(dashboard: AbstractUserDashboardInfo): void { |
||||
|
const action: UserDashboardAction = dashboard.starred ? UserDashboardAction.UNSTAR : UserDashboardAction.STAR; |
||||
|
dashboard.starred = !dashboard.starred; |
||||
|
this.userSettingService.reportUserDashboardAction(dashboard.id, action, {ignoreLoading: true}).subscribe(); |
||||
|
this.dirty = true; |
||||
|
if (this.toggleValue === 'starred') { |
||||
|
const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id); |
||||
|
if (index > -1) { |
||||
|
this.userDashboardsInfo.starred.splice(index, 1); |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onStarDashboard(dashboard: DashboardInfo) { |
||||
|
if (dashboard) { |
||||
|
this.starDashboardAutocomplete.clear(); |
||||
|
const index = this.userDashboardsInfo.starred.findIndex((d) => d.id === dashboard.id.id); |
||||
|
if (index === -1) { |
||||
|
const starredDashboard: StarredDashboardInfo = { |
||||
|
starredAt: Date.now(), |
||||
|
id: dashboard.id.id, |
||||
|
starred: true, |
||||
|
title: dashboard.title |
||||
|
}; |
||||
|
this.userDashboardsInfo.starred.push(starredDashboard); |
||||
|
this.userSettingService.reportUserDashboardAction(dashboard.id.id, UserDashboardAction.STAR, {ignoreLoading: true}).subscribe(); |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export class LastVisitedDashboardsDataSource implements DataSource<LastVisitedDashboardInfo> { |
||||
|
|
||||
|
private lastVisitedDashboardsSubject = new BehaviorSubject<LastVisitedDashboardInfo[]>([]); |
||||
|
private pageDataSubject = new BehaviorSubject<PageData<LastVisitedDashboardInfo>>(emptyPageData<LastVisitedDashboardInfo>()); |
||||
|
|
||||
|
public pageData$ = this.pageDataSubject.asObservable(); |
||||
|
|
||||
|
constructor(private lastVisitedDashboards: Array<LastVisitedDashboardInfo>) { |
||||
|
} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<LastVisitedDashboardInfo[] | ReadonlyArray<LastVisitedDashboardInfo>> { |
||||
|
return this.lastVisitedDashboardsSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.lastVisitedDashboardsSubject.complete(); |
||||
|
this.pageDataSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadData(pageLink: PageLink): void { |
||||
|
const result = pageLink.filterData(this.lastVisitedDashboards); |
||||
|
this.lastVisitedDashboardsSubject.next(result.data); |
||||
|
this.pageDataSubject.next(result); |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.lastVisitedDashboardsSubject.pipe( |
||||
|
map((entities) => !entities.length), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.pageDataSubject.pipe( |
||||
|
map((pageData) => pageData.totalElements), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header" [name]="name" [(ngModel)]="value" |
||||
|
(ngModelChange)="valueChange.emit(value)"> |
||||
|
<mat-button-toggle *ngFor="let option of options" [value]="option.value">{{ option.name }}</mat-button-toggle> |
||||
|
</mat-button-toggle-group> |
||||
|
<ng-template #select> |
||||
|
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic"> |
||||
|
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)"> |
||||
|
<mat-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</ng-template> |
||||
@ -0,0 +1,134 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
@import "../../../../../../../scss/constants"; |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { |
||||
|
width: 100%; |
||||
|
border-radius: 100px; |
||||
|
height: 32px; |
||||
|
padding: 2px; |
||||
|
border: none; |
||||
|
background: rgba(0, 0, 0, 0.06); |
||||
|
.mat-button-toggle + .mat-button-toggle { |
||||
|
border-left: none; |
||||
|
} |
||||
|
.mat-button-toggle.mat-button-toggle-appearance-standard { |
||||
|
flex: 1; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
background: transparent; |
||||
|
|
||||
|
.mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { |
||||
|
border-radius: 20px; |
||||
|
} |
||||
|
.mat-button-toggle-button { |
||||
|
height: 28px; |
||||
|
.mat-button-toggle-label-content { |
||||
|
line-height: 26px; |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
letter-spacing: 0.2px; |
||||
|
} |
||||
|
} |
||||
|
&.mat-button-toggle-checked { |
||||
|
.mat-button-toggle-button { |
||||
|
background: #F3F6FA; |
||||
|
color: #305680; |
||||
|
border: 1px solid #305680; |
||||
|
border-radius: 20px; |
||||
|
|
||||
|
.mat-button-toggle-label-content { |
||||
|
font-weight: 500; |
||||
|
line-height: 24px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
@media #{$mat-md-lg} { |
||||
|
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { |
||||
|
height: 24px; |
||||
|
.mat-button-toggle.mat-button-toggle-appearance-standard { |
||||
|
.mat-button-toggle-button { |
||||
|
height: 20px; |
||||
|
display: grid; |
||||
|
.mat-button-toggle-label-content { |
||||
|
line-height: 20px; |
||||
|
font-size: 10px; |
||||
|
padding: 0 2px; |
||||
|
} |
||||
|
} |
||||
|
&.mat-button-toggle-checked { |
||||
|
.mat-button-toggle-button { |
||||
|
.mat-button-toggle-label-content { |
||||
|
line-height: 18px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-toggle-header-select { |
||||
|
&.mat-mdc-form-field { |
||||
|
line-height: 16px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix { |
||||
|
min-height: 0; |
||||
|
width: auto; |
||||
|
padding-top: 8px; |
||||
|
padding-bottom: 8px; |
||||
|
} |
||||
|
.mdc-text-field--outlined { |
||||
|
padding-left: 8px; |
||||
|
padding-right: 6px; |
||||
|
} |
||||
|
.mat-mdc-select { |
||||
|
font-weight: 400; |
||||
|
font-size: 12px; |
||||
|
line-height: 16px; |
||||
|
letter-spacing: 0.25px; |
||||
|
} |
||||
|
.mat-mdc-select-value { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
.mat-mdc-select-arrow-wrapper { |
||||
|
height: 12px; |
||||
|
padding-left: 6px; |
||||
|
} |
||||
|
.mat-mdc-select-arrow { |
||||
|
width: 12px; |
||||
|
height: 12px; |
||||
|
& > svg { |
||||
|
display: none; |
||||
|
} |
||||
|
&:after { |
||||
|
font-family: 'Material Icons'; |
||||
|
content: "expand_more"; |
||||
|
position: absolute; |
||||
|
display: inline-block; |
||||
|
font-size: 20px; |
||||
|
line-height: 12px; |
||||
|
top: 0; |
||||
|
left: -6px; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
AfterContentInit, |
||||
|
AfterViewInit, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ContentChildren, EventEmitter, |
||||
|
Input, |
||||
|
OnInit, Output, |
||||
|
QueryList, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { AdminService } from '@core/http/admin.service'; |
||||
|
import { UpdateMessage } from '@shared/models/settings.models'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { of, Subscription } from 'rxjs'; |
||||
|
import { MatStepper } from '@angular/material/stepper'; |
||||
|
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; |
||||
|
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; |
||||
|
import { MediaBreakpoints } from '@shared/models/constants'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coerce-boolean'; |
||||
|
|
||||
|
export interface ToggleHeaderOption { |
||||
|
name: string; |
||||
|
value: any; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-toggle-header', |
||||
|
templateUrl: './toggle-header.component.html', |
||||
|
styleUrls: ['./toggle-header.component.scss'] |
||||
|
}) |
||||
|
export class ToggleHeaderComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
value: any; |
||||
|
|
||||
|
@Output() |
||||
|
valueChange = new EventEmitter<any>(); |
||||
|
|
||||
|
@Input() |
||||
|
options: ToggleHeaderOption[]; |
||||
|
|
||||
|
@Input() |
||||
|
name: string; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
useSelectOnMdLg = true; |
||||
|
|
||||
|
isMdLg: boolean; |
||||
|
|
||||
|
private observeBreakpointSubscription: Subscription; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private breakpointObserver: BreakpointObserver) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); |
||||
|
this.observeBreakpointSubscription = this.breakpointObserver |
||||
|
.observe(MediaBreakpoints['md-lg']) |
||||
|
.subscribe((state: BreakpointState) => { |
||||
|
this.isMdLg = state.matches; |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px"> |
||||
|
<div class="tb-card-header"> |
||||
|
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a> |
||||
|
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[ |
||||
|
{ |
||||
|
name: ctx.translate.instant('widgets.usage-info.entities'), |
||||
|
value: 'entities' |
||||
|
}, |
||||
|
{ |
||||
|
name: ctx.translate.instant('widgets.usage-info.api-calls'), |
||||
|
value: 'apiCalls' |
||||
|
} |
||||
|
]"> |
||||
|
</tb-toggle-header> |
||||
|
</div> |
||||
|
<ng-container [ngSwitch]="usageToggle.value"> |
||||
|
<ng-template [ngSwitchCase]="'entities'"> |
||||
|
<div fxFlex class="tb-usage-list"> |
||||
|
<div class="tb-usage-items"> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div> |
||||
|
</div> |
||||
|
<div class="tb-usage-items-values"> |
||||
|
<div class="tb-usage-items-counts"> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.dashboards}">{{ usageInfo?.dashboards | shortNumber }} / {{ maxValue(usageInfo?.maxDashboards) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.customers}">{{ usageInfo?.customers | shortNumber }} / {{ maxValue(usageInfo?.maxCustomers) }}</div> |
||||
|
</div> |
||||
|
<div class="tb-usage-items-progress"> |
||||
|
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="'apiCalls'"> |
||||
|
<div fxFlex class="tb-usage-list"> |
||||
|
<div class="tb-usage-items"> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.transportMessages}" translate>api-usage.transport-messages</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" translate>api-usage.javascript</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.alarms}" translate>api-usage.alarms-created</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.emails}" translate>api-usage.email</div> |
||||
|
<div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</div> |
||||
|
</div> |
||||
|
<div class="tb-usage-items-values"> |
||||
|
<div class="tb-usage-items-counts"> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.emails}">{{ usageInfo?.emails | shortNumber }} / {{ maxValue(usageInfo?.maxEmails) }}</div> |
||||
|
<div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.sms}">{{ usageInfo?.sms | shortNumber }} / {{ maxValue(usageInfo?.maxSms) }}</div> |
||||
|
</div> |
||||
|
<div class="tb-usage-items-progress"> |
||||
|
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar> |
||||
|
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</ng-container> |
||||
|
</div> |
||||
@ -0,0 +1,113 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import "../../../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
.tb-card-header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
@media #{$mat-lt-md} { |
||||
|
flex-direction: column; |
||||
|
align-items: flex-start; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-usage-list { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.tb-usage-items, .tb-usage-items-counts, .tb-usage-items-progress { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-evenly; |
||||
|
align-items: flex-start; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.tb-usage-items-progress { |
||||
|
width: 34px; |
||||
|
@media #{$mat-md} { |
||||
|
display: none; |
||||
|
} |
||||
|
.mdc-linear-progress { |
||||
|
height: 8px; |
||||
|
margin-top: 6px; |
||||
|
margin-bottom: 6px; |
||||
|
border-radius: 2px; |
||||
|
@media #{$mat-md-lg} { |
||||
|
margin-top: 4px; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-usage-items-values { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: stretch; |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.tb-usage-item, .tb-usage-item-counts { |
||||
|
font-weight: 400; |
||||
|
font-size: 14px; |
||||
|
line-height: 20px; |
||||
|
letter-spacing: 0.2px; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
width: 100%; |
||||
|
@media #{$mat-md-lg} { |
||||
|
font-size: 11px; |
||||
|
line-height: 16px; |
||||
|
} |
||||
|
&.critical { |
||||
|
color: #D12730; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-usage-item { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
|
||||
|
.tb-usage-item-counts { |
||||
|
color: rgba(0, 0, 0, 0.76); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-usage-items-progress { |
||||
|
.mat-mdc-progress-bar { |
||||
|
.mdc-linear-progress__bar-inner { |
||||
|
border-top-width: 8px; |
||||
|
} |
||||
|
&.critical { |
||||
|
.mdc-linear-progress__buffer-bar { |
||||
|
background: rgba(209, 39, 48, 0.06); |
||||
|
} |
||||
|
.mdc-linear-progress__bar-inner { |
||||
|
border-top-color: #D12730; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { of } from 'rxjs'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { UsageInfo } from '@shared/models/usage.models'; |
||||
|
import { UsageInfoService } from '@core/http/usage-info.service'; |
||||
|
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-usage-info-widget', |
||||
|
templateUrl: './usage-info-widget.component.html', |
||||
|
styleUrls: ['./home-page-widget.scss', './usage-info-widget.component.scss'] |
||||
|
}) |
||||
|
export class UsageInfoWidgetComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
usageInfo: UsageInfo; |
||||
|
authUser = getCurrentAuthUser(this.store); |
||||
|
|
||||
|
toggleValue: 'entities' | 'apiCalls' = 'entities'; |
||||
|
|
||||
|
entityItemCritical: {[key: string]: boolean} = {}; |
||||
|
apiCallItemCritical: {[key: string]: boolean} = {}; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private shortNumberPipe: ShortNumberPipe, |
||||
|
private usageInfoService: UsageInfoService) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
(this.authUser.authority === Authority.TENANT_ADMIN ? |
||||
|
this.usageInfoService.getUsageInfo() : of(null)).subscribe( |
||||
|
(usageInfo) => { |
||||
|
this.usageInfo = usageInfo; |
||||
|
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices); |
||||
|
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets); |
||||
|
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers); |
||||
|
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards); |
||||
|
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers); |
||||
|
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages, |
||||
|
this.usageInfo?.maxTransportMessages); |
||||
|
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions); |
||||
|
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms); |
||||
|
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails); |
||||
|
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms); |
||||
|
let entitiesHasCriticalItem = false; |
||||
|
let apiCallsHasCriticalItem = false; |
||||
|
for (const key of Object.keys(this.entityItemCritical)) { |
||||
|
if (this.entityItemCritical[key]) { |
||||
|
entitiesHasCriticalItem = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
for (const key of Object.keys(this.apiCallItemCritical)) { |
||||
|
if (this.apiCallItemCritical[key]) { |
||||
|
apiCallsHasCriticalItem = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) { |
||||
|
this.toggleValue = 'apiCalls'; |
||||
|
} |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
maxValue(max: number): number | string { |
||||
|
return max ? this.shortNumberPipe.transform(max) : '∞'; |
||||
|
} |
||||
|
|
||||
|
progressValue(value: number, max: number): number { |
||||
|
if (max && value) { |
||||
|
return (value / max) * 100; |
||||
|
} |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
private isItemCritical(value: number, max: number): boolean { |
||||
|
if (max && value) { |
||||
|
return (value / max) >= 0.85; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2023 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<section class="tb-widget-settings" [formGroup]="quickLinksWidgetSettingsForm" fxLayout="column"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label translate>widgets.quick-links.columns</mat-label> |
||||
|
<input required matInput type="number" step="1" min="1" max="20" formControlName="columns"> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
@ -0,0 +1,52 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2023 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-quick-links-widget-settings', |
||||
|
templateUrl: './quick-links-widget-settings.component.html', |
||||
|
styleUrls: ['./../widget-settings.scss'] |
||||
|
}) |
||||
|
export class QuickLinksWidgetSettingsComponent extends WidgetSettingsComponent { |
||||
|
|
||||
|
quickLinksWidgetSettingsForm: UntypedFormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: UntypedFormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
protected settingsForm(): UntypedFormGroup { |
||||
|
return this.quickLinksWidgetSettingsForm; |
||||
|
} |
||||
|
|
||||
|
protected defaultSettings(): WidgetSettings { |
||||
|
return { |
||||
|
columns: 3 |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected onSettingsSet(settings: WidgetSettings) { |
||||
|
this.quickLinksWidgetSettingsForm = this.fb.group({ |
||||
|
columns: [settings.columns, [Validators.required, Validators.min(1), Validators.max(20)]] |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue