Browse Source

UI: Display device activity state in table. Implement device activity state filter.

pull/8455/head^2
Igor Kulikov 3 years ago
parent
commit
c11ff1434d
  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, ClaimResult,
Device, Device,
DeviceCredentials, DeviceCredentials,
DeviceInfo, DeviceInfo, DeviceInfoQuery,
DeviceSearchQuery DeviceSearchQuery
} from '@app/shared/models/device.models'; } from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -42,6 +42,11 @@ export class DeviceService {
private http: HttpClient 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 = '', public getTenantDeviceInfos(pageLink: PageLink, type: string = '',
config?: RequestConfig): Observable<PageData<DeviceInfo>> { config?: RequestConfig): Observable<PageData<DeviceInfo>> {
return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`, 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(); $event.stopPropagation();
} }
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-alarm-filter-panel', panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true, hasBackdrop: true,
maxHeight: '80vh', 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 { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component'; import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -282,6 +283,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent, DeviceProfileComponent,
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
AssetProfileComponent, AssetProfileComponent,
AssetProfileDialogComponent, AssetProfileDialogComponent,
AssetProfileAutocompleteComponent, AssetProfileAutocompleteComponent,
@ -425,6 +427,7 @@ import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assig
DeviceProfileComponent, DeviceProfileComponent,
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
DeviceInfoFilterComponent,
RuleChainAutocompleteComponent, RuleChainAutocompleteComponent,
DeviceWizardDialogComponent, DeviceWizardDialogComponent,
AssetProfileComponent, 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 target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.panelClass = 'tb-alarm-filter-panel'; config.panelClass = 'tb-filter-panel';
config.hasBackdrop = true; config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = { const connectedPosition: ConnectedPosition = {
originX: 'end', originX: 'end',

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

@ -67,4 +67,50 @@
color: inherit; 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-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header"> <ng-container *ngIf="hasDashboardsAccess; else noDataAvailable">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a> <div class="tb-card-header">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px"> <a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[ <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.last'),
}, value: 'last'
{ },
name: ctx.translate.instant('widgets.recent-dashboards.starred'), {
value: 'starred' name: ctx.translate.instant('widgets.recent-dashboards.starred'),
} value: 'starred'
]"> }
</tb-toggle-header> ]">
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md </tb-toggle-header>
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a> <a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
</div> mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>
</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> </div>
<ng-template #noLastVisitedDashboards> </div>
<div class="tb-no-dashboards"> <ng-container *ngIf="userDashboardsInfo; else loading" [ngSwitch]="dashboardsToggle.value">
<div class="tb-no-last-visited-bg"></div> <ng-template [ngSwitchCase]="'last'">
<div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div> <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> </div>
</ng-template> <ng-template #noLastVisitedDashboards>
</ng-template> <div class="tb-no-dashboards">
<ng-template [ngSwitchCase]="'starred'"> <div class="tb-no-last-visited-bg"></div>
<div style="overflow-y: auto;"> <div class="tb-no-last-visited-dashboards-text">{{ 'widgets.recent-dashboards.no-last-viewed-dashboards' | translate }}</div>
<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>
<div class="tb-cell title"> </ng-template>
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a> </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>
</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> </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-template>
</ng-container> </ng-container>
<ng-template #loading> <ng-template #noDataAvailable>
<div class="tb-no-dashboards"> <div class="tb-card-header">
<mat-spinner [diameter]="50" mode="indeterminate"></mat-spinner> <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> </div>
</ng-template> </ng-template>
</div> </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; lastVisitedDashboardsPageLink: PageLink;
starredDashboardValue = null; starredDashboardValue = null;
hasDashboardsAccess = true;
dirty = false; dirty = false;
@ -84,7 +85,10 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
} }
ngOnInit() { ngOnInit() {
this.reload(); this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority);
if (this.hasDashboardsAccess) {
this.reload();
}
} }
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-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header"> <ng-container *ngIf="hasUsageInfoAccess; else noDataAvailable">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a> <div class="tb-card-header">
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[ <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.entities'),
}, value: 'entities'
{ },
name: ctx.translate.instant('widgets.usage-info.api-calls'), {
value: 'apiCalls' name: ctx.translate.instant('widgets.usage-info.api-calls'),
} value: 'apiCalls'
]"> }
</tb-toggle-header> ]">
</div> </tb-toggle-header>
<ng-container [ngSwitch]="usageToggle.value"> </div>
<ng-template [ngSwitchCase]="'entities'"> <ng-container [ngSwitch]="usageToggle.value">
<div fxFlex class="tb-usage-list"> <ng-template [ngSwitchCase]="'entities'">
<div class="tb-usage-items"> <div fxFlex class="tb-usage-list">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div> <div class="tb-usage-items">
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div> <div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.devices}" translate>device.devices</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div> <div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.assets}" translate>asset.assets</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div> <div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.users}" translate>user.users</div>
<div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</div> <div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.dashboards}" translate>dashboard.dashboards</div>
</div> <div class="tb-usage-item" [ngClass]="{'critical': entityItemCritical.customers}" translate>customer.customers</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>
<div class="tb-usage-items-progress"> <div class="tb-usage-items-values">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.devices}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.devices, usageInfo?.maxDevices)"></mat-progress-bar> <div class="tb-usage-items-counts">
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.assets}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.assets, usageInfo?.maxAssets)"></mat-progress-bar> <div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.devices}">{{ usageInfo?.devices | shortNumber }} / {{ maxValue(usageInfo?.maxDevices) }}</div>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.users}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.users, usageInfo?.maxUsers)"></mat-progress-bar> <div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.assets}">{{ usageInfo?.assets | shortNumber }} / {{ maxValue(usageInfo?.maxAssets) }}</div>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.dashboards}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.dashboards, usageInfo?.maxDashboards)"></mat-progress-bar> <div class="tb-usage-item-counts" [ngClass]="{'critical': entityItemCritical.users}">{{ usageInfo?.users | shortNumber }} / {{ maxValue(usageInfo?.maxUsers) }}</div>
<mat-progress-bar [ngClass]="{'critical': entityItemCritical.customers}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.customers, usageInfo?.maxCustomers)"></mat-progress-bar> <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> </div>
</div> </ng-template>
</ng-template> <ng-template [ngSwitchCase]="'apiCalls'">
<ng-template [ngSwitchCase]="'apiCalls'"> <div fxFlex class="tb-usage-list">
<div fxFlex class="tb-usage-list"> <div class="tb-usage-items">
<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.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.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.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.emails}" translate>api-usage.email</div> <div class="tb-usage-item" [ngClass]="{'critical': apiCallItemCritical.sms}" translate>api-usage.sms</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>
<div class="tb-usage-items-progress"> <div class="tb-usage-items-values">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.transportMessages}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.transportMessages, usageInfo?.maxTransportMessages)"></mat-progress-bar> <div class="tb-usage-items-counts">
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.jsExecutions}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.jsExecutions, usageInfo?.maxJsExecutions)"></mat-progress-bar> <div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.transportMessages}">{{ usageInfo?.transportMessages | shortNumber }} / {{ maxValue(usageInfo?.maxTransportMessages) }}</div>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.alarms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.alarms, usageInfo?.maxAlarms)"></mat-progress-bar> <div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.jsExecutions}">{{ usageInfo?.jsExecutions | shortNumber }} / {{ maxValue(usageInfo?.maxJsExecutions) }}</div>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.emails}"color="primary" mode="determinate" [value]="progressValue(usageInfo?.emails, usageInfo?.maxEmails)"></mat-progress-bar> <div class="tb-usage-item-counts" [ngClass]="{'critical': apiCallItemCritical.alarms}">{{ usageInfo?.alarms | shortNumber }} / {{ maxValue(usageInfo?.maxAlarms) }}</div>
<mat-progress-bar [ngClass]="{'critical': apiCallItemCritical.sms}" color="primary" mode="determinate" [value]="progressValue(usageInfo?.sms, usageInfo?.maxSms)"></mat-progress-bar> <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> </div>
</div> </ng-template>
</ng-template> </ng-container>
</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> </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} = {}; entityItemCritical: {[key: string]: boolean} = {};
apiCallItemCritical: {[key: string]: boolean} = {}; apiCallItemCritical: {[key: string]: boolean} = {};
hasUsageInfoAccess = true;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private shortNumberPipe: ShortNumberPipe, private shortNumberPipe: ShortNumberPipe,
@ -52,41 +54,43 @@ export class UsageInfoWidgetComponent extends PageComponent implements OnInit, O
} }
ngOnInit() { ngOnInit() {
(this.authUser.authority === Authority.TENANT_ADMIN ? this.hasUsageInfoAccess = this.authUser.authority === Authority.TENANT_ADMIN;
this.usageInfoService.getUsageInfo() : of(null)).subscribe( if (this.hasUsageInfoAccess) {
(usageInfo) => { this.usageInfoService.getUsageInfo().subscribe(
this.usageInfo = usageInfo; (usageInfo) => {
this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices); this.usageInfo = usageInfo;
this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets); this.entityItemCritical.devices = this.isItemCritical(this.usageInfo?.devices, this.usageInfo?.maxDevices);
this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers); this.entityItemCritical.assets = this.isItemCritical(this.usageInfo?.assets, this.usageInfo?.maxAssets);
this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards); this.entityItemCritical.users = this.isItemCritical(this.usageInfo?.users, this.usageInfo?.maxUsers);
this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers); this.entityItemCritical.dashboards = this.isItemCritical(this.usageInfo?.dashboards, this.usageInfo?.maxDashboards);
this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages, this.entityItemCritical.customers = this.isItemCritical(this.usageInfo?.customers, this.usageInfo?.maxCustomers);
this.usageInfo?.maxTransportMessages); this.apiCallItemCritical.transportMessages = this.isItemCritical(this.usageInfo?.transportMessages,
this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions); this.usageInfo?.maxTransportMessages);
this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms); this.apiCallItemCritical.jsExecutions = this.isItemCritical(this.usageInfo?.jsExecutions, this.usageInfo?.maxJsExecutions);
this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails); this.apiCallItemCritical.alarms = this.isItemCritical(this.usageInfo?.alarms, this.usageInfo?.maxAlarms);
this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms); this.apiCallItemCritical.emails = this.isItemCritical(this.usageInfo?.emails, this.usageInfo?.maxEmails);
let entitiesHasCriticalItem = false; this.apiCallItemCritical.sms = this.isItemCritical(this.usageInfo?.sms, this.usageInfo?.maxSms);
let apiCallsHasCriticalItem = false; let entitiesHasCriticalItem = false;
for (const key of Object.keys(this.entityItemCritical)) { let apiCallsHasCriticalItem = false;
if (this.entityItemCritical[key]) { for (const key of Object.keys(this.entityItemCritical)) {
entitiesHasCriticalItem = true; if (this.entityItemCritical[key]) {
break; entitiesHasCriticalItem = true;
break;
}
} }
} for (const key of Object.keys(this.apiCallItemCritical)) {
for (const key of Object.keys(this.apiCallItemCritical)) { if (this.apiCallItemCritical[key]) {
if (this.apiCallItemCritical[key]) { apiCallsHasCriticalItem = true;
apiCallsHasCriticalItem = true; break;
break; }
} }
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) {
this.toggleValue = 'apiCalls';
}
this.cd.markForCheck();
} }
if (apiCallsHasCriticalItem && !entitiesHasCriticalItem) { );
this.toggleValue = 'apiCalls'; }
}
this.cd.markForCheck();
}
);
} }
maxValue(max: number): number | string { 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. limitations under the License.
--> -->
<tb-device-profile-autocomplete <tb-device-info-filter [ngModel]="entitiesTableConfig.componentsData.deviceInfoFilter"
subscriptSizing="dynamic" (ngModelChange)="deviceInfoFilterChanged($event)">
[ngModel]="entitiesTableConfig.componentsData.deviceProfileId" </tb-device-info-filter>
(ngModelChange)="deviceProfileChanged($event)"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>

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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; 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 { EntityType } from '@shared/models/entity-type.models';
import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id'; import { DeviceProfileId } from '../../../../shared/models/id/device-profile-id';
@ -35,8 +35,8 @@ export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<Devic
super(store); super(store);
} }
deviceProfileChanged(deviceProfileId: DeviceProfileId) { deviceInfoFilterChanged(deviceInfoFilter: DeviceInfoFilter) {
this.entitiesTableConfig.componentsData.deviceProfileId = deviceProfileId; this.entitiesTableConfig.componentsData.deviceInfoFilter = deviceInfoFilter;
this.entitiesTableConfig.getTable().resetSortAndFilter(true); 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 { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Resolve, Router } from '@angular/router';
import { import {
CellActionDescriptor, CellActionDescriptor,
checkBoxCell, checkBoxCell,
@ -30,7 +30,13 @@ import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.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 { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of, Subject } from 'rxjs'; import { forkJoin, Observable, of, Subject } from 'rxjs';
import { select, Store } from '@ngrx/store'; 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 { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component'; import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { BaseData, HasId } from '@shared/models/base-data'; 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 { EdgeService } from '@core/http/edge.service';
import { import {
AddEntitiesToEdgeDialogComponent, AddEntitiesToEdgeDialogComponent,
AddEntitiesToEdgeDialogData AddEntitiesToEdgeDialogData
} from '@home/dialogs/add-entities-to-edge-dialog.component'; } 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() @Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> { 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.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text');
this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id); this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id);
this.config.saveEntity = device => { this.config.saveEntity = device => this.deviceService.saveDevice(device).pipe(
return this.deviceService.saveDevice(device).pipe(
tap(() => { tap(() => {
this.broadcast.broadcast('deviceSaved'); this.broadcast.broadcast('deviceSaved');
}), }),
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id) mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id)
)); ));
};
this.config.onEntityAction = action => this.onDeviceAction(action, this.config); this.config.onEntityAction = action => this.onDeviceAction(action, this.config);
this.config.detailsReadonly = () => this.config.detailsReadonly = () =>
(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'); (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.onLoadAction = (route) => this.onLoadAction(route);
this.config.headerComponent = DeviceTableHeaderComponent; this.config.headerComponent = DeviceTableHeaderComponent;
@ -123,7 +137,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
const routeParams = route.params; const routeParams = route.params;
this.config.componentsData = { this.config.componentsData = {
deviceScope: route.data.devicesType, deviceScope: route.data.devicesType,
deviceProfileId: null, deviceInfoFilter: {},
deviceCredentials$: new Subject<DeviceCredentials>(), deviceCredentials$: new Subject<DeviceCredentials>(),
edgeId: routeParams.edgeId 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.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(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.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config; 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>> { configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
const columns: Array<EntityTableColumn<DeviceInfo>> = [ const columns: Array<EntityTableColumn<DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), new DateEntityTableColumn<DeviceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'), new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'),
new EntityTableColumn<DeviceInfo>('deviceProfileName', 'device-profile.device-profile', '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') { if (deviceScope === 'tenant') {
columns.push( columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'), new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px', new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => { entity => checkBoxCell(entity.customerIsPublic), () => ({})),
return checkBoxCell(entity.customerIsPublic);
}, () => ({}), false),
); );
} }
columns.push( columns.push(
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px', new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px',
entity => { entity => checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway), () => ({}), false)
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway);
}, () => ({}), false)
); );
return columns; 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 { configureEntityFunctions(deviceScope: string): void {
this.config.entitiesFetchFunction = pageLink => this.deviceService.getDeviceInfosByQuery(this.prepareDeviceInfoQuery(pageLink));
if (deviceScope === 'tenant') { 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); 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 { } else {
this.config.entitiesFetchFunction = pageLink => this.config.deleteEntity = () => of();
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); 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>> { configureCellActions(deviceScope: string): Array<CellActionDescriptor<DeviceInfo>> {
@ -540,7 +604,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
data: { data: {
deviceId: device.id.id, deviceId: device.id.id,
deviceProfileId: device.deviceProfileId.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 => { }).afterClosed().subscribe(deviceCredentials => {
if (isDefinedAndNotNull(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", "padding": "16px",
"settings": { "settings": {
"useMarkdownTextFunction": false, "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, "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", "title": "Devices",
"showTitleIcon": false, "showTitleIcon": false,
@ -804,7 +804,7 @@
"useMarkdownTextFunction": false, "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>", "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, "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", "title": "Alarms",
"showTitleIcon": false, "showTitleIcon": false,

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

@ -35,6 +35,9 @@ import {
getDefaultProfileObserveAttrConfig, getDefaultProfileObserveAttrConfig,
PowerMode PowerMode
} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; } 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 { export enum DeviceProfileType {
DEFAULT = 'DEFAULT', DEFAULT = 'DEFAULT',
@ -328,7 +331,7 @@ export interface DeviceProvisionConfiguration {
allowCreateNewDevicesByX509Certificate?: boolean; allowCreateNewDevicesByX509Certificate?: boolean;
} }
export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { export const createDeviceProfileConfiguration = (type: DeviceProfileType): DeviceProfileConfiguration => {
let configuration: DeviceProfileConfiguration = null; let configuration: DeviceProfileConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -339,9 +342,9 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic
} }
} }
return configuration; return configuration;
} };
export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { export const createDeviceConfiguration = (type: DeviceProfileType): DeviceConfiguration => {
let configuration: DeviceConfiguration = null; let configuration: DeviceConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -352,9 +355,9 @@ export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfig
} }
} }
return configuration; return configuration;
} };
export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { export const createDeviceProfileTransportConfiguration = (type: DeviceTransportType): DeviceProfileTransportConfiguration => {
let transportConfiguration: DeviceProfileTransportConfiguration = null; let transportConfiguration: DeviceProfileTransportConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -408,9 +411,9 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT
} }
} }
return transportConfiguration; return transportConfiguration;
} };
export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { export const createDeviceTransportConfiguration = (type: DeviceTransportType): DeviceTransportConfiguration => {
let transportConfiguration: DeviceTransportConfiguration = null; let transportConfiguration: DeviceTransportConfiguration = null;
if (type) { if (type) {
switch (type) { switch (type) {
@ -446,7 +449,7 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
} }
} }
return transportConfiguration; return transportConfiguration;
} };
export enum AlarmConditionType { export enum AlarmConditionType {
SIMPLE = 'SIMPLE', SIMPLE = 'SIMPLE',
@ -489,7 +492,7 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
export interface AlarmSchedule{ export interface AlarmSchedule{
dynamicValue?: { dynamicValue?: {
sourceAttribute: string, sourceAttribute: string;
sourceType: string; sourceType: string;
}; };
type: AlarmScheduleType; type: AlarmScheduleType;
@ -516,17 +519,13 @@ interface AlarmRule {
export { AlarmRule as DeviceProfileAlarmRule }; 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; const alarmRule: AlarmRule = control.value;
return alarmRuleValid(alarmRule) ? null : {alarmRule: true}; 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 { export interface DeviceProfileAlarm {
id: string; id: string;
@ -539,7 +538,7 @@ export interface DeviceProfileAlarm {
propagateRelationTypes?: Array<string>; propagateRelationTypes?: Array<string>;
} }
export function deviceProfileAlarmValidator(control: AbstractControl): ValidationErrors | null { export const deviceProfileAlarmValidator = (control: AbstractControl): ValidationErrors | null => {
const deviceProfileAlarm: DeviceProfileAlarm = control.value; const deviceProfileAlarm: DeviceProfileAlarm = control.value;
if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType && if (deviceProfileAlarm && deviceProfileAlarm.id && deviceProfileAlarm.alarmType &&
deviceProfileAlarm.createRules) { deviceProfileAlarm.createRules) {
@ -564,7 +563,7 @@ export function deviceProfileAlarmValidator(control: AbstractControl): Validatio
} }
} }
return {deviceProfileAlarm: true}; return {deviceProfileAlarm: true};
} };
export interface DeviceProfileData { export interface DeviceProfileData {
@ -718,6 +717,47 @@ export interface DeviceInfo extends Device {
customerTitle: string; customerTitle: string;
customerIsPublic: boolean; customerIsPublic: boolean;
deviceProfileName: string; 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 { export enum DeviceCredentialsType {
@ -763,13 +803,11 @@ export interface DeviceCredentialMQTTBasic {
password: string; password: string;
} }
export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic { export const getDeviceCredentialMQTTDefault = (): DeviceCredentialMQTTBasic => ({
return { clientId: '',
clientId: '', userName: '',
userName: '', password: ''
password: '' });
};
}
export interface DeviceSearchQuery extends EntitySearchQuery { export interface DeviceSearchQuery extends EntitySearchQuery {
deviceTypes: Array<string>; deviceTypes: Array<string>;
@ -800,44 +838,23 @@ export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.sunday' 'device-profile.schedule-day.sunday'
); );
export function getDayString(day: number): string { export const timeOfDayToUTCTimestamp = (date: Date | number): number => {
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 {
if (typeof date === 'number' || date === null) { if (typeof date === 'number' || date === null) {
return 0; return 0;
} }
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
} };
export function utcTimestampToTimeOfDay(time = 0): Date { export const utcTimestampToTimeOfDay = (time = 0): Date => new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
return 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) { if (typeof date === 'number' || date === null) {
return _moment([1970, 0, 1, 0, 0, 0, 0]); return _moment([1970, 0, 1, 0, 0, 0, 0]);
} }
return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 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 start = timeOfDayToMoment(startsOn);
const end = timeOfDayToMoment(endsOn); const end = timeOfDayToMoment(endsOn);
if (start < end) { 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>` + 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>`; ` 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}}", "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", "is-gateway": "Is gateway",
"overwrite-activity-time": "Overwrite activity time for connected device", "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", "public": "Public",
"device-public": "Device is public", "device-public": "Device is public",
"select-device": "Select device", "select-device": "Select device",
@ -5131,6 +5139,9 @@
"color": "Color", "color": "Color",
"shadow-color": "Shadow color" "shadow-color": "Shadow color"
}, },
"home": {
"no-data-available": "No data available"
},
"system-info": { "system-info": {
"cpu": "CPU", "cpu": "CPU",
"ram": "RAM", "ram": "RAM",

2
ui-ngx/src/styles.scss

@ -266,7 +266,7 @@ pre.tb-highlight {
letter-spacing: normal; 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; overflow: hidden;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;

Loading…
Cancel
Save