Merge branch 'master' of github.com:thingsboard/thingsboard

This commit is contained in:
Andrii Shvaika 2020-11-11 09:19:18 +02:00
commit fe028a4fe5
22 changed files with 275 additions and 132 deletions

View File

@ -391,16 +391,16 @@
},
{
"alias": "web_camera_input",
"name": "Web Camera Input",
"name": "Photo camera input",
"descriptor": {
"type": "latest",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-web-camera-widget \n [ctx]=\"ctx\">\n</tb-web-camera-widget>",
"templateHtml": "<tb-photo-camera-widget \n [ctx]=\"ctx\">\n</tb-photo-camera-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.webCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Web Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Photo Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}

View File

@ -55,6 +55,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
@ -158,7 +159,11 @@ public class AccessValidator {
new FutureCallback<DeferredResult<ResponseEntity>>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
onSuccess.accept(response, currentUser.getTenantId(), entityId);
try {
onSuccess.accept(response, currentUser.getTenantId(), entityId);
} catch (Exception e) {
onFailure(e);
}
}
@Override
@ -434,9 +439,9 @@ public class AccessValidator {
public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
ResponseEntity responseEntity;
if (e != null && e instanceof ToErrorResponseEntity) {
if (e instanceof ToErrorResponseEntity) {
responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
} else if (e != null && e instanceof IllegalArgumentException) {
} else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) {
responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} else {
responseEntity = new ResponseEntity<>(defaultErrorStatus);

View File

@ -76,8 +76,8 @@
"prettier": "^2.1.2",
"prop-types": "^15.7.2",
"raphael": "^2.3.0",
"rc-select": "^11.3.3",
"react": "^16.13.1",
"rc-select": "~10.5.1",
"react": "~16.14.0",
"react-ace": "^9.1.4",
"react-dom": "^16.13.1",
"react-dropzone": "^11.2.0",
@ -92,7 +92,7 @@
"tslib": "^2.0.2",
"tv4": "^1.3.0",
"typeface-roboto": "^1.1.13",
"zone.js": "~0.11.1"
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^10.0.1",

View File

@ -52,6 +52,7 @@ export interface AlarmDataSubscriptionOptions {
export class AlarmDataSubscription {
private alarmDataSubscriptionOptions = this.listener.alarmDataSubscriptionOptions;
private datasourceType: DatasourceType = this.alarmDataSubscriptionOptions.datasourceType;
private history: boolean;
@ -65,8 +66,7 @@ export class AlarmDataSubscription {
private subsTw: SubscriptionTimewindow;
constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions,
private listener: AlarmDataListener,
constructor(private listener: AlarmDataListener,
private telemetryService: TelemetryService) {
}

View File

@ -32,6 +32,7 @@ export interface AlarmDataListener {
alarmSource: Datasource;
alarmsLoaded: (pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) => void;
alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void;
alarmDataSubscriptionOptions?: AlarmDataSubscriptionOptions;
subscription?: AlarmDataSubscription;
}
@ -47,11 +48,11 @@ export class AlarmDataService {
pageLink: AlarmDataPageLink,
keyFilters: KeyFilter[]) {
const alarmSource = listener.alarmSource;
listener.alarmDataSubscriptionOptions = this.createAlarmSubscriptionOptions(listener, pageLink, keyFilters);
if (alarmSource.type === DatasourceType.entity && (!alarmSource.entityFilter || !pageLink)) {
return;
}
listener.subscription = this.createSubscription(listener,
pageLink, alarmSource.keyFilters, keyFilters);
listener.subscription = new AlarmDataSubscription(listener, this.telemetryService);
return listener.subscription.subscribe();
}
@ -61,10 +62,9 @@ export class AlarmDataService {
}
}
private createSubscription(listener: AlarmDataListener,
pageLink: AlarmDataPageLink,
keyFilters: KeyFilter[],
additionalKeyFilters: KeyFilter[]): AlarmDataSubscription {
private createAlarmSubscriptionOptions(listener: AlarmDataListener,
pageLink: AlarmDataPageLink,
additionalKeyFilters: KeyFilter[]): AlarmDataSubscriptionOptions {
const alarmSource = listener.alarmSource;
const alarmSubscriptionDataKeys: Array<AlarmSubscriptionDataKey> = [];
alarmSource.dataKeys.forEach((dataKey) => {
@ -82,11 +82,10 @@ export class AlarmDataService {
if (alarmDataSubscriptionOptions.datasourceType === DatasourceType.entity) {
alarmDataSubscriptionOptions.entityFilter = alarmSource.entityFilter;
alarmDataSubscriptionOptions.pageLink = pageLink;
alarmDataSubscriptionOptions.keyFilters = keyFilters;
alarmDataSubscriptionOptions.keyFilters = alarmSource.keyFilters;
alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
}
return new AlarmDataSubscription(alarmDataSubscriptionOptions,
listener, this.telemetryService);
return alarmDataSubscriptionOptions;
}
}

View File

@ -960,8 +960,8 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateAlarmDataSubscription() {
if (this.alarmDataListener) {
const pageLink = this.alarmDataListener.subscription.alarmDataSubscriptionOptions.pageLink;
const keyFilters = this.alarmDataListener.subscription.alarmDataSubscriptionOptions.additionalKeyFilters;
const pageLink = this.alarmDataListener.alarmDataSubscriptionOptions.pageLink;
const keyFilters = this.alarmDataListener.alarmDataSubscriptionOptions.additionalKeyFilters;
this.subscribeForAlarms(pageLink, keyFilters);
}
}
@ -1226,9 +1226,6 @@ export class WidgetSubscription implements IWidgetSubscription {
});
});
}
if (this.displayLegend) {
this.legendData.keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
}
if (this.caulculateLegendData) {
this.data.forEach((dataSetHolder, keyIndex) => {
this.updateLegend(keyIndex, dataSetHolder.data, false);

View File

@ -19,8 +19,9 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeData, AttributeScope, DataSortOrder, TimeseriesData } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
import { AggregationType } from '@shared/models/time/time.models';
@Injectable({
providedIn: 'root'
@ -110,4 +111,28 @@ export class AttributeService {
}
return forkJoin([saveEntityTimeseriesObservable, deleteEntityTimeseriesObservable]);
}
public getEntityTimeseries(entityId: EntityId, keys: Array<string>, startTs: number, endTs: number,
limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number,
orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false,
config?: RequestConfig): Observable<TimeseriesData> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`;
if (isDefinedAndNotNull(limit)) {
url += `&limit=${limit}`;
}
if (isDefinedAndNotNull(agg)) {
url += `&agg=${agg}`;
}
if (isDefinedAndNotNull(interval)) {
url += `&interval=${interval}`;
}
if (isDefinedAndNotNull(orderBy)) {
url += `&orderBy=${orderBy}`;
}
if (isDefinedAndNotNull(useStrictDataTypes)) {
url += `&useStrictDataTypes=${useStrictDataTypes}`;
}
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
}
}

View File

@ -38,6 +38,9 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="sortDataKeys">
{{ 'legend.sort-legend' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMin">
{{ 'legend.show-min' | translate }}
</mat-checkbox>

View File

@ -68,6 +68,7 @@ export class LegendConfigPanelComponent extends PageComponent implements OnInit
this.legendConfigForm = this.fb.group({
direction: [this.data.legendConfig.direction, []],
position: [this.data.legendConfig.position, []],
sortDataKeys: [this.data.legendConfig.sortDataKeys, []],
showMin: [this.data.legendConfig.showMin, []],
showMax: [this.data.legendConfig.showMax, []],
showAvg: [this.data.legendConfig.showAvg, []],

View File

@ -60,8 +60,11 @@ export class LegendComponent implements OnInit {
}
legendKeys(): LegendKey[] {
return this.legendData.keys
.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend);
let keys = this.legendData.keys;
if (this.legendConfig.sortDataKeys) {
keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
}
return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend);
}
}

View File

@ -16,13 +16,13 @@
-->
<div fxLayout="column" fxLayoutAlign="center center" class="tb-web-camera" tb-fullscreen [fullscreen]="isShowCamera">
<div *ngIf="isEntityDetected && dataKeyDetected && isCameraSupport && isDeviceDetect" fxFlexFill>
<div *ngIf="isEntityDetected && dataKeyDetected && isCameraSupport && isProtocolHttps" class="image-container">
<div [fxShow]="!isShowCamera" fxLayout="column" fxLayoutAlign="space-between center" fxFlexFill>
<div class="tb-web-camera__last-photo" fxFlex>
<span [fxShow]="!lastPhoto" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span>
<img [fxShow]="lastPhoto" class="tb-web-camera__last-photo_img" [src]="lastPhoto" alt="last photo"/>
</div>
<button mat-raised-button color="primary" (click)="takePhoto()">
<button mat-raised-button color="primary" (click)="takePhoto()" *ngIf="!textMessage">
{{ "widgets.input-widgets.take-photo" | translate }}
</button>
</div>
@ -59,16 +59,7 @@
</div>
</div>
</div>
<div class="message-text" *ngIf="!isEntityDetected">
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
</div>
<div class="message-text" *ngIf="isEntityDetected && !dataKeyDetected">
{{ 'widgets.input-widgets.no-datakey-selected' | translate }}
</div>
<div class="message-text" *ngIf="isEntityDetected && dataKeyDetected && !isCameraSupport">
{{ 'widgets.input-widgets.no-support-web-camera' | translate }}
</div>
<div class="message-text" *ngIf="isEntityDetected && dataKeyDetected && isCameraSupport && !isDeviceDetect">
{{ 'widgets.input-widgets.no-support-web-camera' | translate }}
<div class="message-text" *ngIf="textMessage">
{{ textMessage | translate }}
</div>
</div>

View File

@ -18,6 +18,7 @@
&__last-photo {
width: 100%;
min-height: 0;
margin: 5px 0;
text-align: center;
border: solid 1px;
@ -71,4 +72,11 @@
color: #a0a0a0;
text-align: center;
}
.image-container{
height: 100%;
min-height: 0;
width: 100%;
min-width: 100%;
}
}

View File

@ -40,7 +40,7 @@ import { Observable } from 'rxjs';
import { isString } from '@core/utils';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
interface WebCameraInputWidgetSettings {
interface PhotoCameraInputWidgetSettings {
widgetTitle: string;
imageQuality: number;
imageFormat: string;
@ -50,12 +50,12 @@ interface WebCameraInputWidgetSettings {
// @dynamic
@Component({
selector: 'tb-web-camera-widget',
templateUrl: './web-camera-input.component.html',
styleUrls: ['./web-camera-input.component.scss'],
selector: 'tb-photo-camera-widget',
templateUrl: './photo-camera-input.component.html',
styleUrls: ['./photo-camera-input.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WebCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
constructor(@Inject(WINDOW) private window: Window,
protected store: Store<AppState>,
@ -93,11 +93,11 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
@Input()
ctx: WidgetContext;
@ViewChild('videoStream', {static: true}) videoStreamRef: ElementRef<HTMLVideoElement>;
@ViewChild('canvas', {static: true}) canvasRef: ElementRef<HTMLCanvasElement>;
@ViewChild('videoStream', {static: false}) videoStreamRef: ElementRef<HTMLVideoElement>;
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
private videoInputsIndex = 0;
private settings: WebCameraInputWidgetSettings;
private settings: PhotoCameraInputWidgetSettings;
private datasource: Datasource;
private width = 640;
private height = 480;
@ -106,10 +106,13 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
isEntityDetected = false;
dataKeyDetected = false;
isProtocolHttps = false;
isCameraSupport = false;
isDeviceDetect = false;
isShowCamera = false;
isPreviewPhoto = false;
isHavePermissionCamera = true;
isLoading = false;
singleDevice = true;
updatePhoto = false;
previewPhoto: SafeUrl;
@ -132,7 +135,8 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
}
ngOnInit(): void {
this.ctx.$scope.webCameraInputWidget = this;
this.ctx.$scope.photoCameraInputWidget = this;
this.isLoading = true;
this.settings = this.ctx.settings;
this.datasource = this.ctx.datasources[0];
@ -168,25 +172,33 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
}
public onDataUpdated() {
this.ngZone.run(() => {
this.updateWidgetData(this.ctx.defaultSubscription.data);
this.ctx.detectChanges();
});
this.updateWidgetData(this.ctx.defaultSubscription.data);
this.ctx.detectChanges();
}
private detectAvailableDevices(): void {
if (WebCameraInputWidgetComponent.hasGetUserMedia()) {
this.isCameraSupport = true;
WebCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
this.isDeviceDetect = !!devices.length;
this.singleDevice = devices.length < 2;
this.availableVideoInputs = devices;
this.ctx.detectChanges();
}, () => {
this.availableVideoInputs = [];
}
)
if (this.window.location.protocol === 'https:' || this.window.location.hostname === 'localhost') {
this.isProtocolHttps = true;
if (PhotoCameraInputWidgetComponent.hasGetUserMedia()) {
this.isCameraSupport = true;
PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
this.isLoading = false;
this.isDeviceDetect = !!devices.length;
this.singleDevice = devices.length < 2;
this.availableVideoInputs = devices;
this.ctx.detectChanges();
}, () => {
this.isLoading = false;
this.availableVideoInputs = [];
}
);
} else {
this.isLoading = false;
}
} else {
this.isLoading = false;
}
}
@ -206,8 +218,7 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
}
takePhoto() {
this.isShowCamera = true;
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId);
this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId, true);
}
closeCamera() {
@ -243,13 +254,13 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
this.closeCamera();
}, () => {
this.updatePhoto = false;
})
});
}
switchWebCamera() {
this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length;
this.stopMediaTracks();
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId)
this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId);
}
createPhoto() {
@ -257,22 +268,53 @@ export class WebCameraInputWidgetComponent extends PageComponent implements OnIn
this.canvasElement.height = this.videoHeight;
this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : WebCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : WebCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.sanitizer.bypassSecurityTrustUrl(this.canvasElement.toDataURL(mimeType, quality));
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
this.isPreviewPhoto = true;
}
private initWebCamera(deviceId?: string) {
private inititedVideoStream(deviceId?: string, init = false) {
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
const videoTrackConstraints = {
video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
};
window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
if (init) {
this.isShowCamera = true;
}
this.mediaStream = stream;
this.videoElement.srcObject = stream;
})
this.ctx.detectChanges();
}, () => {
this.isHavePermissionCamera = false;
});
}
}
get textMessage() {
if (this.isLoading) {
return '';
}
if (!this.isProtocolHttps) {
return 'widgets.input-widgets.enable-https-use-widget';
}
if (!this.isCameraSupport) {
return 'widgets.input-widgets.no-support-web-camera';
}
if (!this.isEntityDetected) {
return 'widgets.input-widgets.no-entity-selected';
}
if (!this.dataKeyDetected) {
return 'widgets.input-widgets.no-datakey-selected';
}
if (!this.isDeviceDetect) {
return 'widgets.input-widgets.no-found-your-camera';
}
if (!this.isHavePermissionCamera) {
return 'widgets.input-widgets.no-permission-camera';
}
return null;
}
}

View File

@ -32,7 +32,7 @@ import {
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
import { TripAnimationComponent } from './trip-animation/trip-animation.component';
import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component';
import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@ -49,7 +49,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
DateRangeNavigatorPanelComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
WebCameraInputWidgetComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent
],
imports: [
@ -67,7 +67,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
DateRangeNavigatorWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
WebCameraInputWidgetComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent
],
providers: [

View File

@ -76,13 +76,15 @@
[required]="!createProfile"
[transportType]="deviceWizardFormGroup.get('transportType').value"
formControlName="deviceProfileId"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
(deviceProfileChanged)="$event?.transportType ? deviceWizardFormGroup.get('transportType').patchValue($event?.transportType) : {}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[selectFirstProfile]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block">
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
@ -93,12 +95,14 @@
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-rule-chain-autocomplete
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
labelText="device-profile.default-rule-chain"
formControlName="defaultRuleChainId">
</tb-rule-chain-autocomplete>
</div>
<div fxLayout="column" fxLayoutAlign="flex-end start">
<tb-queue-type-list
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}"
[queueType]="serviceType"
formControlName="defaultQueueName">
</tb-queue-type-list>

View File

@ -22,6 +22,10 @@
max-height: 75vh;
}
}
.invisible{
visibility: hidden;
}
}
:host ::ng-deep {
@ -60,6 +64,12 @@
height: 100%;
}
}
tb-device-profile-autocomplete, tb-queue-type-list{
.mat-form-field-wrapper{
width: 180px !important;
}
}
}
}
}

View File

@ -37,6 +37,7 @@
.tb-nav-header-toolbar {
min-height: 64px;
height: inherit;
padding: 0;
z-index: 2;
flex-shrink: 0;
white-space: nowrap;
@ -45,7 +46,9 @@
height: 64px;
.tb-logo-title {
width: auto;
max-width: 100%;
height: 36px;
max-height: 100%;
margin: auto;
}
}

View File

@ -26,7 +26,7 @@ import { EntityInfoData } from '@shared/models/entity.models';
import { KeyFilter } from '@shared/models/query/query.models';
import { TimeUnit } from '@shared/models/time/time.models';
import * as _moment from 'moment-timezone';
import { AbstractControl, FormGroup, ValidationErrors } from '@angular/forms';
import { AbstractControl, ValidationErrors } from '@angular/forms';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT'
@ -513,7 +513,7 @@ export function timeOfDayToUTCTimestamp(date: Date | number): number {
}
export function utcTimestampToTimeOfDay(time = 0): Date {
return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
return new Date(time + new Date(time).getTimezoneOffset() * 60 * 1000);
}
function timeOfDayToMoment(date: Date | number): _moment.Moment {

View File

@ -21,7 +21,14 @@ import { Observable, ReplaySubject, Subject } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { map } from 'rxjs/operators';
import { NgZone } from '@angular/core';
import { AlarmData, AlarmDataQuery, EntityData, EntityDataQuery, EntityKey } from '@shared/models/query/query.models';
import {
AlarmData,
AlarmDataQuery,
EntityData,
EntityDataQuery,
EntityKey,
TsValue
} from '@shared/models/query/query.models';
import { PageData } from '@shared/models/page/page-data';
export enum DataKeyType {
@ -81,6 +88,15 @@ export interface AttributeData {
value: any;
}
export interface TimeseriesData {
[key: string]: Array<TsValue>;
}
export enum DataSortOrder {
ASC = 'ASC',
DESC = 'DESC'
}
export interface WebsocketCmd {
cmdId: number;
}

View File

@ -203,6 +203,7 @@ export const legendPositionTranslationMap = new Map<LegendPosition, string>(
export interface LegendConfig {
position: LegendPosition;
direction?: LegendDirection;
sortDataKeys: boolean;
showMin: boolean;
showMax: boolean;
showAvg: boolean;
@ -213,6 +214,7 @@ export function defaultLegendConfig(wType: widgetType): LegendConfig {
return {
direction: LegendDirection.column,
position: LegendPosition.bottom,
sortDataKeys: false,
showMin: false,
showMax: false,
showAvg: wType === widgetType.timeseries,

View File

@ -875,7 +875,7 @@
"profile-configuration": "Profile configuration",
"transport-configuration": "Transport configuration",
"default-rule-chain": "Default rule chain",
"select-queue-hint": "The queue name can be selected from a drop-down list or add a custom name.",
"select-queue-hint": "Select from a drop-down list or add a custom name.",
"delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?",
"delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.",
"delete-device-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 device profile} other {# device profiles} }?",
@ -1668,6 +1668,7 @@
"legend": {
"direction": "Legend direction",
"position": "Legend position",
"sort-legend": "Sort datakeys in legend",
"show-max": "Show max value",
"show-min": "Show min value",
"show-avg": "Show average value",
@ -2318,7 +2319,10 @@
"no-entity-selected": "No entity selected",
"no-image": "No image",
"no-support-geolocation": "Your browser doesn't support geolocation",
"no-support-web-camera": "No supported web camera",
"no-support-web-camera": "Your browser does not support cameras",
"enable-https-use-widget": "Please enable HTTPS to use this widget",
"no-found-your-camera": "Can't find your camera",
"no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera",
"no-timeseries-selected": "No timeseries selected",
"secret-key": "Secret key",
"secret-key-required": "Secret key is required",

View File

@ -272,6 +272,11 @@
dependencies:
tslib "^2.0.0"
"@ant-design/css-animation@^1.7.2":
version "1.7.3"
resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.3.tgz#60a1c970014e86b28f940510d69e503e428f1136"
integrity sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA==
"@auth0/angular-jwt@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@auth0/angular-jwt/-/angular-jwt-5.0.1.tgz#37851d3ca2a0e88b3e673afd7dd2891f0c61bdf5"
@ -1787,6 +1792,13 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
add-dom-event-listener@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310"
integrity sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==
dependencies:
object-assign "4.x"
adjust-sourcemap-loader@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz#6471143af75ec02334b219f54bc7970c52fb29a4"
@ -6752,7 +6764,7 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@ -7645,7 +7657,7 @@ promise-retry@^1.1.1:
err-code "^1.0.0"
retry "^0.10.0"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -7810,6 +7822,13 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
raf@^3.4.0, raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -7866,50 +7885,63 @@ rc-align@^4.0.0:
rc-util "^5.3.0"
resize-observer-polyfill "^1.5.1"
rc-motion@^2.0.0, rc-motion@^2.0.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.3.1.tgz#a0c9f402c267bd03543ef80a970297a6ba77c503"
integrity sha512-UAB2gwS9c1DuCFKVCimAkL2JUEGCwzgCbb+VslhIMFg6vY7oyMxYIQ6hkoji1PzgDEw0tHIhP+a17R6Y5DGMrQ==
rc-animate@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.1.tgz#defdd863f56816c222534e4dc68feddecd081386"
integrity sha512-8wg2Zg3EETy0k/9kYuis30NJNQg1D6/WSQwnCiz6SvyxQXNet/rVraRz3bPngwY6rcU2nlRvoShiYOorXyF7Sg==
dependencies:
"@ant-design/css-animation" "^1.7.2"
classnames "^2.2.6"
raf "^3.4.0"
rc-util "^4.15.3"
rc-motion@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-1.1.2.tgz#07212f1b96c715b8245ec121339146c4a9b0968c"
integrity sha512-YC/E7SSWKBFakYg4PENhSRWD4ZLDqkI7FKmutJcrMewZ91/ZIWfoZSDvPaBdKO0hsFrrzWepFhXQIq0FNnCMWA==
dependencies:
"@babel/runtime" "^7.11.1"
classnames "^2.2.1"
rc-util "^5.2.1"
raf "^3.4.1"
rc-util "^5.0.6"
rc-resize-observer@^0.2.3:
version "0.2.5"
resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-0.2.5.tgz#03e3a5c3dfccd6c996a547e4f82721e4f20f6156"
integrity sha512-cc4sOI722MVoCkGf/ZZybDVsjxvnH0giyDdA7wBJLTiMSFJ0eyxBMnr0JLYoClxftjnr75Xzl/VUB3HDrAx04Q==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.1"
rc-util "^5.0.0"
resize-observer-polyfill "^1.5.1"
rc-select@^11.3.3:
version "11.3.3"
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-11.3.3.tgz#ba445ac4d2d933dd1f80b796c1de28ce6c81bbf8"
integrity sha512-YMsGVEZxXctj15nIZKlFCkiOxMe0PNBeACN6nHqDozDYKR/aqP8J3XZqZ5Gw/fcgS4bI50zPVMieJKlY8/6Wfw==
rc-select@~10.5.1:
version "10.5.1"
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-10.5.1.tgz#4d4c5d4f8d2fd3b7e3dccf74e4c43142b2979247"
integrity sha512-fZraoNNhjUmDJccfk6VYgrgHBWFmHWh/NJZh2Xttcm/usOolYI1RjO9ikP4QGhzlJBFtnTLH2pE2nchjn3TCXA==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "2.x"
rc-motion "^2.0.1"
rc-trigger "^5.0.4"
rc-animate "^3.0.0"
rc-trigger "^4.3.0"
rc-util "^5.0.1"
rc-virtual-list "^3.0.3"
rc-virtual-list "^1.1.2"
warning "^4.0.3"
rc-trigger@^5.0.4:
version "5.0.6"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.0.6.tgz#7e84717525871a7923a671edb5a290e9e616525b"
integrity sha512-L/xIX5OO7a/bdTH0yYT9mwAsV6oM1inAqAbLjoUzpcIW+UUE3jjVOjm5VaKDfHd41rzDzOfN05URmhet5QzXKQ==
rc-trigger@^4.3.0:
version "4.4.3"
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.4.3.tgz#ed449cd6cce446555bc57f4394229c5c7154f4b0"
integrity sha512-yq/WyuiPwxd2q6jy+VPyy0GUCRFJ2eFqAaCwPE27AOftXeIupOcJ/2t1wakSq63cfk7qtzev5DKHUAjb8LOJCw==
dependencies:
"@babel/runtime" "^7.11.2"
classnames "^2.2.6"
raf "^3.4.1"
rc-align "^4.0.0"
rc-motion "^2.0.0"
rc-util "^5.3.4"
rc-motion "^1.0.0"
rc-util "^5.0.1"
rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.7, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.3.4:
rc-util@^4.15.3:
version "4.21.1"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05"
integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==
dependencies:
add-dom-event-listener "^1.1.0"
prop-types "^15.5.10"
react-is "^16.12.0"
react-lifecycles-compat "^3.0.4"
shallowequal "^1.1.0"
rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004"
integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg==
@ -7917,14 +7949,14 @@ rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.7, rc-util@^5.2.1, rc-util@^5.3.0,
react-is "^16.12.0"
shallowequal "^1.1.0"
rc-virtual-list@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.1.0.tgz#ca7ddbb291dace89c00cc4198ca7ef6e5e2034f7"
integrity sha512-DYU3wOjVuQo4hzYvmmpnoNtxrd8OIcutazA90x374ZFGGm4xYoSjCdh6UhBLi47IJI2BRry4l583nuoi7+06GA==
rc-virtual-list@^1.1.2:
version "1.1.6"
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a"
integrity sha512-u3+izqWL8p8bQy8nYH48qWpiGyxR/ye8D2k0zJlXmfYeL55/xh83YrzHqiDzO78uj0Ewag3nXDA0JTVrYO7ygQ==
dependencies:
classnames "^2.2.6"
rc-resize-observer "^0.2.3"
rc-util "^5.0.7"
raf "^3.4.1"
rc-util "^5.0.0"
react-ace@^9.1.4:
version "9.1.4"
@ -7961,6 +7993,11 @@ react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-transition-group@^4.0.0, react-transition-group@^4.4.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@ -7971,10 +8008,10 @@ react-transition-group@^4.0.0, react-transition-group@^4.4.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
react@~16.14.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@ -10206,10 +10243,3 @@ zone.js@~0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16"
integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==
zone.js@~0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.1.tgz#0301d00d26febb2722f074c46aac4a948698ce39"
integrity sha512-KcZawpmVgS+3U2rzKTM6fLKaCX1QDv3//NxiSOOsqpQY/r5hl+xpYikPwY93Sp7CAB+J5mZJpb/YubxEYLGK5g==
dependencies:
tslib "^2.0.0"