43 changed files with 1968 additions and 143 deletions
@ -0,0 +1,40 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
|
|||
@Data |
|||
public class DeviceInfo extends Device { |
|||
|
|||
private String customerTitle; |
|||
private boolean customerIsPublic; |
|||
|
|||
public DeviceInfo() { |
|||
super(); |
|||
} |
|||
|
|||
public DeviceInfo(DeviceId deviceId) { |
|||
super(deviceId); |
|||
} |
|||
|
|||
public DeviceInfo(Device device, String customerTitle, boolean customerIsPublic) { |
|||
super(device); |
|||
this.customerTitle = customerTitle; |
|||
this.customerIsPublic = customerIsPublic; |
|||
} |
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.model.sql; |
|||
|
|||
import com.datastax.driver.core.utils.UUIDs; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.hibernate.annotations.Type; |
|||
import org.hibernate.annotations.TypeDef; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.dao.model.BaseSqlEntity; |
|||
import org.thingsboard.server.dao.model.ModelConstants; |
|||
import org.thingsboard.server.dao.model.SearchTextEntity; |
|||
import org.thingsboard.server.dao.util.mapping.JsonStringType; |
|||
|
|||
import javax.persistence.Column; |
|||
import javax.persistence.Entity; |
|||
import javax.persistence.MappedSuperclass; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@TypeDef(name = "json", typeClass = JsonStringType.class) |
|||
@MappedSuperclass |
|||
public abstract class AbstractDeviceEntity<T extends Device> extends BaseSqlEntity<T> implements SearchTextEntity<T> { |
|||
|
|||
@Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY) |
|||
private String tenantId; |
|||
|
|||
@Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY) |
|||
private String customerId; |
|||
|
|||
@Column(name = ModelConstants.DEVICE_TYPE_PROPERTY) |
|||
private String type; |
|||
|
|||
@Column(name = ModelConstants.DEVICE_NAME_PROPERTY) |
|||
private String name; |
|||
|
|||
@Column(name = ModelConstants.DEVICE_LABEL_PROPERTY) |
|||
private String label; |
|||
|
|||
@Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) |
|||
private String searchText; |
|||
|
|||
@Type(type = "json") |
|||
@Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY) |
|||
private JsonNode additionalInfo; |
|||
|
|||
public AbstractDeviceEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public AbstractDeviceEntity(Device device) { |
|||
if (device.getId() != null) { |
|||
this.setId(device.getId().getId()); |
|||
} |
|||
if (device.getTenantId() != null) { |
|||
this.tenantId = toString(device.getTenantId().getId()); |
|||
} |
|||
if (device.getCustomerId() != null) { |
|||
this.customerId = toString(device.getCustomerId().getId()); |
|||
} |
|||
this.name = device.getName(); |
|||
this.type = device.getType(); |
|||
this.label = device.getLabel(); |
|||
this.additionalInfo = device.getAdditionalInfo(); |
|||
} |
|||
|
|||
public AbstractDeviceEntity(DeviceEntity deviceEntity) { |
|||
this.setId(deviceEntity.getId());; |
|||
this.tenantId = deviceEntity.getTenantId(); |
|||
this.customerId = deviceEntity.getCustomerId(); |
|||
this.type = deviceEntity.getType(); |
|||
this.name = deviceEntity.getName(); |
|||
this.label = deviceEntity.getLabel(); |
|||
this.searchText = deviceEntity.getSearchText(); |
|||
this.additionalInfo = deviceEntity.getAdditionalInfo(); |
|||
} |
|||
|
|||
@Override |
|||
public String getSearchTextSource() { |
|||
return name; |
|||
} |
|||
|
|||
@Override |
|||
public void setSearchText(String searchText) { |
|||
this.searchText = searchText; |
|||
} |
|||
|
|||
protected Device toDevice() { |
|||
Device device = new Device(new DeviceId(getId())); |
|||
device.setCreatedTime(UUIDs.unixTimestamp(getId())); |
|||
if (tenantId != null) { |
|||
device.setTenantId(new TenantId(toUUID(tenantId))); |
|||
} |
|||
if (customerId != null) { |
|||
device.setCustomerId(new CustomerId(toUUID(customerId))); |
|||
} |
|||
device.setName(name); |
|||
device.setType(type); |
|||
device.setLabel(label); |
|||
device.setAdditionalInfo(additionalInfo); |
|||
return device; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.model.sql; |
|||
|
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.thingsboard.server.common.data.DeviceInfo; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class DeviceInfoEntity extends AbstractDeviceEntity<DeviceInfo> { |
|||
|
|||
public static final Map<String,String> deviceInfoColumnMap = new HashMap<>(); |
|||
static { |
|||
deviceInfoColumnMap.put("customerTitle", "c.title"); |
|||
} |
|||
|
|||
private String customerTitle; |
|||
private boolean customerIsPublic; |
|||
|
|||
public DeviceInfoEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public DeviceInfoEntity(DeviceEntity deviceEntity, |
|||
String customerTitle, |
|||
Object customerAdditionalInfo) { |
|||
super(deviceEntity); |
|||
this.customerTitle = customerTitle; |
|||
if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { |
|||
this.customerIsPublic = ((JsonNode)customerAdditionalInfo).get("isPublic").asBoolean(); |
|||
} else { |
|||
this.customerIsPublic = false; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public DeviceInfo toData() { |
|||
return new DeviceInfo(super.toDevice(), customerTitle, customerIsPublic); |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { defaultHttpOptions } from './http-utils'; |
|||
import { Observable } from 'rxjs/index'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { PageData } from '@shared/models/page/page-data'; |
|||
import { Tenant } from '@shared/models/tenant.model'; |
|||
import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; |
|||
import {map} from 'rxjs/operators'; |
|||
import {DeviceInfo, Device} from '@app/shared/models/device.models'; |
|||
import {EntitySubtype} from '@app/shared/models/entity-type.models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class DeviceService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) { } |
|||
|
|||
public getTenantDeviceInfos(pageLink: PageLink, type: string = '', ignoreErrors: boolean = false, |
|||
ignoreLoading: boolean = false): Observable<PageData<DeviceInfo>> { |
|||
return this.http.get<PageData<DeviceInfo>>(`/api/tenant/deviceInfos${pageLink.toQuery()}&type=${type}`, |
|||
defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public getCustomerDeviceInfos(customerId: string, pageLink: PageLink, type: string = '', ignoreErrors: boolean = false, |
|||
ignoreLoading: boolean = false): Observable<PageData<DeviceInfo>> { |
|||
return this.http.get<PageData<DeviceInfo>>(`/api/customer/${customerId}/deviceInfos${pageLink.toQuery()}&type=${type}`, |
|||
defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public getDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> { |
|||
return this.http.get<Device>(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public getDeviceInfo(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DeviceInfo> { |
|||
return this.http.get<DeviceInfo>(`/api/device/info/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public saveDevice(device: Device, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Device> { |
|||
return this.http.post<Device>('/api/device', device, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public deleteDevice(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
|||
return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public getDeviceTypes(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<EntitySubtype>> { |
|||
return this.http.get<Array<EntitySubtype>>('/api/device/types', defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
|||
return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
|
|||
export interface BroadcastMessage { |
|||
name: string; |
|||
args?: Array<any>; |
|||
} |
|||
|
|||
export interface BroadcastEvent { |
|||
name: string; |
|||
} |
|||
|
|||
export type BroadcastListener = (event: BroadcastEvent, ...args: Array<any>) => void; |
|||
@ -0,0 +1,51 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import {Injectable} from '@angular/core'; |
|||
import {Subject, Subscription} from 'rxjs'; |
|||
import {NotificationMessage} from '@core/notification/notification.models'; |
|||
import {BroadcastEvent, BroadcastListener, BroadcastMessage} from '@core/services/broadcast.models'; |
|||
import {filter} from 'rxjs/operators'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class BroadcastService { |
|||
|
|||
private broadcastSubject: Subject<BroadcastMessage> = new Subject(); |
|||
|
|||
broadcast(name: string, ...args: Array<any>) { |
|||
const message = { |
|||
name, |
|||
args |
|||
} as BroadcastMessage; |
|||
this.broadcastSubject.next(message); |
|||
} |
|||
|
|||
on(name: string, listener: BroadcastListener): Subscription { |
|||
return this.broadcastSubject.asObservable().pipe( |
|||
filter((message) => message.name === name) |
|||
).subscribe( |
|||
(message) => { |
|||
const event = { |
|||
name: message.name |
|||
} as BroadcastEvent; |
|||
listener(event, message.args); |
|||
} |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import {NgModule} from '@angular/core'; |
|||
import {RouterModule, Routes} from '@angular/router'; |
|||
|
|||
import {EntitiesTableComponent} from '@shared/components/entity/entities-table.component'; |
|||
import {Authority} from '@shared/models/authority.enum'; |
|||
import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: 'devices', |
|||
component: EntitiesTableComponent, |
|||
data: { |
|||
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], |
|||
title: 'device.devices', |
|||
devicesType: 'tenant', |
|||
breadcrumb: { |
|||
label: 'device.devices', |
|||
icon: 'devices_other' |
|||
} |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: DevicesTableConfigResolver |
|||
} |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
providers: [ |
|||
DevicesTableConfigResolver |
|||
] |
|||
}) |
|||
export class DeviceRoutingModule { } |
|||
@ -0,0 +1,23 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<tb-entity-subtype-select |
|||
[showLabel]="true" |
|||
[entityType]="entityType.DEVICE" |
|||
[ngModel]="entitiesTableConfig.componentsData.deviceType" |
|||
(ngModelChange)="deviceTypeChanged($event)"> |
|||
</tb-entity-subtype-select> |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
flex: 1; |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
tb-entity-subtype-select { |
|||
mat-form-field { |
|||
font-size: 16px; |
|||
|
|||
.mat-form-field-wrapper { |
|||
padding-bottom: 0; |
|||
} |
|||
|
|||
.mat-form-field-underline { |
|||
bottom: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component'; |
|||
import {DeviceInfo} from '@app/shared/models/device.models'; |
|||
import {EntityType} from '@shared/models/entity-type.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-device-table-header', |
|||
templateUrl: './device-table-header.component.html', |
|||
styleUrls: ['./device-table-header.component.scss'] |
|||
}) |
|||
export class DeviceTableHeaderComponent extends EntityTableHeaderComponent<DeviceInfo> { |
|||
|
|||
entityType = EntityType; |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
deviceTypeChanged(deviceType: string) { |
|||
this.entitiesTableConfig.componentsData.deviceType = deviceType; |
|||
this.entitiesTableConfig.table.updateData(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-details-buttons"> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'makePublic')" |
|||
[fxShow]="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer(entity) && !entity?.customerIsPublic"> |
|||
{{'device.make-public' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'assignToCustomer')" |
|||
[fxShow]="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer(entity)"> |
|||
{{'device.assign-to-customer' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'unassignFromCustomer')" |
|||
[fxShow]="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer(entity)"> |
|||
{{ (entity?.customerIsPublic ? 'device.make-private' : 'device.unassign-from-customer') | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'manageCredentials')" |
|||
[fxShow]="!isEdit"> |
|||
{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'delete')" |
|||
[fxShow]="!hideDelete() && !isEdit"> |
|||
{{'device.delete' | translate }} |
|||
</button> |
|||
<div fxLayout="row"> |
|||
<button mat-raised-button |
|||
ngxClipboard |
|||
(cbOnSuccess)="onDeviceIdCopied($event)" |
|||
[cbContent]="entity?.id?.id" |
|||
[fxShow]="!isEdit"> |
|||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
|||
<span translate>device.copyId</span> |
|||
</button> |
|||
<button mat-raised-button |
|||
(click)="copyAccessToken($event)" |
|||
[fxShow]="!isEdit"> |
|||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
|||
<span translate>device.copyAccessToken</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="mat-padding" fxLayout="column"> |
|||
<mat-form-field class="mat-block" |
|||
[fxShow]="!isEdit && isAssignedToCustomer(entity) |
|||
&& !entity?.customerIsPublic && deviceScope === 'tenant'"> |
|||
<mat-label translate>device.assignedToCustomer</mat-label> |
|||
<input matInput disabled [ngModel]="entity?.customerTitle"> |
|||
</mat-form-field> |
|||
<div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;" |
|||
[fxShow]="!isEdit && entity?.customerIsPublic && (deviceScope === 'customer' || deviceScope === 'tenant')"> |
|||
{{ 'device.device-public' | translate }} |
|||
</div> |
|||
<form #entityNgForm="ngForm" [formGroup]="entityForm"> |
|||
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>device.name</mat-label> |
|||
<input matInput formControlName="name" required> |
|||
<mat-error *ngIf="entityForm.get('name').hasError('required')"> |
|||
{{ 'device.name-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-entity-subtype-autocomplete |
|||
formControlName="type" |
|||
[required]="true" |
|||
[entityType]="entityType.DEVICE" |
|||
> |
|||
</tb-entity-subtype-autocomplete> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>device.label</mat-label> |
|||
<input matInput formControlName="label"> |
|||
</mat-form-field> |
|||
<div formGroupName="additionalInfo" fxLayout="column"> |
|||
<mat-checkbox fxFlex formControlName="gateway"> |
|||
{{ 'device.is-gateway' | translate }} |
|||
</mat-checkbox> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>device.description</mat-label> |
|||
<textarea matInput formControlName="description" rows="2"></textarea> |
|||
</mat-form-field> |
|||
</div> |
|||
</fieldset> |
|||
</form> |
|||
</div> |
|||
@ -0,0 +1,19 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
|
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { select, Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { EntityComponent } from '@shared/components/entity/entity.component'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { User } from '@shared/models/user.model'; |
|||
import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import {DeviceInfo} from '@shared/models/device.models'; |
|||
import {EntityType} from '@shared/models/entity-type.models'; |
|||
import {NULL_UUID} from '@shared/models/id/has-uuid'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-device', |
|||
templateUrl: './device.component.html', |
|||
styleUrls: ['./device.component.scss'] |
|||
}) |
|||
export class DeviceComponent extends EntityComponent<DeviceInfo> { |
|||
|
|||
entityType = EntityType; |
|||
|
|||
deviceScope: 'tenant' | 'customer' | 'customer_user'; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
public fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.deviceScope = this.entitiesTableConfig.componentsData.deviceScope; |
|||
super.ngOnInit(); |
|||
} |
|||
|
|||
hideDelete() { |
|||
if (this.entitiesTableConfig) { |
|||
return !this.entitiesTableConfig.deleteEnabled(this.entity); |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
isAssignedToCustomer(entity: DeviceInfo): boolean { |
|||
return entity && entity.customerId && entity.customerId.id !== NULL_UUID; |
|||
} |
|||
|
|||
buildForm(entity: DeviceInfo): FormGroup { |
|||
return this.fb.group( |
|||
{ |
|||
name: [entity ? entity.name : '', [Validators.required]], |
|||
type: [entity ? entity.type : null, [Validators.required]], |
|||
label: [entity ? entity.label : ''], |
|||
additionalInfo: this.fb.group( |
|||
{ |
|||
gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false], |
|||
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], |
|||
} |
|||
) |
|||
} |
|||
); |
|||
} |
|||
|
|||
updateForm(entity: DeviceInfo) { |
|||
this.entityForm.patchValue({name: entity.name}); |
|||
this.entityForm.patchValue({type: entity.type}); |
|||
this.entityForm.patchValue({label: entity.label}); |
|||
this.entityForm.patchValue({additionalInfo: |
|||
{gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false}}); |
|||
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import {DeviceComponent} from '@modules/home/pages/device/device.component'; |
|||
import {DeviceRoutingModule} from './device-routing.module'; |
|||
import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; |
|||
|
|||
@NgModule({ |
|||
entryComponents: [ |
|||
DeviceComponent, |
|||
DeviceTableHeaderComponent |
|||
], |
|||
declarations: [ |
|||
DeviceComponent, |
|||
DeviceTableHeaderComponent |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
DeviceRoutingModule |
|||
] |
|||
}) |
|||
export class DeviceModule { } |
|||
@ -0,0 +1,196 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
import {ActivatedRouteSnapshot, Resolve, Router} from '@angular/router'; |
|||
|
|||
import { Tenant } from '@shared/models/tenant.model'; |
|||
import { |
|||
CellActionDescriptor, |
|||
checkBoxCell, |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@shared/components/entity/entities-table-config.models'; |
|||
import { TenantService } from '@core/http/tenant.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { |
|||
EntityType, |
|||
entityTypeResources, |
|||
entityTypeTranslations |
|||
} from '@shared/models/entity-type.models'; |
|||
import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; |
|||
import { EntityAction } from '@shared/components/entity/entity-component.models'; |
|||
import { User } from '@shared/models/user.model'; |
|||
import {Device, DeviceInfo} from '@app/shared/models/device.models'; |
|||
import {DeviceComponent} from '@modules/home/pages/device/device.component'; |
|||
import {Observable, of} from 'rxjs'; |
|||
import {select, Store} from '@ngrx/store'; |
|||
import {selectAuth, selectAuthUser} from '@core/auth/auth.selectors'; |
|||
import {map, mergeMap, take, tap} from 'rxjs/operators'; |
|||
import {AppState} from '@core/core.state'; |
|||
import {DeviceService} from '@app/core/http/device.service'; |
|||
import {Authority} from '@app/shared/models/authority.enum'; |
|||
import {CustomerService} from '@core/http/customer.service'; |
|||
import {Customer} from '@app/shared/models/customer.model'; |
|||
import {NULL_UUID} from '@shared/models/id/has-uuid'; |
|||
import {BroadcastService} from '@core/services/broadcast.service'; |
|||
import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; |
|||
|
|||
@Injectable() |
|||
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> { |
|||
|
|||
private readonly config: EntityTableConfig<DeviceInfo> = new EntityTableConfig<DeviceInfo>(); |
|||
|
|||
private customerId: string; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private broadcast: BroadcastService, |
|||
private deviceService: DeviceService, |
|||
private customerService: CustomerService, |
|||
private translate: TranslateService, |
|||
private datePipe: DatePipe, |
|||
private router: Router) { |
|||
|
|||
this.config.entityType = EntityType.CUSTOMER; |
|||
this.config.entityComponent = DeviceComponent; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); |
|||
this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); |
|||
|
|||
this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', { deviceName: device.name }); |
|||
this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); |
|||
this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); |
|||
this.config.deleteEntitiesContent = () => this.translate.instant('device.delete-devices-text'); |
|||
|
|||
this.config.loadEntity = id => this.deviceService.getDeviceInfo(id.id); |
|||
this.config.saveEntity = device => { |
|||
return this.deviceService.saveDevice(device).pipe( |
|||
tap(() => { |
|||
this.broadcast.broadcast('deviceSaved'); |
|||
}), |
|||
mergeMap((savedDevice) => this.deviceService.getDeviceInfo(savedDevice.id.id) |
|||
)); |
|||
}; |
|||
this.config.onEntityAction = action => this.onDeviceAction(action); |
|||
|
|||
this.config.headerComponent = DeviceTableHeaderComponent; |
|||
|
|||
} |
|||
|
|||
resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DeviceInfo>> { |
|||
const routeParams = route.params; |
|||
this.config.componentsData = { |
|||
deviceScope: route.data.devicesType, |
|||
deviceType: '' |
|||
}; |
|||
this.customerId = routeParams.customerId; |
|||
return this.store.pipe(select(selectAuthUser), take(1)).pipe( |
|||
tap((authUser) => { |
|||
if (authUser.authority === Authority.CUSTOMER_USER) { |
|||
this.config.componentsData.deviceScope = 'customer_user'; |
|||
this.customerId = authUser.customerId; |
|||
} |
|||
}), |
|||
mergeMap(() => |
|||
this.customerId ? this.customerService.getCustomer(this.customerId) : of(null as Customer) |
|||
), |
|||
map((parentCustomer) => { |
|||
if (parentCustomer) { |
|||
if (parentCustomer.additionalInfo && parentCustomer.additionalInfo.isPublic) { |
|||
this.config.tableTitle = this.translate.instant('customer.public-devices'); |
|||
} else { |
|||
this.config.tableTitle = parentCustomer.title + ': ' + this.translate.instant('device.devices'); |
|||
} |
|||
} else { |
|||
this.config.tableTitle = this.translate.instant('device.devices'); |
|||
} |
|||
this.config.columns = this.configureColumns(this.config.componentsData.deviceScope); |
|||
this.configureEntityFunctions(this.config.componentsData.deviceScope); |
|||
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); |
|||
return this.config; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
configureColumns(deviceScope: string): Array<EntityTableColumn<Device | DeviceInfo>> { |
|||
const columns: Array<EntityTableColumn<Device | DeviceInfo>> = [ |
|||
new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'), |
|||
new EntityTableColumn<DeviceInfo>('name', 'device.name'), |
|||
new EntityTableColumn<DeviceInfo>('type', 'device.device-type'), |
|||
new EntityTableColumn<DeviceInfo>('label', 'device.label') |
|||
]; |
|||
if (deviceScope === 'tenant') { |
|||
columns.push( |
|||
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer'), |
|||
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px', |
|||
entity => { |
|||
return checkBoxCell(entity.customerIsPublic); |
|||
}, () => ({}), false), |
|||
); |
|||
} |
|||
columns.push( |
|||
new EntityTableColumn<DeviceInfo>('gateway', 'device.is-gateway', '60px', |
|||
entity => { |
|||
return checkBoxCell(entity.additionalInfo && entity.additionalInfo.gateway); |
|||
}, () => ({}), false) |
|||
); |
|||
return columns; |
|||
} |
|||
|
|||
configureEntityFunctions(deviceScope: string): void { |
|||
if (deviceScope === 'tenant') { |
|||
this.config.entitiesFetchFunction = pageLink => this.deviceService.getTenantDeviceInfos(pageLink, this.config.componentsData.deviceType); |
|||
this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id); |
|||
} else { |
|||
this.config.entitiesFetchFunction = pageLink => this.deviceService.getCustomerDeviceInfos(this.customerId, pageLink, this.config.componentsData.deviceType); |
|||
this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); |
|||
} |
|||
} |
|||
|
|||
configureCellActions(deviceScope: string): Array<CellActionDescriptor<Device | DeviceInfo>> { |
|||
const actions: Array<CellActionDescriptor<Device | DeviceInfo>> = []; |
|||
if (deviceScope === 'tenant') { |
|||
actions.push( |
|||
{ |
|||
name: this.translate.instant('device.make-public'), |
|||
icon: 'share', |
|||
isEnabled: (entity) => (!entity.customerId || entity.customerId.id === NULL_UUID), |
|||
onAction: ($event, entity) => this.makePublic($event, entity) |
|||
} |
|||
); |
|||
} |
|||
return actions; |
|||
} |
|||
|
|||
makePublic($event: Event, device: Device) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
// TODO:
|
|||
} |
|||
|
|||
onDeviceAction(action: EntityAction<Device | DeviceInfo>): boolean { |
|||
switch (action.action) { |
|||
case 'makePublic': |
|||
this.makePublic(action.event, action.entity); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block"> |
|||
<mat-label>{{ entitySubtypeText | translate }}</mat-label> |
|||
<input matInput type="text" placeholder="{{ selectEntitySubtypeText | translate }}" |
|||
#subTypeInput |
|||
formControlName="subType" |
|||
[required]="required" |
|||
[matAutocomplete]="subTypeAutocomplete"> |
|||
<button *ngIf="subTypeFormGroup.get('subType').value && !disabled" |
|||
type="button" |
|||
matSuffix mat-button mat-icon-button aria-label="Clear" |
|||
(click)="clear()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete #subTypeAutocomplete="matAutocomplete" [displayWith]="displaySubTypeFn"> |
|||
<mat-option *ngFor="let subType of filteredSubTypes | async" [value]="subType"> |
|||
<span [innerHTML]="subType.type | highlight:searchText"></span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')"> |
|||
{{ entitySubtypeRequiredText | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,228 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core'; |
|||
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms'; |
|||
import {Observable, of, throwError, Subscription} from 'rxjs'; |
|||
import {PageLink} from '@shared/models/page/page-link'; |
|||
import {Direction} from '@shared/models/page/sort-order'; |
|||
import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators'; |
|||
import {PageData, emptyPageData} from '@shared/models/page/page-data'; |
|||
import {DashboardInfo} from '@app/shared/models/dashboard.models'; |
|||
import {DashboardId} from '@app/shared/models/id/dashboard-id'; |
|||
import {DashboardService} from '@core/http/dashboard.service'; |
|||
import {Store} from '@ngrx/store'; |
|||
import {AppState} from '@app/core/core.state'; |
|||
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors'; |
|||
import {Authority} from '@shared/models/authority.enum'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {DeviceService} from '@core/http/device.service'; |
|||
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models'; |
|||
import {BroadcastService} from '@app/core/services/broadcast.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-subtype-autocomplete', |
|||
templateUrl: './entity-subtype-autocomplete.component.html', |
|||
styleUrls: [], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntitySubTypeAutocompleteComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class EntitySubTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { |
|||
|
|||
subTypeFormGroup: FormGroup; |
|||
|
|||
modelValue: string | null; |
|||
|
|||
@Input() |
|||
entityType: EntityType; |
|||
|
|||
@Input() |
|||
required: boolean; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef; |
|||
|
|||
selectEntitySubtypeText: string; |
|||
entitySubtypeText: string; |
|||
entitySubtypeRequiredText: string; |
|||
|
|||
filteredSubTypes: Observable<Array<EntitySubtype>>; |
|||
|
|||
subTypes: Observable<Array<EntitySubtype>>; |
|||
|
|||
private broadcastSubscription: Subscription; |
|||
|
|||
private searchText = ''; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private broadcast: BroadcastService, |
|||
public translate: TranslateService, |
|||
private deviceService: DeviceService, |
|||
private fb: FormBuilder) { |
|||
this.subTypeFormGroup = this.fb.group({ |
|||
subType: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
|
|||
switch (this.entityType) { |
|||
case EntityType.ASSET: |
|||
this.selectEntitySubtypeText = 'asset.select-asset-type'; |
|||
this.entitySubtypeText = 'asset.asset-type'; |
|||
this.entitySubtypeRequiredText = 'asset.asset-type-required'; |
|||
this.broadcastSubscription = this.broadcast.on('assetSaved', () => { |
|||
this.subTypes = null; |
|||
}); |
|||
break; |
|||
case EntityType.DEVICE: |
|||
this.selectEntitySubtypeText = 'device.select-device-type'; |
|||
this.entitySubtypeText = 'device.device-type'; |
|||
this.entitySubtypeRequiredText = 'device.device-type-required'; |
|||
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { |
|||
this.subTypes = null; |
|||
}); |
|||
break; |
|||
case EntityType.ENTITY_VIEW: |
|||
this.selectEntitySubtypeText = 'entity-view.select-entity-view-type'; |
|||
this.entitySubtypeText = 'entity-view.entity-view-type'; |
|||
this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; |
|||
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { |
|||
this.subTypes = null; |
|||
}); |
|||
break; |
|||
} |
|||
|
|||
this.filteredSubTypes = this.subTypeFormGroup.get('subType').valueChanges |
|||
.pipe( |
|||
tap(value => { |
|||
let modelValue; |
|||
if (!value) { |
|||
modelValue = null; |
|||
} else if (typeof value === 'string') { |
|||
modelValue = value; |
|||
} else { |
|||
modelValue = value.type; |
|||
} |
|||
this.updateView(modelValue); |
|||
}), |
|||
startWith<string | EntitySubtype>(''), |
|||
map(value => value ? (typeof value === 'string' ? value : value.type) : ''), |
|||
mergeMap(type => this.fetchSubTypes(type) ) |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.broadcastSubscription) { |
|||
this.broadcastSubscription.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(value: string | null): void { |
|||
this.searchText = ''; |
|||
if (value != null) { |
|||
this.modelValue = value; |
|||
this.fetchSubTypes(value, true).subscribe( |
|||
(subTypes) => { |
|||
const subType = subTypes && subTypes.length === 1 ? subTypes[0] : null; |
|||
this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true}); |
|||
} |
|||
); |
|||
} else { |
|||
this.modelValue = null; |
|||
this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true}); |
|||
} |
|||
} |
|||
|
|||
updateView(value: string | null) { |
|||
if (this.modelValue !== value) { |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displaySubTypeFn(subType?: EntitySubtype): string | undefined { |
|||
return subType ? subType.type : undefined; |
|||
} |
|||
|
|||
fetchSubTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<EntitySubtype>> { |
|||
this.searchText = searchText; |
|||
return this.getSubTypes().pipe( |
|||
map(subTypes => subTypes.filter( subType => { |
|||
if (strictMatch) { |
|||
return searchText ? subType.type === searchText : false; |
|||
} else { |
|||
return searchText ? subType.type.toUpperCase().startsWith(searchText.toUpperCase()) : true; |
|||
} |
|||
})) |
|||
); |
|||
} |
|||
|
|||
getSubTypes(): Observable<Array<EntitySubtype>> { |
|||
if (!this.subTypes) { |
|||
switch (this.entityType) { |
|||
case EntityType.ASSET: |
|||
// TODO:
|
|||
break; |
|||
case EntityType.DEVICE: |
|||
this.subTypes = this.deviceService.getDeviceTypes(false, true); |
|||
break; |
|||
case EntityType.ENTITY_VIEW: |
|||
// TODO:
|
|||
break; |
|||
} |
|||
if (this.subTypes) { |
|||
this.subTypes = this.subTypes.pipe( |
|||
publishReplay(1), |
|||
refCount() |
|||
); |
|||
} else { |
|||
return throwError(null); |
|||
} |
|||
} |
|||
return this.subTypes; |
|||
} |
|||
|
|||
clear() { |
|||
this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.subTypeInput.nativeElement.blur(); |
|||
this.subTypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="subTypeFormGroup" class="mat-block"> |
|||
<mat-label *ngIf="showLabel">{{ entitySubtypeTitle | translate }}</mat-label> |
|||
<mat-select class="tb-entity-subtype-select" matInput formControlName="subType"> |
|||
<mat-option *ngFor="let subType of subTypesOptions | async" [value]="subType"> |
|||
{{ displaySubTypeFn(subType) }} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="subTypeFormGroup.get('subType').hasError('required')"> |
|||
{{ entitySubtypeRequiredText | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
mat-select.tb-entity-subtype-select { |
|||
min-width: 200px; |
|||
} |
|||
} |
|||
@ -0,0 +1,238 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild, OnDestroy} from '@angular/core'; |
|||
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms'; |
|||
import {Observable, of, throwError, Subscription, Subject} from 'rxjs'; |
|||
import {PageLink} from '@shared/models/page/page-link'; |
|||
import {Direction} from '@shared/models/page/sort-order'; |
|||
import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators'; |
|||
import {PageData, emptyPageData} from '@shared/models/page/page-data'; |
|||
import {DashboardInfo} from '@app/shared/models/dashboard.models'; |
|||
import {DashboardId} from '@app/shared/models/id/dashboard-id'; |
|||
import {DashboardService} from '@core/http/dashboard.service'; |
|||
import {Store} from '@ngrx/store'; |
|||
import {AppState} from '@app/core/core.state'; |
|||
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors'; |
|||
import {Authority} from '@shared/models/authority.enum'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {DeviceService} from '@core/http/device.service'; |
|||
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models'; |
|||
import {BroadcastService} from '@app/core/services/broadcast.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-subtype-select', |
|||
templateUrl: './entity-subtype-select.component.html', |
|||
styleUrls: ['./entity-subtype-select.component.scss'], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntitySubTypeSelectComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class EntitySubTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { |
|||
|
|||
subTypeFormGroup: FormGroup; |
|||
|
|||
modelValue: string | null; |
|||
|
|||
@Input() |
|||
entityType: EntityType; |
|||
|
|||
@Input() |
|||
showLabel: boolean; |
|||
|
|||
@Input() |
|||
required: boolean; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
typeTranslatePrefix: string; |
|||
|
|||
@ViewChild('subTypeInput', {static: true}) subTypeInput: ElementRef; |
|||
|
|||
entitySubtypeTitle: string; |
|||
entitySubtypeRequiredText: string; |
|||
|
|||
subTypesOptions: Observable<Array<EntitySubtype | string>>; |
|||
|
|||
private subTypesOptionsSubject: Subject<string> = new Subject(); |
|||
|
|||
subTypes: Observable<Array<EntitySubtype | string>>; |
|||
|
|||
private broadcastSubscription: Subscription; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private broadcast: BroadcastService, |
|||
public translate: TranslateService, |
|||
private deviceService: DeviceService, |
|||
private fb: FormBuilder) { |
|||
this.subTypeFormGroup = this.fb.group({ |
|||
subType: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
|
|||
switch (this.entityType) { |
|||
case EntityType.ASSET: |
|||
this.entitySubtypeTitle = 'asset.asset-type'; |
|||
this.entitySubtypeRequiredText = 'asset.asset-type-required'; |
|||
this.broadcastSubscription = this.broadcast.on('assetSaved', () => { |
|||
this.subTypes = null; |
|||
this.subTypesOptionsSubject.next(''); |
|||
}); |
|||
break; |
|||
case EntityType.DEVICE: |
|||
this.entitySubtypeTitle = 'device.device-type'; |
|||
this.entitySubtypeRequiredText = 'device.device-type-required'; |
|||
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { |
|||
this.subTypes = null; |
|||
this.subTypesOptionsSubject.next(''); |
|||
}); |
|||
break; |
|||
case EntityType.ENTITY_VIEW: |
|||
this.entitySubtypeTitle = 'entity-view.entity-view-type'; |
|||
this.entitySubtypeRequiredText = 'entity-view.entity-view-type-required'; |
|||
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { |
|||
this.subTypes = null; |
|||
this.subTypesOptionsSubject.next(''); |
|||
}); |
|||
break; |
|||
} |
|||
|
|||
this.subTypesOptions = this.subTypesOptionsSubject.asObservable().pipe( |
|||
startWith<string | EntitySubtype>(''), |
|||
mergeMap(() => this.getSubTypes()) |
|||
); |
|||
|
|||
this.subTypeFormGroup.get('subType').valueChanges.subscribe( |
|||
(value) => { |
|||
let modelValue; |
|||
if (!value || value === '') { |
|||
modelValue = ''; |
|||
} else { |
|||
modelValue = value.type; |
|||
} |
|||
this.updateView(modelValue); |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.broadcastSubscription) { |
|||
this.broadcastSubscription.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
writeValue(value: string | null): void { |
|||
if (value != null && value !== '') { |
|||
this.modelValue = value; |
|||
this.findSubTypes(value).subscribe( |
|||
(subTypes) => { |
|||
const subType = subTypes && subTypes.length === 1 ? subTypes[0] : ''; |
|||
this.subTypeFormGroup.get('subType').patchValue(subType, {emitEvent: true}); |
|||
} |
|||
); |
|||
} else { |
|||
this.modelValue = ''; |
|||
this.subTypeFormGroup.get('subType').patchValue('', {emitEvent: true}); |
|||
} |
|||
} |
|||
|
|||
updateView(value: string | null) { |
|||
if (this.modelValue !== value) { |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displaySubTypeFn(subType?: EntitySubtype | string): string | undefined { |
|||
if (subType && typeof subType !== 'string') { |
|||
if (this.typeTranslatePrefix) { |
|||
return this.translate.instant(this.typeTranslatePrefix + '.' + subType.type); |
|||
} else { |
|||
return subType.type; |
|||
} |
|||
} else { |
|||
return this.translate.instant('entity.all-subtypes'); |
|||
} |
|||
} |
|||
|
|||
findSubTypes(searchText?: string): Observable<Array<EntitySubtype | string>> { |
|||
return this.getSubTypes().pipe( |
|||
map(subTypes => subTypes.filter( subType => { |
|||
return searchText ? (typeof subType === 'string' ? false : subType.type === searchText) : false; |
|||
})) |
|||
); |
|||
} |
|||
|
|||
getSubTypes(): Observable<Array<EntitySubtype | string>> { |
|||
if (!this.subTypes) { |
|||
switch (this.entityType) { |
|||
case EntityType.ASSET: |
|||
// TODO:
|
|||
break; |
|||
case EntityType.DEVICE: |
|||
this.subTypes = this.deviceService.getDeviceTypes(false, true); |
|||
break; |
|||
case EntityType.ENTITY_VIEW: |
|||
// TODO:
|
|||
break; |
|||
} |
|||
if (this.subTypes) { |
|||
this.subTypes = this.subTypes.pipe( |
|||
map((allSubtypes) => { |
|||
allSubtypes.unshift(''); |
|||
return allSubtypes; |
|||
}), |
|||
publishReplay(1), |
|||
refCount() |
|||
); |
|||
} else { |
|||
return throwError(null); |
|||
} |
|||
} |
|||
return this.subTypes; |
|||
} |
|||
|
|||
clear() { |
|||
this.subTypeFormGroup.get('subType').patchValue(null, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.subTypeInput.nativeElement.blur(); |
|||
this.subTypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import {BaseData} from '@shared/models/base-data'; |
|||
import {DeviceId} from './id/device-id'; |
|||
import {TenantId} from '@shared/models/id/tenant-id'; |
|||
import {CustomerId} from '@shared/models/id/customer-id'; |
|||
|
|||
export interface Device extends BaseData<DeviceId> { |
|||
tenantId: TenantId; |
|||
customerId: CustomerId; |
|||
name: string; |
|||
type: string; |
|||
label: string; |
|||
additionalInfo?: any; |
|||
} |
|||
|
|||
export interface DeviceInfo extends Device { |
|||
customerTitle: string; |
|||
customerIsPublic: boolean; |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
///
|
|||
/// Copyright © 2016-2019 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { EntityId } from './entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
export class DeviceId implements EntityId { |
|||
entityType = EntityType.DEVICE; |
|||
id: string; |
|||
constructor(id: string) { |
|||
this.id = id; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue