Browse Source

Merge branch 'feature/device-info-filter' of github.com:thingsboard/thingsboard into feature/device-info-filter

pull/8455/head
Andrii Shvaika 3 years ago
parent
commit
a36a0be5fb
  1. 7
      ui-ngx/src/app/core/http/device.service.ts
  2. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  3. 74
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html
  4. 35
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss
  5. 254
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts
  6. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  7. 2
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  8. 46
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  9. 165
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  10. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
  11. 137
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
  12. 68
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts
  13. 10
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html
  14. 6
      ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts
  15. 121
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  16. 6
      ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw
  17. 127
      ui-ngx/src/app/shared/models/device.models.ts
  18. 22
      ui-ngx/src/assets/home/no_data_folder_bg.svg
  19. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  20. 2
      ui-ngx/src/styles.scss

7
ui-ngx/src/app/core/http/device.service.ts

@ -25,7 +25,7 @@ import {
ClaimResult,
Device,
DeviceCredentials,
DeviceInfo,
DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -42,6 +42,11 @@ export class DeviceService {
private http: HttpClient
) { }
public getDeviceInfosByQuery(deviceInfoQuery: DeviceInfoQuery, config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api${deviceInfoQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getTenantDeviceInfos(pageLink: PageLink, type: string = '',
config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`,

2
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts

@ -190,7 +190,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-alarm-filter-panel',
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',

74
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html

@ -0,0 +1,74 @@
<!--
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="deviceFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else deviceFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleDeviceFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #deviceFilterPanel>
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="deviceFilter"></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]="deviceInfoFilterForm.invalid || !deviceInfoFilterForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #deviceFilter>
<div fxLayout="column" fxLayoutGap="16px" style="min-width: 280px;" [formGroup]="deviceInfoFilterForm">
<tb-device-profile-autocomplete
subscriptSizing="dynamic"
formControlName="deviceProfileId"
[displayAllOnEmpty]="true"
(deviceProfileChanged)="deviceProfileChanged($event)"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<mat-form-field>
<mat-label translate>device.device-state</mat-label>
<mat-select formControlName="active">
<mat-option value="">
{{ 'device.any' | translate }}
</mat-option>
<mat-option [value]="true">
{{ 'device.active' | translate }}
</mat-option>
<mat-option [value]="false">
{{ 'device.inactive' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>

35
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: block;
overflow: hidden;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

254
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.ts

@ -0,0 +1,254 @@
///
/// 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,
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 { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';
import { DeviceInfoFilter } from '@shared/models/device.models';
import { isDefinedAndNotNull } from '@core/utils';
import { EntityInfoData } from '@shared/models/entity.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
export const DEVICE_FILTER_CONFIG_DATA = new InjectionToken<any>('DeviceFilterConfigData');
export interface DeviceFilterConfigData {
panelMode: boolean;
deviceInfoFilter: DeviceInfoFilter;
}
// @dynamic
@Component({
selector: 'tb-device-info-filter',
templateUrl: './device-info-filter.component.html',
styleUrls: ['./device-info-filter.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceInfoFilterComponent),
multi: true
}
]
})
export class DeviceInfoFilterComponent implements OnInit, OnDestroy, ControlValueAccessor {
@ViewChild('deviceFilterPanel')
deviceFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
panelMode = false;
buttonDisplayValue = this.translate.instant('device.device-filter');
deviceInfoFilterForm: UntypedFormGroup;
deviceFilterOverlayRef: OverlayRef;
panelResult: DeviceInfoFilter = null;
private deviceProfileInfo: EntityInfoData;
private deviceInfoFilter: DeviceInfoFilter;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(DEVICE_FILTER_CONFIG_DATA)
private data: DeviceFilterConfigData | undefined,
@Optional()
private overlayRef: OverlayRef,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private deviceProfileService: DeviceProfileService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.deviceInfoFilter = this.data.deviceInfoFilter;
}
this.deviceInfoFilterForm = this.fb.group({
deviceProfileId: [null, []],
active: ['', []]
});
this.deviceInfoFilterForm.valueChanges.subscribe(
() => {
this.updateValidators();
if (!this.buttonMode) {
this.deviceFilterUpdated(this.deviceInfoFilterForm.value);
}
}
);
if (this.panelMode) {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
}
}
ngOnDestroy(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceInfoFilterForm.disable({emitEvent: false});
} else {
this.deviceInfoFilterForm.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(deviceInfoFilter?: DeviceInfoFilter): void {
this.deviceInfoFilter = deviceInfoFilter;
this.updateButtonDisplayValue();
this.updateDeviceInfoFilterForm(deviceInfoFilter);
}
private updateValidators() {
}
toggleDeviceFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-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.deviceFilterOverlayRef = this.overlay.create(config);
this.deviceFilterOverlayRef.backdropClick().subscribe(() => {
this.deviceFilterOverlayRef.dispose();
});
this.deviceFilterOverlayRef.attach(new TemplatePortal(this.deviceFilterPanel,
this.viewContainerRef));
}
cancel() {
this.updateDeviceInfoFilterForm(this.deviceInfoFilter);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.deviceFilterOverlayRef.dispose();
}
}
update() {
this.deviceFilterUpdated(this.deviceInfoFilterForm.value);
if (this.panelMode) {
this.panelResult = this.deviceInfoFilter;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.deviceFilterOverlayRef.dispose();
}
}
deviceProfileChanged(deviceProfileInfo: EntityInfoData) {
this.deviceProfileInfo = deviceProfileInfo;
this.updateButtonDisplayValue();
}
private updateDeviceInfoFilterForm(deviceInfoFilter?: DeviceInfoFilter) {
this.deviceInfoFilterForm.patchValue({
deviceProfileId: deviceInfoFilter?.deviceProfileId,
active: isDefinedAndNotNull(deviceInfoFilter?.active) ? deviceInfoFilter?.active : ''
}, {emitEvent: false});
this.updateValidators();
}
private deviceFilterUpdated(deviceInfoFilter: DeviceInfoFilter) {
this.deviceInfoFilter = deviceInfoFilter;
if ((this.deviceInfoFilter.active as any) === '') {
this.deviceInfoFilter.active = null;
}
this.updateButtonDisplayValue();
this.propagateChange(this.deviceInfoFilter);
}
private updateButtonDisplayValue() {
if (this.buttonMode) {
const filterTextParts: string[] = [];
if (isDefinedAndNotNull(this.deviceInfoFilter?.deviceProfileId)) {
if (!this.deviceProfileInfo) {
this.deviceProfileService.getDeviceProfileInfo(this.deviceInfoFilter?.deviceProfileId.id,
{ignoreLoading: true, ignoreErrors: true}).subscribe(
(deviceProfileInfo) => {
this.deviceProfileChanged(deviceProfileInfo);
});
return;
} else {
filterTextParts.push(this.deviceProfileInfo.name);
}
}
if (isDefinedAndNotNull(this.deviceInfoFilter?.active)) {
const translationKey = this.deviceInfoFilter?.active ? 'device.active' : 'device.inactive';
filterTextParts.push(this.translate.instant(translationKey));
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('device.device-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('device.filter-title') + `: ${filterTextParts.join(', ')}`;
}
this.cd.detectChanges();
}
}
}

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -181,6 +181,7 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
@NgModule({
declarations:
@ -282,6 +283,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
AssetProfileComponent,
AssetProfileDialogComponent,
AssetProfileAutocompleteComponent,
@ -425,6 +427,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
AssetProfileComponent,

2
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -591,7 +591,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.panelClass = 'tb-alarm-filter-panel';
config.panelClass = 'tb-filter-panel';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',

46
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss

@ -67,4 +67,50 @@
color: inherit;
}
}
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
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;
}
}
}

165
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html

@ -16,91 +16,102 @@
-->
<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>
<ng-container *ngIf="hasDashboardsAccess; else noDataAvailable">
<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>
<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-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>
</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>
<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>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
</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>
<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>
<ng-template #noDataAvailable>
<div class="tb-card-header">
<div class="tb-title">{{ 'widgets.recent-dashboards.title' | translate }}</div>
</div>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>widgets.home.no-data-available</div>
</div>
</ng-template>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts

@ -74,6 +74,7 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
lastVisitedDashboardsPageLink: PageLink;
starredDashboardValue = null;
hasDashboardsAccess = true;
dirty = false;
@ -84,7 +85,10 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
}
ngOnInit() {
this.reload();
this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority);
if (this.hasDashboardsAccess) {
this.reload();
}
}
reload() {

137
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html

@ -16,74 +16,85 @@
-->
<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>
<ng-container *ngIf="hasUsageInfoAccess; else noDataAvailable">
<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-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 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>
</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>
</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-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 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>
</div>
</ng-template>
</ng-template>
</ng-container>
</ng-container>
<ng-template #noDataAvailable>
<div class="tb-card-header">
<div class="tb-title">{{ 'widgets.usage-info.title' | translate }}</div>
</div>
<div class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>widgets.home.no-data-available</div>
</div>
</ng-template>
</div>

68
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.ts

@ -44,6 +44,8 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
entityItemCritical: {[key: string]: boolean} = {};
apiCallItemCritical: {[key: string]: boolean} = {};
hasUsageInfoAccess = true;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private shortNumberPipe: ShortNumberPipe,
@ -52,41 +54,43 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
}
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;
this.hasUsageInfoAccess = this.authUser.authority === Authority.TENANT_ADMIN;
if (this.hasUsageInfoAccess) {
this.usageInfoService.getUsageInfo().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;
for (const key of Object.keys(this.apiCallItemCritical)) {
if (this.apiCallItemCritical[key]) {
apiCallsHasCriticalItem = true;
break;
}
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
}
);
);
}
}
maxValue(max: number): number | string {

10
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.html

@ -15,10 +15,6 @@
limitations under the License.
-->
<tb-device-profile-autocomplete
subscriptSizing="dynamic"
[ngModel]="entitiesTableConfig.componentsData.deviceProfileId"
(ngModelChange)="deviceProfileChanged($event)"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<tb-device-info-filter [ngModel]="entitiesTableConfig.componentsData.deviceInfoFilter"
(ngModelChange)="deviceInfoFilterChanged($event)">
</tb-device-info-filter>

6
ui-ngx/src/app/modules/home/pages/device/device-table-header.component.ts

@ -18,7 +18,7 @@ import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { DeviceInfo } from '@app/shared/models/device.models';
import { DeviceInfo, DeviceInfoFilter } from '@app/shared/models/device.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id';
@ -35,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<Devic
super(store);
}
deviceProfileChanged(deviceProfileId: DeviceProfileId) {
this.entitiesTableConfig.componentsData.deviceProfileId = deviceProfileId;
deviceInfoFilterChanged(deviceInfoFilter: DeviceInfoFilter) {
this.entitiesTableConfig.componentsData.deviceInfoFilter = deviceInfoFilter;
this.entitiesTableConfig.getTable().resetSortAndFilter(true);
}

121
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import {
CellActionDescriptor,
checkBoxCell,
@ -30,7 +30,13 @@ import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models';
import {
Device,
DeviceCredentials,
DeviceInfo,
DeviceInfoFilter,
DeviceInfoQuery
} from '@app/shared/models/device.models';
import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { select, Store } from '@ngrx/store';
@ -63,12 +69,21 @@ import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { BaseData, HasId } from '@shared/models/base-data';
import { isDefinedAndNotNull } from '@core/utils';
import { deepClone, isDefined, isDefinedAndNotNull } from '@core/utils';
import { EdgeService } from '@core/http/edge.service';
import {
AddEntitiesToEdgeDialogComponent,
AddEntitiesToEdgeDialogData
} from '@home/dialogs/add-entities-to-edge-dialog.component';
import { EdgeId } from '@shared/models/id/edge-id';
import { CustomerId } from '@shared/models/id/customer-id';
import { PageLink, PageQueryParam } from '@shared/models/page/page-link';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
interface DevicePageQueryParams extends PageQueryParam {
deviceProfileId?: string;
active?: boolean | string;
}
@Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
@ -103,17 +118,16 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text');
this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
this.config.saveEntity = device => {
return this.deviceService.saveDevice(device).pipe(
this.config.saveEntity = device => this.deviceService.saveDevice(device).pipe(
tap(() => {
this.broadcast.broadcast('deviceSaved');
}),
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
));
};
this.config.onEntityAction = action => this.onDeviceAction(action, this.config);
this.config.detailsReadonly = () =>
(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.onLoadAction = (route) => this.onLoadAction(route);
this.config.headerComponent = DeviceTableHeaderComponent;
@ -123,7 +137,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
const routeParams = route.params;
this.config.componentsData = {
deviceScope: route.data.devicesType,
deviceProfileId: null,
deviceInfoFilter: {},
deviceCredentials$: new Subject<DeviceCredentials>(),
edgeId: routeParams.edgeId
};
@ -162,7 +176,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' ||
this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config;
@ -170,48 +185,97 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
);
}
onLoadAction(route: ActivatedRoute): void {
const routerQueryParams: DevicePageQueryParams = route.snapshot.queryParams;
if (routerQueryParams) {
const queryParams = deepClone(routerQueryParams);
let replaceUrl = false;
if (routerQueryParams?.deviceProfileId) {
this.config.componentsData.deviceInfoFilter.deviceProfileId = new DeviceProfileId(routerQueryParams?.deviceProfileId);
delete queryParams.deviceProfileId;
replaceUrl = true;
}
if (isDefined(routerQueryParams?.active)) {
this.config.componentsData.deviceInfoFilter.active = (routerQueryParams?.active === true || routerQueryParams?.active === 'true');
delete queryParams.active;
replaceUrl = true;
}
if (replaceUrl) {
this.router.navigate([], {
relativeTo: route,
queryParams,
queryParamsHandling: '',
replaceUrl: true
});
}
}
}
configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
const columns: Array<EntityTableColumn<DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'),
new EntityTableColumn<DeviceInfo>('deviceProfileName', 'device-profile.device-profile', '25%'),
new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%')
new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%'),
new EntityTableColumn<DeviceInfo>('active', 'device.state', '80px',
entity => this.deviceState(entity), entity => this.deviceStateStyle(entity))
];
if (deviceScope === 'tenant') {
columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);
}, () => ({}), false),
entity => checkBoxCell(entity.customerIsPublic), () => ({})),
);
}
columns.push(
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
entity => {
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
}, () => ({}), false)
entity => checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway), () => ({}), false)
);
return columns;
}
private deviceState(device: DeviceInfo): string {
let translateKey = 'device.active';
let backgroundColor = 'rgba(25, 128, 56, 0.08)';
if (!device.active) {
translateKey = 'device.inactive';
backgroundColor = 'rgba(209, 39, 48, 0.08)';
}
return `<div class="status" style="border-radius: 16px; height: 32px;
line-height: 32px; padding: 0 12px; width: fit-content; background-color: ${backgroundColor}">
${this.translate.instant(translateKey)}
</div>`;
}
private deviceStateStyle(device: DeviceInfo): object {
const styleObj = {
fontSize: '14px',
color: '#198038',
cursor: 'pointer'
};
if (!device.active) {
styleObj.color = '#d12730';
}
return styleObj;
}
configureEntityFunctions(deviceScope: string): void {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getDeviceInfosByQuery(this.prepareDeviceInfoQuery(pageLink));
if (deviceScope === 'tenant') {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getTenantDeviceInfosByDeviceProfileId(pageLink,
this.config.componentsData.deviceProfileId !== null ?
this.config.componentsData.deviceProfileId.id : '');
this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id);
} else if (deviceScope === 'edge' || deviceScope === 'edge_customer_user') {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getEdgeDevices(this.config.componentsData.edgeId, pageLink, this.config.componentsData.edgeType);
} else {
this.config.entitiesFetchFunction = pageLink =>
this.deviceService.getCustomerDeviceInfosByDeviceProfileId(this.customerId, pageLink,
this.config.componentsData.deviceProfileId !== null ?
this.config.componentsData.deviceProfileId.id : '');
this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id);
this.config.deleteEntity = () => of();
}
}
prepareDeviceInfoQuery(pageLink: PageLink): DeviceInfoQuery {
const deviceInfoFilter: DeviceInfoFilter = deepClone(this.config.componentsData.deviceInfoFilter);
if (this.config.componentsData.deviceScope === 'edge' || this.config.componentsData.deviceScope === 'edge_customer_user') {
deviceInfoFilter.edgeId = new EdgeId(this.config.componentsData.edgeId);
} else if (this.config.componentsData.deviceScope !== 'tenant') {
deviceInfoFilter.customerId = new CustomerId(this.customerId);
}
return new DeviceInfoQuery(pageLink, deviceInfoFilter);
}
configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> {
@ -540,7 +604,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
data: {
deviceId: device.id.id,
deviceProfileId: device.deviceProfileId.id,
isReadOnly: this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'
isReadOnly: this.config.componentsData.deviceScope === 'customer_user' ||
this.config.componentsData.deviceScope === 'edge_customer_user'
}
}).afterClosed().subscribe(deviceCredentials => {
if (isDefinedAndNotNull(deviceCredentials)) {

6
ui-ngx/src/app/modules/home/pages/home-links/tenant_admin_home_page.raw

@ -648,9 +648,9 @@
"padding": "16px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div fxLayout=\"row\" fxLayoutGap=\"12px\">\n <a fxHide.md mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n <a mat-flat-button color=\"primary\" routerLink=\"/entities/devices\" [queryParams]=\"{action: 'add'}\">{{ 'device.add' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/entities/devices\">{{ 'device.devices' | translate }}</a>\n </div>\n <div fxLayout=\"row\" fxLayoutGap=\"12px\">\n <a fxHide.md mat-stroked-button color=\"primary\" href=\"https://thingsboard.io/docs\" target=\"_blank\">{{ 'widgets.devices.view-docs' | translate }}</a>\n <a mat-flat-button color=\"primary\" routerLink=\"/entities/devices\" [queryParams]=\"{action: 'add'}\">{{ 'device.add' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-inactive\" routerLink=\"/entities/devices\" [queryParams]=\"{active: false}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.inactive</div>\n </div>\n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${inactiveDevices:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-active\" routerLink=\"/entities/devices\" [queryParams]=\"{active: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.active</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${activeDevices:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/entities/devices\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.devices.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalDevices:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n"
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n"
},
"title": "Devices",
"showTitleIcon": false,
@ -804,7 +804,7 @@
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div class=\"tb-card-content\">\n <div class=\"tb-content-container\">\n <div class=\"tb-card-header\">\n <div class=\"tb-card-title\">\n <a class=\"tb-home-widget-title tb-home-widget-link\" routerLink=\"/alarms\">{{ 'alarm.alarms' | translate }}</a>\n </div>\n </div>\n <div class=\"tb-item-cards\">\n <a class=\"tb-item-card tb-critical\" routerLink=\"/alarms\" [queryParams]=\"{severityList: ['CRITICAL']}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.critical</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${criticalAlarms:0}</div>\n </div>\n </a>\n <a class=\"tb-item-card tb-assigned\" routerLink=\"/alarms\" [queryParams]=\"{assignedToMe: true}\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.assigned-to-me</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${assignedToMeAlarms:0}</div>\n </div>\n </a>\n <a fxHide.md class=\"tb-item-card tb-total\" routerLink=\"/alarms\">\n <div class=\"tb-item-title-container\">\n <div class=\"tb-item-title tb-home-widget-link\" translate>widgets.alarms.total</div>\n </div> \n <div class=\"tb-count-container\">\n <div class=\"tb-count\">${totalAlarms:0}</div>\n </div>\n </a>\n </div>\n </div>\n</div>",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n"
"markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n overflow: hidden;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n overflow: hidden;\n justify-content: space-evenly;\n}\n\na.tb-item-card.tb-critical {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-assigned {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\na.tb-item-card.tb-critical .tb-count:after {\n content: \"warning\";\n display: inline-block;\n position: relative;\n font-family: 'Material Icons Round';\n color: #D12730;\n vertical-align: bottom;\n margin-left: 6px;\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n a.tb-item-card.tb-critical .tb-count:after {\n margin-left: 2px;\n }\n}\n"
},
"title": "Alarms",
"showTitleIcon": false,

127
ui-ngx/src/app/shared/models/device.models.ts

@ -35,6 +35,9 @@ import {
getDefaultProfileObserveAttrConfig,
PowerMode
} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { PageLink } from '@shared/models/page/page-link';
import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
import { EdgeId } from '@shared/models/id/edge-id';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT',
@ -328,7 +331,7 @@ export interface DeviceProvisionConfiguration {
allowCreateNewDevicesByX509Certificate?: boolean;
}
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration {
export const createDeviceProfileConfiguration = (type: DeviceProfileType): DeviceProfileConfiguration => {
let configuration: DeviceProfileConfiguration = null;
if (type) {
switch (type) {
@ -339,9 +342,9 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic
}
}
return configuration;
}
};
export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration {
export const createDeviceConfiguration = (type: DeviceProfileType): DeviceConfiguration => {
let configuration: DeviceConfiguration = null;
if (type) {
switch (type) {
@ -352,9 +355,9 @@ export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfig
}
}
return configuration;
}
};
export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration {
export const createDeviceProfileTransportConfiguration = (type: DeviceTransportType): DeviceProfileTransportConfiguration => {
let transportConfiguration: DeviceProfileTransportConfiguration = null;
if (type) {
switch (type) {
@ -408,9 +411,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
}
}
return transportConfiguration;
}
};
export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration {
export const createDeviceTransportConfiguration = (type: DeviceTransportType): DeviceTransportConfiguration => {
let transportConfiguration: DeviceTransportConfiguration = null;
if (type) {
switch (type) {
@ -446,7 +449,7 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
}
}
return transportConfiguration;
}
};
export enum AlarmConditionType {
SIMPLE = 'SIMPLE',
@ -489,7 +492,7 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
export interface AlarmSchedule{
dynamicValue?: {
sourceAttribute: string,
sourceAttribute: string;
sourceType: string;
};
type: AlarmScheduleType;
@ -516,17 +519,13 @@ interface AlarmRule {
export { AlarmRule as DeviceProfileAlarmRule };
export function alarmRuleValidator(control: AbstractControl): ValidationErrors | null {
const alarmRuleValid = (alarmRule: AlarmRule): boolean =>
!(!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length);
export const alarmRuleValidator = (control: AbstractControl): ValidationErrors | null => {
const alarmRule: AlarmRule = control.value;
return alarmRuleValid(alarmRule) ? null : {alarmRule: true};
}
function alarmRuleValid(alarmRule: AlarmRule): boolean {
if (!alarmRule || !alarmRule.condition || !alarmRule.condition.condition || !alarmRule.condition.condition.length) {
return false;
}
return true;
}
};
export interface DeviceProfileAlarm {
id: string;
@ -539,7 +538,7 @@ export interface DeviceProfileAlarm {
propagateRelationTypes?: Array<string>;
}
export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null {
export const deviceProfileAlarmValidator = (control: AbstractControl): ValidationErrors | null => {
const deviceProfileAlarm: DeviceProfileAlarm = control.value;
if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType &&
deviceProfileAlarm.createRules) {
@ -564,7 +563,7 @@ export function deviceProfileAlarmValidator(control: AbstractControl): Validatio
}
}
return {deviceProfileAlarm: true};
}
};
export interface DeviceProfileData {
@ -718,6 +717,47 @@ export interface DeviceInfo extends Device {
customerTitle: string;
customerIsPublic: boolean;
deviceProfileName: string;
active: boolean;
}
export interface DeviceInfoFilter {
customerId?: CustomerId;
edgeId?: EdgeId;
type?: string;
deviceProfileId?: DeviceProfileId;
active?: boolean;
}
export class DeviceInfoQuery {
pageLink: PageLink;
deviceInfoFilter: DeviceInfoFilter;
constructor(pageLink: PageLink, deviceInfoFilter: DeviceInfoFilter) {
this.pageLink = pageLink;
this.deviceInfoFilter = deviceInfoFilter;
}
public toQuery(): string {
let query;
if (this.deviceInfoFilter.customerId) {
query = `/customer/${this.deviceInfoFilter.customerId.id}/deviceInfos`;
} else if (this.deviceInfoFilter.edgeId) {
query = `/edge/${this.deviceInfoFilter.edgeId.id}/devices`;
} else {
query = '/tenant/deviceInfos';
}
query += this.pageLink.toQuery();
if (isNotEmptyStr(this.deviceInfoFilter.type)) {
query += `&type=${this.deviceInfoFilter.type}`;
} else if (this.deviceInfoFilter.deviceProfileId) {
query += `&deviceProfileId=${this.deviceInfoFilter.deviceProfileId.id}`;
}
if (isDefinedAndNotNull(this.deviceInfoFilter.active)) {
query += `&active=${this.deviceInfoFilter.active}`;
}
return query;
}
}
export enum DeviceCredentialsType {
@ -763,13 +803,11 @@ export interface DeviceCredentialMQTTBasic {
password: string;
}
export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
return {
clientId: '',
userName: '',
password: ''
};
}
export const getDeviceCredentialMQTTDefault = (): DeviceCredentialMQTTBasic => ({
clientId: '',
userName: '',
password: ''
});
export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>;
@ -800,44 +838,23 @@ export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.sunday'
);
export function getDayString(day: number): string {
switch (day) {
case 0:
return 'device-profile.schedule-day.monday';
case 1:
return this.translate.instant('device-profile.schedule-day.tuesday');
case 2:
return this.translate.instant('device-profile.schedule-day.wednesday');
case 3:
return this.translate.instant('device-profile.schedule-day.thursday');
case 4:
return this.translate.instant('device-profile.schedule-day.friday');
case 5:
return this.translate.instant('device-profile.schedule-day.saturday');
case 6:
return this.translate.instant('device-profile.schedule-day.sunday');
}
}
export function timeOfDayToUTCTimestamp(date: Date | number): number {
export const timeOfDayToUTCTimestamp = (date: Date | number): number => {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
};
export function utcTimestampToTimeOfDay(time = 0): Date {
return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
}
export const utcTimestampToTimeOfDay = (time = 0): Date => new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
function timeOfDayToMoment(date: Date | number): _moment.Moment {
const timeOfDayToMoment = (date: Date | number): _moment.Moment => {
if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]);
}
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
}
};
export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string {
export const getAlarmScheduleRangeText = (startsOn: Date | number, endsOn: Date | number): string => {
const start = timeOfDayToMoment(startsOn);
const end = timeOfDayToMoment(endsOn);
if (start < end) {
@ -847,4 +864,4 @@ export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
}
};

22
ui-ngx/src/assets/home/no_data_folder_bg.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="95" height="76" fill="none" version="1.1" viewBox="0 0 95 76" xmlns="http://www.w3.org/2000/svg">
<path d="m51.403 0.0064445c-0.33266 0.0098-0.6688 0.03164-1.0078 0.06445-10.642 1.0299-13.531 15.151-21.564 22.271-9.2267 8.1782-26.213 9.5198-28.119 21.764-1.9245 12.36 9.6705 18.371 19.793 25.592 8.5214 6.0789 19.566 5.925 30 6.0606 10.653 0.1384 21.672 1.6547 30-5.0508 9.0209-7.2631 14.774-14.987 13.91-26.602-0.8211-11.046-10.09-18.582-17.885-26.379-7.4985-7.5013-14.814-18.025-25.127-17.721zm-24.902 28.377h50c1.1046 0 2 0.87988 2 1.9844v33.535c0 0.67107-0.32283 1.2562-0.82227 1.6152-0.2557 0.78477-0.98095 1.3848-1.8945 1.3848h-50.225c-0.8252 0-1.5611-0.49632-1.8594-1.2656-1.5098-3.8931-6.1084-15.77-8.2363-21.449-0.482-1.2863 0.4682-2.627 1.8418-2.627h7.1953v-11.178c0-1.1046 0.8954-2 2-2z" fill="#00664d" fill-opacity=".039216"/>
<path d="m25 63.889v-33.506c0-0.8284 0.6716-1.5 1.5-1.5h50c0.8334 0 1.5 0.6613 1.5 1.4847v33.535c0 0.8329-0.6496 1.4871-1.4736 1.4871h-50.038c-0.8248 0-1.4888-0.6678-1.4888-1.5z" fill="none" stroke="#00695C" stroke-opacity=".56"/>
<path d="m25.56 66.403c-0.6222 0-1.1711-0.3725-1.3936-0.9462-1.5102-3.8941-6.109-15.769-8.2357-21.445-0.3564-0.9513 0.3411-1.951 1.3744-1.951h50.362c1.4933 0 2.8049 0.9447 3.2568 2.3597 1.6176 5.0656 4.8019 15.263 6.2931 20.046 0.2999 0.962-0.4196 1.9369-1.4342 1.9369h-50.223z" fill="none" stroke="#00695C" stroke-opacity=".56"/>
<g fill="#00695C" fill-opacity=".2">
<ellipse cx="49.5" cy="31.014" rx="1" ry="1.0137"/>
<ellipse cx="43" cy="24.52" rx="1.5" ry="1.5205"/>
<circle cx="55" cy="36.5" r="1.5"/>
</g>
<g transform="translate(-15.413 -35.297)">
<path d="m54.413 82.94c-1.9248 0-3.5 1.5928-3.5 3.5488 0 1.9559 1.5752 3.5488 3.5 3.5488 1.9248 0 3.5-1.5929 3.5-3.5488 0-1.956-1.5752-3.5488-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 0.08594 0.0039 1.75 1.774 0 0 1 0.08398 0.0059 1.75 1.774 0 0 1 0.08594 0.0098 1.75 1.774 0 0 1 0.08398 0.01563 1.75 1.774 0 0 1 0.08398 0.01953 1.75 1.774 0 0 1 0.08398 0.02344 1.75 1.774 0 0 1 0.08008 0.02734 1.75 1.774 0 0 1 0.08008 0.03125 1.75 1.774 0 0 1 0.08008 0.03516 1.75 1.774 0 0 1 0.07617 0.03906 1.75 1.774 0 0 1 0.07422 0.04297 1.75 1.774 0 0 1 0.07227 0.04687 1.75 1.774 0 0 1 0.07031 0.04883 1.75 1.774 0 0 1 0.06836 0.05469 1.75 1.774 0 0 1 0.06445 0.05664 1.75 1.774 0 0 1 0.03711 0.03516c1.1127 0.25355 1.9492 1.2586 1.9492 2.4863 0 1.4235-1.1238 2.5488-2.5 2.5488-1.3762 0-2.5-1.1254-2.5-2.5488 0-0.29103 0.04828-0.5691 0.13477-0.82812a1.75 1.774 0 0 1-0.02734-0.12695 1.75 1.774 0 0 1-0.01758-0.25195 1.75 1.774 0 0 1 0.01758-0.25391 1.75 1.774 0 0 1 0.05273-0.24609 1.75 1.774 0 0 1 0.08789-0.23828 1.75 1.774 0 0 1 0.11914-0.2207 1.75 1.774 0 0 1 0.14844-0.20312 1.75 1.774 0 0 1 0.17774-0.17969 1.75 1.774 0 0 1 0.19922-0.15039 1.75 1.774 0 0 1 0.21875-0.12109 1.75 1.774 0 0 1 0.23437-0.08984 1.75 1.774 0 0 1 0.24414-0.05273 1.75 1.774 0 0 1 0.25-0.01953z" color="#000000" fill="#00695c" fill-opacity=".56" style="-inkscape-stroke:none"/>
<path d="m54.413 82.941a3.5 3.5479 0 0 0-3.5 3.5488 3.5 3.5479 0 0 0 3.5 3.5469 3.5 3.5479 0 0 0 3.5-3.5469 3.5 3.5479 0 0 0-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 1.75 1.7734 1.75 1.774 0 0 1-1.75 1.7754 1.75 1.774 0 0 1-1.75-1.7754 1.75 1.774 0 0 1 1.75-1.7734z" fill="#00695a" fill-opacity=".4"/>
</g>
<g transform="translate(-1.4128 -35.28)">
<path d="m54.413 82.94c-1.9248 0-3.5 1.5928-3.5 3.5488 0 1.9559 1.5752 3.5488 3.5 3.5488 1.9248 0 3.5-1.5929 3.5-3.5488 0-1.956-1.5752-3.5488-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 0.08594 0.0039 1.75 1.774 0 0 1 0.08398 0.0059 1.75 1.774 0 0 1 0.08594 0.0098 1.75 1.774 0 0 1 0.08398 0.01563 1.75 1.774 0 0 1 0.08398 0.01953 1.75 1.774 0 0 1 0.08398 0.02344 1.75 1.774 0 0 1 0.08008 0.02734 1.75 1.774 0 0 1 0.08008 0.03125 1.75 1.774 0 0 1 0.08008 0.03516 1.75 1.774 0 0 1 0.07617 0.03906 1.75 1.774 0 0 1 0.07422 0.04297 1.75 1.774 0 0 1 0.07227 0.04687 1.75 1.774 0 0 1 0.07031 0.04883 1.75 1.774 0 0 1 0.06836 0.05469 1.75 1.774 0 0 1 0.06445 0.05664 1.75 1.774 0 0 1 0.03711 0.03516c1.1127 0.25355 1.9492 1.2586 1.9492 2.4863 0 1.4235-1.1238 2.5488-2.5 2.5488-1.3762 0-2.5-1.1254-2.5-2.5488 0-0.29103 0.04828-0.5691 0.13477-0.82812a1.75 1.774 0 0 1-0.02734-0.12695 1.75 1.774 0 0 1-0.01758-0.25195 1.75 1.774 0 0 1 0.01758-0.25391 1.75 1.774 0 0 1 0.05273-0.24609 1.75 1.774 0 0 1 0.08789-0.23828 1.75 1.774 0 0 1 0.11914-0.2207 1.75 1.774 0 0 1 0.14844-0.20312 1.75 1.774 0 0 1 0.17774-0.17969 1.75 1.774 0 0 1 0.19922-0.15039 1.75 1.774 0 0 1 0.21875-0.12109 1.75 1.774 0 0 1 0.23437-0.08984 1.75 1.774 0 0 1 0.24414-0.05273 1.75 1.774 0 0 1 0.25-0.01953z" color="#000000" fill="#00695c" fill-opacity=".56" style="-inkscape-stroke:none"/>
<path d="m54.413 82.941a3.5 3.5479 0 0 0-3.5 3.5488 3.5 3.5479 0 0 0 3.5 3.5469 3.5 3.5479 0 0 0 3.5-3.5469 3.5 3.5479 0 0 0-3.5-3.5488zm-0.66016 0.56641a1.75 1.774 0 0 1 1.75 1.7734 1.75 1.774 0 0 1-1.75 1.7754 1.75 1.774 0 0 1-1.75-1.7754 1.75 1.774 0 0 1 1.75-1.7734z" fill="#00695a" fill-opacity=".4"/>
</g>
<path d="m42.5 59.807c1.098-0.765 4.2353-1.8359 8 0" stroke="#00695C" stroke-linecap="round" stroke-opacity=".56"/>
<circle cx="52.5" cy="20" r="1" fill="#00695C" fill-opacity=".2"/>
<circle cx="45.5" cy="11" r="1" fill="#00695C" fill-opacity=".2"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

11
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1296,6 +1296,14 @@
"unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"is-gateway": "Is gateway",
"overwrite-activity-time": "Overwrite activity time for connected device",
"device-filter": "Device filter",
"device-filter-title": "Device Filter",
"filter-title": "Filter",
"device-state": "Device state",
"state": "State",
"any": "Any",
"active": "Active",
"inactive": "Inactive",
"public": "Public",
"device-public": "Device is public",
"select-device": "Select device",
@ -5131,6 +5139,9 @@
"color": "Color",
"shadow-color": "Shadow color"
},
"home": {
"no-data-available": "No data available"
},
"system-info": {
"cpu": "CPU",
"ram": "RAM",

2
ui-ngx/src/styles.scss

@ -266,7 +266,7 @@ pre.tb-highlight {
letter-spacing: normal;
}
.tb-timewindow-panel, .tb-legend-config-panel, .tb-alarm-filter-panel {
.tb-timewindow-panel, .tb-legend-config-panel, .tb-filter-panel {
overflow: hidden;
background: #fff;
border-radius: 4px;

Loading…
Cancel
Save