From 3963b56102f9a1d58a23d3ddf0002dffa17ea981 Mon Sep 17 00:00:00 2001 From: Serhii Mikhnytskyi Date: Fri, 18 Sep 2020 12:04:13 +0300 Subject: [PATCH 01/19] dashboard-form name validation added, rule-chain-details name validation pattern and trim added --- .../modules/home/pages/dashboard/dashboard-form.component.ts | 5 ----- .../home/pages/rulechain/rule-node-details.component.html | 3 ++- .../home/pages/rulechain/rule-node-details.component.ts | 5 ++++- .../modules/home/pages/rulechain/rulechain-page.component.ts | 5 +++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index 5751c88341..7f19cfe571 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -95,11 +95,6 @@ export class DashboardFormComponent extends EntityComponent { this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}}); } - prepareFormValue(formValue: any): any { - formValue.configuration = {...(this.entity.configuration || {}), ...(formValue.configuration || {})}; - return formValue; - } - onPublicLinkCopied($event) { this.store.dispatch(new ActionNotificationShow( { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index e87f7158d0..fd4efddf5a 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -29,7 +29,8 @@ rulenode.name - + {{ 'rulenode.name-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index c19fe98348..b5630b52f8 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -72,8 +72,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O } if (this.ruleNode) { if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { + this.ruleNodeFormGroup = this.fb.group({ - name: [this.ruleNode.name, [Validators.required]], + name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], debugMode: [this.ruleNode.debugMode, []], configuration: [this.ruleNode.configuration, [Validators.required]], additionalInfo: this.fb.group( @@ -102,6 +103,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O private updateRuleNode() { const formValue = this.ruleNodeFormGroup.value || {}; + if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) { const targetRuleChainId: string = formValue.targetRuleChainId; if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) { @@ -115,6 +117,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O Object.assign(this.ruleNode, formValue); } } else { + formValue.name = formValue.name.trim(); Object.assign(this.ruleNode, formValue); } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 2c8ecc9419..2e5cdaa866 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1550,6 +1550,11 @@ export class AddRuleNodeDialogComponent extends DialogComponent Date: Mon, 21 Sep 2020 12:13:17 +0300 Subject: [PATCH 02/19] modified parent method prepareFormValue in dashboard-form, deleted unused code in rulechain-page --- .../home/pages/dashboard/dashboard-form.component.ts | 6 ++++++ .../home/pages/rulechain/rulechain-page.component.ts | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index 7f19cfe571..ee72b33453 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -95,6 +95,12 @@ export class DashboardFormComponent extends EntityComponent { this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}}); } + prepareFormValue(formValue: any): any { + const preparedValue = super.prepareFormValue(formValue); + preparedValue.configuration = {...(this.entity.configuration || {}), ...(preparedValue.configuration || {})}; + return preparedValue; + } + onPublicLinkCopied($event) { this.store.dispatch(new ActionNotificationShow( { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 2e5cdaa866..7096045aed 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1551,10 +1551,6 @@ export class AddRuleNodeDialogComponent extends DialogComponent Date: Fri, 18 Sep 2020 17:31:22 +0300 Subject: [PATCH 03/19] set lib attribute "animatedValue" to "true" in analogue-gauge.models.ts --- .../home/components/widget/lib/analogue-gauge.models.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts index 9919173fbc..7038a2f18c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts @@ -1002,7 +1002,8 @@ export abstract class TbAnalogueGauge Date: Sun, 20 Sep 2020 23:45:04 +0300 Subject: [PATCH 04/19] UI Updated dependency --- ui-ngx/package.json | 3 +-- .../import-export/import-export.service.ts | 2 +- ui-ngx/src/tsconfig.app.json | 2 +- ui-ngx/yarn.lock | 23 +++++++------------ 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4a94929bc5..51d044c3bf 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -105,10 +105,9 @@ "@types/jquery": "^3.5.1", "@types/js-beautify": "^1.11.0", "@types/jstree": "^3.3.40", - "@types/jszip": "^3.4.1", "@types/leaflet": "^1.5.17", - "@types/leaflet-markercluster": "^1.0.3", "@types/leaflet-polylinedecorator": "^1.6.0", + "@types/leaflet.markercluster": "^1.4.2", "@types/lodash": "^4.14.159", "@types/raphael": "^2.3.0", "@types/react": "^16.9.46", diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index 210c04d6b2..53c70ce0ee 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -421,7 +421,7 @@ export class ImportExportService { } public exportJSZip(data: object, filename: string) { - const jsZip: JSZip = new JSZip(); + const jsZip = new JSZip(); for (const keyName in data) { if (data.hasOwnProperty(keyName)) { const valueData = data[keyName]; diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json index 2bd6ffd811..1139007835 100644 --- a/ui-ngx/src/tsconfig.app.json +++ b/ui-ngx/src/tsconfig.app.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../out-tsc/app", "types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", - "react", "react-dom", "jstree", "raphael", "canvas-gauges", "leaflet", "leaflet-markercluster"] + "react", "react-dom", "jstree", "raphael", "canvas-gauges", "leaflet", "leaflet.markercluster"] }, "angularCompilerOptions": { "fullTemplateTypeCheck": true diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 2aad17330d..5fdc64ee19 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1397,20 +1397,6 @@ dependencies: "@types/jquery" "*" -"@types/jszip@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.4.1.tgz#e7a4059486e494c949ef750933d009684227846f" - integrity sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A== - dependencies: - jszip "*" - -"@types/leaflet-markercluster@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/leaflet-markercluster/-/leaflet-markercluster-1.0.3.tgz#64151be453f6490e8751500482deb961064e782c" - integrity sha1-ZBUb5FP2SQ6HUVAEgt65YQZOeCw= - dependencies: - "@types/leaflet" "*" - "@types/leaflet-polylinedecorator@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#1572131ffedb3154c6e18e682d2fb700e203af19" @@ -1418,6 +1404,13 @@ dependencies: "@types/leaflet" "*" +"@types/leaflet.markercluster@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.2.tgz#86b8ab7ca2397b48d9ba637757aaf7a6d1cc6f0f" + integrity sha512-QQ//hevAxMH2dlRQdRre7V/1G+TbtuDtZnZF/75TNwVIgklrsQVCIcS/cvLsl7UUryfPJ6xmoYHfFzK5iGVgpg== + dependencies: + "@types/leaflet" "*" + "@types/leaflet@*", "@types/leaflet@^1.5.17": version "1.5.17" resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b" @@ -5632,7 +5625,7 @@ jstree@^3.3.10: dependencies: jquery ">=1.9.1" -jszip@*, jszip@^3.1.3, jszip@^3.5.0: +jszip@^3.1.3, jszip@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== From fbc58d0e2c2a4020cfaa2156d5ba83e6db3084ec Mon Sep 17 00:00:00 2001 From: Kalutka Zhenya Date: Mon, 21 Sep 2020 10:45:29 +0300 Subject: [PATCH 05/19] Fix daterangepicker --- ui-ngx/package.json | 2 +- ui-ngx/yarn.lock | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 51d044c3bf..a74d9eee86 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -65,7 +65,7 @@ "moment": "^2.27.0", "ngx-clipboard": "^13.0.1", "ngx-color-picker": "^10.0.1", - "ngx-daterangepicker-material": "^3.0.4", + "ngx-daterangepicker-material": "^4.0.1", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-hm-carousel": "^2.0.0-rc.1", "ngx-sharebuttons": "^8.0.1", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 5fdc64ee19..ddc2e363b0 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6414,10 +6414,12 @@ ngx-color-picker@^10.0.1: dependencies: tslib "^2.0.0" -ngx-daterangepicker-material@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-3.0.4.tgz#af759e52fd587fcc9bce1fbcfc8cde828df6a471" - integrity sha512-pDg8kdXx/h8es8dpjBI+xbsxQbS0dV3uSPgfsx39t9LIw3Dv50h8T1achT5jUWSzSU7855ywTk+NlNBDTgkeNg== +ngx-daterangepicker-material@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-4.0.1.tgz#788c2e32eb4717629d4a0e60a60bf8d6430d8c13" + integrity sha512-0gY6DGU+dgYdmoAKrIJSB9xnDqBvj91Yis3II/ZJxxMfZVTG4qMMatck6w8FzdU+CYT64ArCq+Uwa6hJRHX6Nw== + dependencies: + tslib "^1.10.0" "ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": version "0.0.0" From 7347230ec42b880c0ec6fa860714894771f5fdea Mon Sep 17 00:00:00 2001 From: kalutkaz <43555187+kalutkaz@users.noreply.github.com> Date: Tue, 6 Oct 2020 18:28:26 +0300 Subject: [PATCH 06/19] Update knob control (#3501) * Epdate knob control * Fix click function * Refactoring * Formatting * Change 'mouseup' --- .../widget/lib/rpc/knob.component.ts | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts index a4c1d1c50b..f5f48c86c1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts @@ -67,6 +67,7 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { title = ''; minValue: number; maxValue: number; + newValue = 0; private startDeg = -1; private currentDeg = 0; @@ -175,16 +176,15 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { const offset = this.knob.offset(); const center = { - y : offset.top + this.knob.height()/2, - x: offset.left + this.knob.width()/2 + y: offset.top + this.knob.height() / 2, + x: offset.left + this.knob.width() / 2 }; - const rad2deg = 180/Math.PI; + const rad2deg = 180 / Math.PI; const t: Touch = ((e.originalEvent as any).touches) ? (e.originalEvent as any).touches[0] : e; - const a = center.y - t.pageY; const b = center.x - t.pageX; - let deg = Math.atan2(a,b)*rad2deg; - if(deg < 0){ + let deg = Math.atan2(a, b) * rad2deg; + if (deg < 0) { deg = 360 + deg; } if (deg > this.maxDeg) { @@ -196,13 +196,17 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { } this.currentDeg = deg; this.lastDeg = deg; - this.knobTopPointerContainer.css('transform','rotate('+(this.currentDeg)+'deg)'); + this.knobTopPointerContainer.css('transform', 'rotate(' + (this.currentDeg) + 'deg)'); this.turn(this.degreeToRatio(this.currentDeg)); this.rotation = this.currentDeg; this.startDeg = -1; + this.rpcUpdateValue(this.newValue); }); + + this.knob.on('mousedown touchstart', (e) => { + this.moving = false; e.preventDefault(); const offset = this.knob.offset(); const center = { @@ -211,7 +215,7 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { }; const rad2deg = 180/Math.PI; - this.knob.on('mousemove.rem touchmove.rem', (ev) => { + $(document).on('mousemove.rem touchmove.rem', (ev) => { this.moving = true; const t: Touch = ((ev.originalEvent as any).touches) ? (ev.originalEvent as any).touches[0] : ev; @@ -262,6 +266,9 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { }); $(document).on('mouseup.rem touchend.rem',() => { + if(this.newValue !== this.rpcValue && this.moving) { + this.rpcUpdateValue(this.newValue); + } this.knob.off('.rem'); $(document).off('.rem'); this.rotation = this.currentDeg; @@ -308,12 +315,12 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { } private turn(ratio: number) { - const value = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals)); - if (this.canvasBar.value !== value) { - this.canvasBar.value = value; + this.newValue = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals)); + if (this.canvasBar.value !== this.newValue) { + this.canvasBar.value = this.newValue; } this.updateColor(this.canvasBar.getValueColor()); - this.onValue(value); + this.onValue(this.newValue); } private resize() { @@ -379,7 +386,6 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy { private onValue(value: number) { this.value = this.formatValue(value); this.checkValueSize(); - this.rpcUpdateValue(value); this.ctx.detectChanges(); } From e35e54a764adad71240cc246e22c5142070f2d3f Mon Sep 17 00:00:00 2001 From: Kalutka Zhenya Date: Mon, 21 Sep 2020 18:12:10 +0300 Subject: [PATCH 07/19] Fix resize switch control --- .../home/components/widget/lib/rpc/switch.component.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss index f878f45320..68bea36407 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss @@ -112,6 +112,9 @@ $error-height: 14px !default; height: 90%; } + .mat-slide-toggle-label{ + height: 100%; + } .mat-slide-toggle-thumb { top: 0; left: 0; From 1eccfcc76aca4997fb2be158ac3dfd7f035d6a86 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 21 Sep 2020 19:38:06 +0300 Subject: [PATCH 08/19] Fixed incorrect display of alarm table columns(AckTime, Cleared time), with no parameters specified --- .../home/components/widget/lib/alarms-table-widget.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index d21fbe9c77..81834ff9c9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -804,7 +804,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, const alarmField = alarmFields[key.name]; if (alarmField) { if (alarmField.time) { - return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss'); + return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : ''; } else if (alarmField.value === alarmFields.severity.value) { return this.translate.instant(alarmSeverityTranslations.get(value)); } else if (alarmField.value === alarmFields.status.value) { From 762c6dae558e0d25ebffd3b54222801a2a977560 Mon Sep 17 00:00:00 2001 From: Serhii Mikhnytskyi Date: Wed, 23 Sep 2020 11:10:40 +0300 Subject: [PATCH 09/19] improved performance of table widgets - removed unused ngZone.run and detectChanges, changed functions trackByRowIndex - now is used index, not id, minor improvements for functions, used after data updating. --- .../lib/alarms-table-widget.component.html | 2 +- .../lib/alarms-table-widget.component.ts | 25 ++++++++--- .../lib/entities-table-widget.component.html | 2 +- .../lib/entities-table-widget.component.ts | 25 ++++++++--- .../lib/timeseries-table-widget.component.ts | 44 ++++++++++--------- 5 files changed, 64 insertions(+), 34 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 7e9a67c485..d269fbbe92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -61,7 +61,7 @@
- diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 81834ff9c9..26055c55b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -247,11 +247,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } public onDataUpdated() { - this.ngZone.run(() => { - this.updateTitle(true); - this.alarmsDatasource.updateAlarms(); - this.ctx.detectChanges(); - }); + this.updateTitle(true); + this.alarmsDatasource.updateAlarms(); } public pageLinkSortDirection(): SortDirection { @@ -565,6 +562,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return column.def; } + public trackByRowIndex(index: number) { + return index; + } + public headerStyle(key: EntityColumn): any { const columnWidth = this.columnWidth[key.def]; return widthStyle(columnWidth); @@ -606,7 +607,19 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } else { content = this.defaultContent(key, contentInfo, value); } - return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + } else { return ''; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index a2c70a3a89..2eb4b372b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -38,7 +38,7 @@
-
{{ column.title }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index ef560dbfc5..dae33f3cfe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -206,11 +206,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } public onDataUpdated() { - this.ngZone.run(() => { - this.updateTitle(true); - this.entityDatasource.dataUpdated(); - this.ctx.detectChanges(); - }); + this.updateTitle(true); + this.entityDatasource.dataUpdated(); } public pageLinkSortDirection(): SortDirection { @@ -488,6 +485,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni return column.def; } + public trackByRowIndex(index: number) { + return index; + } + public headerStyle(key: EntityColumn): any { const columnWidth = this.columnWidth[key.def]; return widthStyle(columnWidth); @@ -529,7 +530,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } else { content = this.defaultContent(key, contentInfo, value); } - return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } + } else { return ''; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 28e6e58c25..e6cf7af811 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -40,7 +40,7 @@ import { } from '@shared/models/widget.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { hashCode, isDefined, isNumber } from '@core/utils'; +import {hashCode, isDefined, isDefinedAndNotNull, isNumber} from '@core/utils'; import cssjs from '@core/css/css'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; @@ -197,11 +197,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } public onDataUpdated() { - this.ngZone.run(() => { - this.sources.forEach((source) => { - source.timeseriesDatasource.dataUpdated(this.data); - }); - this.ctx.detectChanges(); + this.sources.forEach((source) => { + source.timeseriesDatasource.dataUpdated(this.data); }); } @@ -410,7 +407,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const units = contentInfo.units || this.ctx.widgetConfig.units; content = this.ctx.utils.formatValue(value, decimals, units, true); } - return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; + + if (!isDefined(content)) { + return ''; + + } else { + switch (typeof content) { + case 'string': + return this.domSanitizer.bypassSecurityTrustHtml(content); + default: + return content; + } + } } } @@ -515,26 +523,22 @@ class TimeseriesDatasource implements DataSource { row[d + 1] = cellData[1]; }); } + const rows: TimeseriesRow[] = []; - for (const t of Object.keys(rowsMap)) { - if (this.hideEmptyLines) { - let hideLine = true; - for (let c = 0; (c < data.length) && hideLine; c++) { - if (rowsMap[t][c + 1]) { - hideLine = false; - } - } - if (!hideLine) { - rows.push(rowsMap[t]); - } + + for (const value of Object.values(rowsMap)) { + this.hideEmptyLines = true; + + if (this.hideEmptyLines && isDefinedAndNotNull(value[1])) { + rows.push(value); } else { - rows.push(rowsMap[t]); + rows.push(value); } } + return rows; } - isEmpty(): Observable { return this.rowsSubject.pipe( map((rows) => !rows.length) From 0227234a6d50bd2235662fd39845fba5b22475bc Mon Sep 17 00:00:00 2001 From: Serhii Mikhnytskyi Date: Wed, 23 Sep 2020 11:14:44 +0300 Subject: [PATCH 10/19] removed test component property --- .../components/widget/lib/timeseries-table-widget.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index e6cf7af811..ebf5387862 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -527,8 +527,6 @@ class TimeseriesDatasource implements DataSource { const rows: TimeseriesRow[] = []; for (const value of Object.values(rowsMap)) { - this.hideEmptyLines = true; - if (this.hideEmptyLines && isDefinedAndNotNull(value[1])) { rows.push(value); } else { From c783a1c60468522961e89a1e189a477f6c18c176 Mon Sep 17 00:00:00 2001 From: Kalutka Zhenya Date: Wed, 23 Sep 2020 18:01:54 +0300 Subject: [PATCH 11/19] Fix knob control on 2.5.5 --- ui/src/app/widget/lib/rpc/knob.directive.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ui/src/app/widget/lib/rpc/knob.directive.js b/ui/src/app/widget/lib/rpc/knob.directive.js index d89c13de5d..493da04a1b 100644 --- a/ui/src/app/widget/lib/rpc/knob.directive.js +++ b/ui/src/app/widget/lib/rpc/knob.directive.js @@ -50,6 +50,7 @@ function KnobController($element, $scope, $document) { vm.value = 0; vm.error = ''; + vm.newValue = 0; var knob = angular.element('.knob', $element), knobContainer = angular.element('#knob-container', $element), @@ -145,9 +146,11 @@ function KnobController($element, $scope, $document) { turn(degreeToRatio(currentDeg)); rotation = currentDeg; startDeg = -1; + rpcUpdateValue(vm.newValue); }); knob.on('mousedown touchstart', (e) => { + moving = false; e.preventDefault(); var offset = knob.offset(); var center = { @@ -158,7 +161,7 @@ function KnobController($element, $scope, $document) { var a, b, deg, tmp, rad2deg = 180/Math.PI; - knob.on('mousemove.rem touchmove.rem', (e) => { + $document.on('mousemove.rem touchmove.rem', (e) => { moving = true; e = (e.originalEvent.touches) ? e.originalEvent.touches[0] : e; @@ -209,6 +212,9 @@ function KnobController($element, $scope, $document) { }); $document.on('mouseup.rem touchend.rem',() => { + if(moving) { + rpcUpdateValue(vm.newValue); + } knob.off('.rem'); $document.off('.rem'); rotation = currentDeg; @@ -269,12 +275,12 @@ function KnobController($element, $scope, $document) { } function turn(ratio) { - var value = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(vm.ctx.decimals); - if (canvasBar.value != value) { - canvasBar.value = value; + vm.newValue = (vm.minValue + (vm.maxValue - vm.minValue)*ratio).toFixed(vm.ctx.decimals); + if (canvasBar.value != vm.newValue) { + canvasBar.value = vm.newValue; } updateColor(canvasBar.getValueColor()); - onValue(value); + onValue(vm.newValue); } function setValue(value) { @@ -303,7 +309,7 @@ function KnobController($element, $scope, $document) { $scope.$applyAsync(() => { vm.value = formatValue(value); checkValueSize(); - rpcUpdateValue(value); + // rpcUpdateValue(vm.newValue); }); } From a50368a1aea1e1bff450ff932a745ec30c65d752 Mon Sep 17 00:00:00 2001 From: Kalutka Zhenya Date: Wed, 23 Sep 2020 18:07:40 +0300 Subject: [PATCH 12/19] Refactoring --- ui/src/app/widget/lib/rpc/knob.directive.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/app/widget/lib/rpc/knob.directive.js b/ui/src/app/widget/lib/rpc/knob.directive.js index 493da04a1b..e56eb89438 100644 --- a/ui/src/app/widget/lib/rpc/knob.directive.js +++ b/ui/src/app/widget/lib/rpc/knob.directive.js @@ -309,7 +309,6 @@ function KnobController($element, $scope, $document) { $scope.$applyAsync(() => { vm.value = formatValue(value); checkValueSize(); - // rpcUpdateValue(vm.newValue); }); } From 0faef1ad164a728a1b2d12417b5b0bb52bdbc34e Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 1 Oct 2020 15:49:15 +0300 Subject: [PATCH 13/19] rest client improvments --- .../thingsboard/rest/client/RestClient.java | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 89b39d2b89..52023a6450 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -45,7 +45,6 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -56,6 +55,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -73,6 +73,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -890,7 +891,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } - public PageData getCustomerDashboards(CustomerId customerId, TimePageLink pageLink) { + public PageData getCustomerDashboards(CustomerId customerId, PageLink pageLink) { Map params = new HashMap<>(); params.put("customerId", customerId.getId().toString()); addPageLinkToParam(params, pageLink); @@ -1634,17 +1635,35 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink, boolean useStrictDataTypes) { + SortOrder sortOrder = pageLink.getSortOrder(); + return getTimeseries(entityId, keys, interval, agg, sortOrder != null ? sortOrder.getDirection() : null, pageLink.getStartTime(), pageLink.getEndTime(), 100, useStrictDataTypes); + } + + public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); params.put("keys", listToString(keys)); params.put("interval", interval == null ? "0" : interval.toString()); params.put("agg", agg == null ? "NONE" : agg.name()); + params.put("limit", limit != null ? limit.toString() : "100"); + params.put("orderBy", sortOrder != null ? sortOrder.name() : "DESC"); params.put("useStrictDataTypes", Boolean.toString(useStrictDataTypes)); - addPageLinkToParam(params, pageLink); + + StringBuilder urlBuilder = new StringBuilder(baseURL); + urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}"); + + if (startTime != null) { + urlBuilder.append("&startTs={startTs}"); + params.put("startTs", String.valueOf(startTime)); + } + if (endTime != null) { + urlBuilder.append("&endTs={endTs}"); + params.put("endTs", String.valueOf(endTime)); + } Map> timeseries = restTemplate.exchange( - baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&" + getUrlParamsTs(pageLink), + urlBuilder.toString(), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @@ -1996,23 +2015,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } private String getTimeUrlParams(TimePageLink pageLink) { - return this.getUrlParams(pageLink); - } - private String getUrlParams(TimePageLink pageLink) { - return getUrlParams(pageLink, "startTime", "endTime"); - } - - private String getUrlParamsTs(TimePageLink pageLink) { - return getUrlParams(pageLink, "startTs", "endTs"); - } - - private String getUrlParams(TimePageLink pageLink, String startTime, String endTime) { String urlParams = "limit={limit}&ascOrder={ascOrder}"; if (pageLink.getStartTime() != null) { - urlParams += "&" + startTime + "={startTime}"; + urlParams += "&startTime={startTime}"; } if (pageLink.getEndTime() != null) { - urlParams += "&" + endTime + "={endTime}"; + urlParams += "&endTime={endTime}"; } return urlParams; } From 3bcebb06669a9697d82ff13917eae91ddf6bf3c1 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 1 Oct 2020 16:03:11 +0300 Subject: [PATCH 14/19] added annotation @Deprecated to the old methods "getTimeseries" --- .../src/main/java/org/thingsboard/rest/client/RestClient.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 52023a6450..d2ba4d43b4 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -1630,10 +1630,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return RestJsonConverter.toTimeseries(timeseries); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink) { return getTimeseries(entityId, keys, interval, agg, pageLink, true); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, TimePageLink pageLink, boolean useStrictDataTypes) { SortOrder sortOrder = pageLink.getSortOrder(); return getTimeseries(entityId, keys, interval, agg, sortOrder != null ? sortOrder.getDirection() : null, pageLink.getStartTime(), pageLink.getEndTime(), 100, useStrictDataTypes); From 872a6fb45dff91a2b6c24977b83edf11a035e907 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 1 Oct 2020 17:43:48 +0300 Subject: [PATCH 15/19] added checkAndTruncateDebugEvent --- .../server/dao/event/BaseEventService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index 88e701d3bd..b12135b301 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.event; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; @@ -35,6 +37,8 @@ import java.util.Optional; @Slf4j public class BaseEventService implements EventService { + private static final int MAX_DEBUG_EVENT_IN_BYTES = 10 * 1024; + @Autowired public EventDao eventDao; @@ -47,6 +51,7 @@ public class BaseEventService implements EventService { @Override public ListenableFuture saveAsync(Event event) { eventValidator.validate(event, Event::getTenantId); + checkAndTruncateDebugEvent(event); return eventDao.saveAsync(event); } @@ -56,9 +61,21 @@ public class BaseEventService implements EventService { if (StringUtils.isEmpty(event.getUid())) { throw new DataValidationException("Event uid should be specified!."); } + checkAndTruncateDebugEvent(event); return eventDao.saveIfNotExists(event); } + private void checkAndTruncateDebugEvent(Event event) { + if (event.getType().startsWith("DEBUG")) { + String dataStr = event.getBody().get("data").asText(); + int dataSize = dataStr.getBytes(StandardCharsets.UTF_8).length; + if (dataSize > MAX_DEBUG_EVENT_IN_BYTES) { + ((ObjectNode) event.getBody()).put("data", dataStr.substring(0, 1024)); + log.trace("[{}] Event was truncated.", event.getId()); + } + } + } + @Override public Optional findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) { if (tenantId == null) { From 67f8327cdec720ec242f201e9f946bba4cad84d5 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 6 Oct 2020 18:41:48 +0300 Subject: [PATCH 16/19] Minor improvements for event truncation --- .../server/dao/event/BaseEventService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index b12135b301..da1c9a0817 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -37,7 +37,7 @@ import java.util.Optional; @Slf4j public class BaseEventService implements EventService { - private static final int MAX_DEBUG_EVENT_IN_BYTES = 10 * 1024; + private static final int MAX_DEBUG_EVENT_SYMBOLS = 4 * 1024; @Autowired public EventDao eventDao; @@ -66,12 +66,12 @@ public class BaseEventService implements EventService { } private void checkAndTruncateDebugEvent(Event event) { - if (event.getType().startsWith("DEBUG")) { + if (event.getType().startsWith("DEBUG") && event.getBody() != null && event.getBody().has("data")) { String dataStr = event.getBody().get("data").asText(); - int dataSize = dataStr.getBytes(StandardCharsets.UTF_8).length; - if (dataSize > MAX_DEBUG_EVENT_IN_BYTES) { - ((ObjectNode) event.getBody()).put("data", dataStr.substring(0, 1024)); - log.trace("[{}] Event was truncated.", event.getId()); + int length = dataStr.length(); + if (length > MAX_DEBUG_EVENT_SYMBOLS) { + ((ObjectNode) event.getBody()).put("data", dataStr.substring(0, MAX_DEBUG_EVENT_SYMBOLS) + "...[truncated " + (length - MAX_DEBUG_EVENT_SYMBOLS) + " symbols]"); + log.trace("[{}] Event was truncated: {}", event.getId(), dataStr); } } } From 95921a847ae542bdd80dfb0791e9f57b327ab3c3 Mon Sep 17 00:00:00 2001 From: "Seok Hyun, Ga" Date: Fri, 2 Oct 2020 14:54:35 +0900 Subject: [PATCH 17/19] Update locale.constant-ko_KR.json Adding additional translation items. --- .../assets/locale/locale.constant-ko_KR.json | 571 +++++++++--------- 1 file changed, 288 insertions(+), 283 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index e5f09184b9..c255440ba7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json @@ -1,12 +1,14 @@ { "access": { - "unauthorized": "권한 없음.", - "unauthorized-access": "허가되지 않은 접근", + "unauthorized": "승인되지 않음", + "unauthorized-access": "승인되지 않은 접근", "unauthorized-access-text": "이 리소스에 접근하려면 로그인해야 합니다!", "access-forbidden": "접근 금지", - "access-forbidden-text": "접근 권한이 없습니다.!
만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", - "refresh-token-expired": "세션이 만료되었습니다.", - "refresh-token-failed": "세션을 새로 고칠 수 없습니다." + "access-forbidden-text": "접근 권한이 없습니다!
만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", + "refresh-token-expired": "세션이 만료되었습니다", + "refresh-token-failed": "세션을 새로 고칠 수 없습니다.", + "permission-denied": "권한이 없습니다", + "permission-denied-text": "이 작업을 수행할 권한이 없습니다!" }, "action": { "activate": "활설화", @@ -22,11 +24,11 @@ "update": "업데이트", "remove": "제거", "search": "검색", - "clear-search": "Clear search", + "clear-search": "검색 초기화", "assign": "할당", "unassign": "비할당", "share": "Share", - "make-private": "Make private", + "make-private": "비공개로 설정", "apply": "적용", "apply-changes": "변경사항 적용", "edit-mode": "수정 모드", @@ -44,8 +46,8 @@ "undo": "취소", "copy": "복사", "paste": "붙여넣기", - "copy-reference": "Copy reference", - "paste-reference": "Paste reference", + "copy-reference": "참조 복사", + "paste-reference": "참조 붙여넣기", "import": "가져오기", "export": "내보내기", "share-via": "Share via {{provider}}" @@ -79,26 +81,26 @@ "smtp-port": "SMTP 포트", "smtp-port-required": "SMTP 포트를 입력해야 합니다.", "smtp-port-invalid": "올바른 SMTP 포트가 아닙니다.", - "timeout-msec": "제한시간 (msec)", - "timeout-required": "제한시간을 입력해야 합니다.", - "timeout-invalid": "올바른 제한시간이 아닙니다.", + "timeout-msec": "제한시간 (ms)", + "timeout-required": "제한시이 입력되지 않았습니다.", + "timeout-invalid": "제한시간이 올바르게 입력되지 않았습니다.", "enable-tls": "TLS 사용", "tls-version" : "TLS 버전", "send-test-mail": "테스트 메일 보내기" }, "alarm": { - "alarm": "Alarm", - "alarms": "Alarms", - "select-alarm": "Select alarm", - "no-alarms-matching": "No alarms matching '{{entity}}' were found.", - "alarm-required": "Alarm is required", - "alarm-status": "Alarm status", + "alarm": "알람", + "alarms": "알람", + "select-alarm": "알람 선택", + "no-alarms-matching": "'{{entity}}'에 대한 알람이 존재하지 않습니다.", + "alarm-required": "알람이 필요합니다", + "alarm-status": "알람 상태", "search-status": { "ANY": "Any", - "ACTIVE": "Active", + "ACTIVE": "활성", "CLEARED": "Cleared", - "ACK": "Acknowledged", - "UNACK": "Unacknowledged" + "ACK": "수용", + "UNACK": "불수용" }, "display-status": { "ACTIVE_UNACK": "Active Unacknowledged", @@ -107,28 +109,28 @@ "CLEARED_ACK": "Cleared Acknowledged" }, "no-alarms-prompt": "No alarms found", - "created-time": "Created time", - "type": "Type", - "severity": "Severity", - "originator": "Originator", - "originator-type": "Originator type", - "details": "Details", - "status": "Status", + "created-time": "생성된 시간", + "type": "종류", + "severity": "심각도", + "originator": "창시자", + "originator-type": "창시자 종류", + "details": "자세히", + "status": "상태", "alarm-details": "Alarm details", - "start-time": "Start time", - "end-time": "End time", + "start-time": "시작 시각", + "end-time": "마지막 시각", "ack-time": "Acknowledged time", "clear-time": "Cleared time", - "severity-critical": "Critical", - "severity-major": "Major", - "severity-minor": "Minor", - "severity-warning": "Warning", - "severity-indeterminate": "Indeterminate", - "acknowledge": "Acknowledge", - "clear": "Clear", - "search": "Search alarms", - "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected", - "no-data": "No data to display", + "severity-critical": "심각한", + "severity-major": "주요한", + "severity-minor": "작은", + "severity-warning": "경고", + "severity-indeterminate": "중간", + "acknowledge": "수용", + "clear": "지우기", + "search": "알람 검색", + "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } 선택됨", + "no-data": "표시할 데이터가 없습니다", "polling-interval": "Alarms polling interval (sec)", "polling-interval-required": "Alarms polling interval is required.", "min-polling-interval-message": "At least 1 sec polling interval is allowed.", @@ -178,46 +180,46 @@ "any-relation": "any" }, "asset": { - "asset": "Asset", - "assets": "Assets", - "management": "Asset management", - "view-assets": "View Assets", - "add": "Add Asset", - "assign-to-customer": "Assign to customer", - "assign-asset-to-customer": "Assign Asset(s) To Customer", - "assign-asset-to-customer-text": "Please select the assets to assign to the customer", - "no-assets-text": "No assets found", - "assign-to-customer-text": "Please select the customer to assign the asset(s)", - "public": "Public", - "assignedToCustomer": "Assigned to customer", - "make-public": "Make asset public", - "make-private": "Make asset private", - "unassign-from-customer": "Unassign from customer", - "delete": "Delete asset", - "asset-public": "Asset is public", - "asset-type": "Asset type", - "asset-type-required": "Asset type is required.", - "select-asset-type": "Select asset type", - "enter-asset-type": "Enter asset type", - "any-asset": "Any asset", - "no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", - "asset-type-list-empty": "No asset types selected.", - "asset-types": "Asset types", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "type": "Type", - "type-required": "Type is required.", - "details": "Details", - "events": "Events", - "add-asset-text": "Add new asset", - "asset-details": "Asset details", - "assign-assets": "Assign assets", - "assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", - "delete-assets": "Delete assets", - "unassign-assets": "Unassign assets", + "asset": "자산", + "assets": "자산", + "management": "자산 관리", + "view-assets": "자산 보기", + "add": "자산 추가", + "assign-to-customer": "고객에게 자산 지정", + "assign-asset-to-customer": "자산을 고객에게 지정", + "assign-asset-to-customer-text": "고객에게 지정할 자산을 선택하세요", + "no-assets-text": "아무 자산도 없습니다", + "assign-to-customer-text": "자산에 지정될 고객을 선택하세요", + "public": "공개", + "assignedToCustomer": "지정된 고객", + "make-public": "자산을 공개로 설정", + "make-private": "자산을 비공개로 설정", + "unassign-from-customer": "고객 지정 해제", + "delete": "자산 삭제", + "asset-public": "공개된 자산", + "asset-type": "자산 종류", + "asset-type-required": "자산 종류를 선택하세요.", + "select-asset-type": "자산 종류 선택", + "enter-asset-type": "자산 종류 입력", + "any-asset": "모든 자산", + "no-asset-types-matching": "'{{entitySubtype}}'과 일치하는 자산 종류가 없습니다.", + "asset-type-list-empty": "아무 자산 종류도 선택되지 않았습니다.", + "asset-types": "자산 종류", + "name": "이름", + "name-required": "이름을 입력하세요.", + "description": "설명", + "type": "종류", + "type-required": "종류를 입력하세요.", + "details": "자세히", + "events": "이벤트", + "add-asset-text": "새로운 자산 추가", + "asset-details": "자산 자세히", + "assign-assets": "자산 지정", + "assign-assets-text": "자산 { count, plural, 1 {1 asset} other {# assets} }을 고객에게 지정", + "delete-assets": "자산 삭제", + "unassign-assets": "자산 지정 해제", "unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", - "assign-new-asset": "Assign new asset", + "assign-new-asset": "새로운 자산 지정", "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?", "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.", "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?", @@ -248,10 +250,11 @@ "scope-server": "서버 속성", "scope-shared": "공유 속성", "add": "속성 추가", - "key": "Key", - "key-required": "속성 key를 입력하세요.", + "key": "키", + "last-update-time": "마지막 수정된 시간", + "key-required": "속성 키를 입력하세요.", "value": "Value", - "value-required": "속성 value를 입력하세요.", + "value-required": "속성 값을 입력하세요.", "delete-attributes-title": "{ count, plural, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", "delete-attributes": "속성 삭제", @@ -264,38 +267,40 @@ "add-widget-to-dashboard": "대시보드에 위젯 추가", "selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨", "selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" + "no-attributes-text": "아무 속성도 찾을 수 없습니다", + "no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다." }, "audit-log": { - "audit": "Audit", - "audit-logs": "Audit Logs", - "timestamp": "Timestamp", - "entity-type": "Entity Type", - "entity-name": "Entity Name", - "user": "User", - "type": "Type", - "status": "Status", - "details": "Details", - "type-added": "Added", - "type-deleted": "Deleted", - "type-updated": "Updated", - "type-attributes-updated": "Attributes updated", - "type-attributes-deleted": "Attributes deleted", + "audit": "감사", + "audit-logs": "감사 로그", + "timestamp": "타임스탬프", + "entity-type": "기체 종류", + "entity-name": "개체 이름", + "user": "사용자", + "type": "종류", + "status": "상태", + "details": "자세히", + "type-added": "추가됨", + "type-deleted": "삭제됨", + "type-updated": "수정됨", + "type-attributes-updated": "속성이 수정되었습니다", + "type-attributes-deleted": "속성이 삭제되었습니다", "type-rpc-call": "RPC call", - "type-credentials-updated": "Credentials updated", - "type-assigned-to-customer": "Assigned to Customer", - "type-unassigned-from-customer": "Unassigned from Customer", - "type-activated": "Activated", - "type-suspended": "Suspended", - "type-credentials-read": "Credentials read", - "type-attributes-read": "Attributes read", - "status-success": "Success", - "status-failure": "Failure", - "audit-log-details": "Audit log details", - "no-audit-logs-prompt": "No logs found", - "action-data": "Action data", - "failure-details": "Failure details", - "search": "Search audit logs", - "clear-search": "Clear search" + "type-credentials-updated": "자격 증명이 갱신되었습니다", + "type-assigned-to-customer": "고객에게 지정", + "type-unassigned-from-customer": "지정된 고객 해제", + "type-activated": "활성", + "type-suspended": "일시 중지", + "type-credentials-read": "자격 증명 읽기", + "type-attributes-read": "속성 읽기", + "status-success": "성공", + "status-failure": "실패", + "audit-log-details": "감사 로그 세부 사항", + "no-audit-logs-prompt": "아무 로그도 없습니다.", + "action-data": "액션 데이터", + "failure-details": "실패 세부 사항", + "search": "감사 로그 검색", + "clear-search": "검색 초기화" }, "confirm-on-exit": { "message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?", @@ -323,8 +328,8 @@ }, "content-type": { "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" + "text": "텍스트", + "binary": "바이너리 (Base64)" }, "customer": { "customers": "커스터머", @@ -337,10 +342,10 @@ "manage-customer-users": "커스터머 사용자 관리", "manage-customer-devices": "커스터머 디바이스 관리", "manage-customer-dashboards": "커스터머 대시보드 관리", - "manage-public-devices": "Manage public devices", - "manage-public-dashboards": "Manage public dashboards", - "manage-customer-assets": "Manage customer assets", - "manage-public-assets": "Manage public assets", + "manage-public-devices": "공개된 디바이스 관리", + "manage-public-dashboards": "공개된 대시보드 관리", + "manage-customer-assets": "고객 자산 관리", + "manage-public-assets": "공개된 자산 관리", "add-customer-text": "커스터머 추가", "no-customers-text": "커스터머가 없습니다.", "customer-details": "커스터머 상세정보", @@ -355,16 +360,16 @@ "title": "타이틀", "title-required": "타이틀을 입력하세요.", "description": "설명", - "details": "Details", - "events": "Events", - "copyId": "Copy customer Id", - "idCopiedMessage": "Customer Id has been copied to clipboard", - "select-customer": "Select customer", - "no-customers-matching": "No customers matching '{{entity}}' were found.", - "customer-required": "Customer is required", - "select-default-customer": "Select default customer", - "default-customer": "Default customer", - "default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" + "details": "자세히", + "events": "이벤트", + "copyId": "고객 ID 복사", + "idCopiedMessage": "고객 ID가 클립 보드에 복사되었습니다.", + "select-customer": "선택된 고객", + "no-customers-matching": "'{{entity}}'에 해당하는 고객을 찾을 수 없습니다.", + "customer-required": "고객을 입력하세요.", + "select-default-customer": "기본 고객 선택", + "default-customer": "기본 고객", + "default-customer-required": "테넌트 수준에서 대시보드를 디버그 하기 위해서는 기본 고객이 필요합니다." }, "datetime": { "date-from": "시작 날짜", @@ -522,8 +527,8 @@ "assign-to-customer-text": "디바이스를 할당할 커스터머를 선택하세요.", "device-details": "디바이스 상세정보", "add-device-text": "디바이스 추가", - "credentials": "크리덴셜", - "manage-credentials": "크리덴셜 관리", + "credentials": "자격 증명", + "manage-credentials": "자격 증명 관리", "delete": "디바이스 삭제", "assign-devices": "디바이스 할당", "assign-devices-text": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커서터머에 할당", @@ -575,8 +580,8 @@ "unknown-error": "알 수 없는 오류" }, "entity": { - "entity": "Entity", - "entities": "Entities", + "entity": "개체", + "entities": "개체", "aliases": "Entity aliases", "entity-alias": "Entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias", @@ -588,70 +593,70 @@ "alias-required": "Entity alias is required.", "remove-alias": "Remove entity alias", "add-alias": "Add entity alias", - "entity-list": "Entity list", - "entity-type": "Entity type", - "entity-types": "Entity types", - "entity-type-list": "Entity type list", - "any-entity": "Any entity", - "enter-entity-type": "Enter entity type", + "entity-list": "개체 목록", + "entity-type": "개체 종류", + "entity-types": "개체 종류", + "entity-type-list": "개체 종류 목록", + "any-entity": "모든 개체", + "enter-entity-type": "개체 종류 입력", "no-entities-matching": "No entities matching '{{entity}}' were found.", "no-entity-types-matching": "No entity types matching '{{entityType}}' were found.", - "name-starts-with": "Name starts with", + "name-starts-with": "다음으로 시작하는 이름", "use-entity-name-filter": "Use filter", - "entity-list-empty": "No entities selected.", - "entity-type-list-empty": "No entity types selected.", + "entity-list-empty": "아무 개체도 선택되지 않았습니다.", + "entity-type-list-empty": "개체 종류가 선택되지 않았습니다.", "entity-name-filter-required": "Entity name filter is required.", "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", - "all-subtypes": "All", - "select-entities": "Select entities", + "all-subtypes": "모두", + "select-entities": "선택된 개체", "no-aliases-found": "No aliases found.", "no-alias-matching": "'{{alias}}' not found.", - "create-new-alias": "Create a new one!", - "key": "Key", - "key-name": "Key name", - "no-keys-found": "No keys found.", - "no-key-matching": "'{{key}}' not found.", - "create-new-key": "Create a new one!", - "type": "Type", - "type-required": "Entity type is required.", - "type-device": "Device", - "type-devices": "Devices", + "create-new-alias": "생성 완료!", + "key": "키", + "key-name": "키 이름", + "no-keys-found": "아무 키도 찾을 수 없습니다..", + "no-key-matching": "'{{key}}'를 찾을 수 없습니다.", + "create-new-key": "생성 완료!", + "type": "종류", + "type-required": "개체의 종류를 입력하세요.", + "type-device": "장치", + "type-devices": "장치", "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", "device-name-starts-with": "Devices whose names start with '{{prefix}}'", - "type-asset": "Asset", - "type-assets": "Assets", + "type-asset": "자산", + "type-assets": "자산", "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", - "type-rule": "Rule", - "type-rules": "Rules", + "type-rule": "규칙", + "type-rules": "규칙", "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", "rule-name-starts-with": "Rules whose names start with '{{prefix}}'", - "type-plugin": "Plugin", - "type-plugins": "Plugins", + "type-plugin": "플러그인", + "type-plugins": "플러그인", "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", - "type-tenant": "Tenant", - "type-tenants": "Tenants", + "type-tenant": "테넌트", + "type-tenants": "테넌트", "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", - "type-customer": "Customer", - "type-customers": "Customers", + "type-customer": "고객", + "type-customers": "고객", "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", "customer-name-starts-with": "Customers whose names start with '{{prefix}}'", - "type-user": "User", - "type-users": "Users", + "type-user": "사용자", + "type-users": "사용자", "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", "user-name-starts-with": "Users whose names start with '{{prefix}}'", - "type-dashboard": "Dashboard", - "type-dashboards": "Dashboards", + "type-dashboard": "대시보드", + "type-dashboards": "대시보드", "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", - "type-alarm": "Alarm", - "type-alarms": "Alarms", + "type-alarm": "알람", + "type-alarms": "알람", "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", - "type-rulechain": "Rule chain", - "type-rulechains": "Rule chains", + "type-rulechain": "규칙 사슬", + "type-rulechains": "규칙 사슬", "list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'", "type-current-customer": "Current Customer", @@ -667,23 +672,23 @@ "type-error": "에러", "type-lc-event": "주기적 이벤트", "type-stats": "통계", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", + "type-debug-rule-node": "디버그", + "type-debug-rule-chain": "디버그", "no-events-prompt": "이벤트 없음", "error": "에러", "alarm": "알람", "event-time": "이벤트 발생 시간", "server": "서버", "body": "Body", - "method": "Method", - "type": "Type", - "entity": "Entity", - "message-id": "Message Id", - "message-type": "Message Type", - "data-type": "Data Type", - "relation-type": "Relation Type", - "metadata": "Metadata", - "data": "Data", + "method": "방법", + "type": "종류", + "entity": "개체", + "message-id": "메시지 ID", + "message-type": "메시지 종류", + "data-type": "데이터 종류", + "relation-type": "관계 종류", + "metadata": "메타데이터", + "data": "데이터", "event": "이벤트", "status": "상태", "success": "성공", @@ -692,11 +697,11 @@ "errors-occurred": "오류가 발생했습니다" }, "extension": { - "extensions": "Extensions", + "extensions": "확장", "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", - "type": "Type", - "key": "Key", - "value": "Value", + "type": "종류", + "key": "키", + "value": "값", "id": "Id", "extension-id": "Extension id", "extension-type": "Extension type", @@ -992,14 +997,14 @@ "invalid-additional-info": "Unable to parse additional info json." }, "rulechain": { - "rulechain": "Rule chain", - "rulechains": "Rule chains", + "rulechain": "규칙 사슬", + "rulechains": "규칙 사슬", "root": "Root", "delete": "Delete rule chain", - "name": "Name", - "name-required": "Name is required.", - "description": "Description", - "add": "Add Rule Chain", + "name": "이름", + "name-required": "이름을 입력하세요.", + "description": "설명", + "add": "규칙 사슬 추가", "set-root": "Make rule chain root", "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", @@ -1008,14 +1013,14 @@ "delete-rulechains-title": "Are you sure you want to delete { count, plural, 1 {1 rule chain} other {# rule chains} }?", "delete-rulechains-action-title": "Delete { count, plural, 1 {1 rule chain} other {# rule chains} }", "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", - "add-rulechain-text": "Add new rule chain", - "no-rulechains-text": "No rule chains found", - "rulechain-details": "Rule chain details", - "details": "Details", - "events": "Events", - "system": "System", - "import": "Import rule chain", - "export": "Export rule chain", + "add-rulechain-text": "새로운 규칙 사슬 추가", + "no-rulechains-text": "아무 규칙 사슬도 없습니다.", + "rulechain-details": "규칙 사슬 상세 정보", + "details": "자세히", + "events": "이벤트", + "system": "시스템", + "import": "규칙 사슬 불러오기", + "export": "규칙 사슬 내보내기", "export-failed-error": "Unable to export rule chain: {{error}}", "create-new-rulechain": "Create new rule chain", "rulechain-file": "Rule chain file", @@ -1029,70 +1034,70 @@ "debug-mode": "Debug mode" }, "rulenode": { - "details": "Details", - "events": "Events", - "search": "Search nodes", - "open-node-library": "Open node library", - "add": "Add rule node", - "name": "Name", - "name-required": "Name is required.", - "type": "Type", - "description": "Description", - "delete": "Delete rule node", - "select-all-objects": "Select all nodes and connections", - "deselect-all-objects": "Deselect all nodes and connections", - "delete-selected-objects": "Delete selected nodes and connections", - "delete-selected": "Delete selected", - "select-all": "Select all", - "copy-selected": "Copy selected", - "deselect-all": "Deselect all", - "rulenode-details": "Rule node details", - "debug-mode": "Debug mode", - "configuration": "Configuration", - "link": "Link", - "link-details": "Rule node link details", - "add-link": "Add link", - "link-label": "Link label", - "link-label-required": "Link label is required.", - "custom-link-label": "Custom link label", - "custom-link-label-required": "Custom link label is required.", - "type-filter": "Filter", + "details": "자세히", + "events": "이벤트", + "search": "노드 검색", + "open-node-library": "노드 라이브러리 열기", + "add": "규칙 노드 추가", + "name": "이름", + "name-required": "이름을 입력하세요.", + "type": "종류", + "description": "설명", + "delete": "규칙 노드 삭제", + "select-all-objects": "모든 노드와 연결을 선택", + "deselect-all-objects": "모든 노드와 연결을 선택 해제", + "delete-selected-objects": "선택된 노드와 연결을 삭제", + "delete-selected": "선택 삭제", + "select-all": "모두 선택", + "copy-selected": "선택 복사", + "deselect-all": "선택 해제", + "rulenode-details": "규칙 노드 상세 정보", + "debug-mode": "디버그 모드", + "configuration": "설정", + "link": "링크", + "link-details": "규칙 노드 링크 상세 정보", + "add-link": "링크 추가", + "link-label": "링크 라벨", + "link-label-required": "링크 라벨을 입력하세요.", + "custom-link-label": "링크 라벨 사용자 정의", + "custom-link-label-required": "링크 라벨 사용자 정의를 입력하세요.", + "type-filter": "필터", "type-filter-details": "Filter incoming messages with configured conditions", "type-enrichment": "Enrichment", "type-enrichment-details": "Add additional information into Message Metadata", "type-transformation": "Transformation", "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Action", + "type-action": "", "type-action-details": "Perform special action", - "type-external": "External", + "type-external": "외부", "type-external-details": "Interacts with external system", "type-rule-chain": "Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-input": "Input", + "type-input": "입력", "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", "ui-resources-load-error": "Failed to load configuration ui resources.", "invalid-target-rulechain": "Unable to resolve target rule chain!", "test-script-function": "Test script function", - "message": "Message", - "message-type": "Message type", - "message-type-required": "Message type is required", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Help" + "message": "메시지", + "message-type": "메시지 종류", + "message-type-required": "메시지 종류를 입력하세요.", + "metadata": "메타데이터", + "metadata-required": "메타데이터 엔트리를 입력하세요.", + "output": "출력", + "test": "테스트", + "help": "도움말" }, "tenant": { "tenants": "테넌트", "management": "테넌트 관리", "add": "테넌트 추가", - "admins": "Admins", + "admins": "관리자", "manage-tenant-admins": "테넌트 관리자 관리", "delete": "테넌트 삭제", "add-tenant-text": "테넌트 추가", "no-tenants-text": "테넌트가 없습니다.", - "tenant-details": "테넌트 상세정보", + "tenant-details": "테넌트 상세 정보", "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", "delete-tenant-text": "테넌트와 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", "delete-tenants-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} }를 삭제하시겠습니까?", @@ -1101,23 +1106,23 @@ "title": "타이틀", "title-required": "타이틀을 입력하세요.", "description": "설명", - "details": "Details", - "events": "Events", - "copyId": "Copy tenant Id", - "idCopiedMessage": "Tenant Id has been copied to clipboard", - "select-tenant": "Select tenant", + "details": "자세히", + "events": "이벤트", + "copyId": "테넌트 ID 복사", + "idCopiedMessage": "테넌트 ID를 클립보드로 복사", + "select-tenant": "테넌트 선택", "no-tenants-matching": "No tenants matching '{{entity}}' were found.", - "tenant-required": "Tenant is required" + "tenant-required": "테넌트가 필요합니다." }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", "days-interval": "{ days, plural, 1 {1 day} other {# days} }", - "days": "Days", - "hours": "Hours", - "minutes": "Minutes", - "seconds": "Seconds", + "days": "일", + "hours": "시간", + "minutes": "분", + "seconds": "초", "advanced": "고급" }, "timewindow": { @@ -1125,13 +1130,13 @@ "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", - "realtime": "Realtime", - "history": "History", - "last-prefix": "last", - "period": "from {{ startTime }} to {{ endTime }}", + "realtime": "실시간", + "history": "기록", + "last-prefix": "과거", + "period": "{{ startTime }}부터 {{ endTime }}까지", "edit": "타임윈도우 편집", "date-range": "날짜 범위", - "last": "Last", + "last": "과거", "time-period": "기간" }, "user": { @@ -1146,7 +1151,7 @@ "delete": "사용자 삭제", "add-user-text": "새로운 사용자 추가", "no-users-text": "사용자가 없습니다.", - "user-details": "사용자 상세정보", + "user-details": "사용자 상세 정보", "delete-user-title": "'{{userEmail}}' 사용자를 삭제하시겠습니까?", "delete-user-text": "사용자와 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-users-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} }을 삭제하시겠니까?", @@ -1242,10 +1247,10 @@ "update-dashboard-state": "Update current dashboard state", "open-dashboard": "Navigate to other dashboard", "custom": "Custom action", - "target-dashboard-state": "Target dashboard state", - "target-dashboard-state-required": "Target dashboard state is required", - "set-entity-from-widget": "Set entity from widget", - "target-dashboard": "Target dashboard", + "target-dashboard-state": "대상 대시보드 상태", + "target-dashboard-state-required": "대상 대시보드 상태가 필요합니다.", + "set-entity-from-widget": "위젯으로 부터 객체 설정", + "target-dashboard": "대상 대시보드", "open-right-layout": "Open right dashboard layout (mobile view)" }, "widgets-bundle": { @@ -1253,13 +1258,13 @@ "widgets-bundles": "위젯 번들", "add": "위젯 번들 추가", "delete": "위젯 번들 삭제", - "title": "타이틀", - "title-required": "타이틀을 입력하세요.", + "title": "제목", + "title-required": "제목을 입력하세요.", "add-widgets-bundle-text": "위젯 번들 추가", "no-widgets-bundles-text": "위젯 번들이 없습니다.", "empty": "위젯 번들이 비어있습니다.", "details": "상세", - "widgets-bundle-details": "위젯 번들 상세정보", + "widgets-bundle-details": "위젯 번들 상세 정보", "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' 위젯 번들을 삭제하시겠습니까?", "delete-widgets-bundle-text": "위젯 번들과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-widgets-bundles-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} }를 삭제하시겠습니까?", @@ -1279,15 +1284,15 @@ "data": "데이터", "settings": "설정", "advanced": "고급", - "title": "타이틀", + "title": "제목", "general-settings": "일반 설정", - "display-title": "타이틀 표시", + "display-title": "제목 표시", "drop-shadow": "그림자", "enable-fullscreen": "전체화면 사용 ", "background-color": "배경 색", "text-color": "글자 색", "padding": "패딩", - "title-style": "타이틀 스타일", + "title-style": "제목 스타일", "mobile-mode-settings": "모바일 모드 설정", "order": "순서", "height": "높이", @@ -1333,18 +1338,18 @@ "Oct": "10월", "Nov": "11월", "Dec": "12월", - "January": "일월", - "February": "이월", - "March": "행진", - "April": "4 월", - "June": "유월", - "July": "칠월", - "August": "팔월", - "September": "구월", - "October": "십월", - "November": "십일월", - "December": "12 월", - "Custom Date Range": "맞춤 기간", + "January": "1월", + "February": "2월", + "March": "3월", + "April": "4월", + "June": "6월", + "July": "7월", + "August": "8월", + "September": "9월", + "October": "10월", + "November": "11월", + "December": "12월", + "Custom Date Range": "임의 기간 범위", "Date Range Template": "기간 템플릿", "Today": "오늘", "Yesterday": "어제", @@ -1359,22 +1364,22 @@ "Hour": "시간", "Day": "일", "Week": "주", - "2 weeks": "이주", + "2 weeks": "2 주", "Month": "달", "3 months": "3 개월", "6 months": "6 개월", "Custom interval": "사용자 지정 간격", "Interval": "간격", "Step size": "단계 크기", - "Ok": "Ok" + "Ok": "확인" } } }, "icon": { - "icon": "Icon", - "select-icon": "Select icon", + "icon": "아이콘", + "select-icon": "선택된 아이콘", "material-icons": "Material icons", - "show-all": "Show all icons" + "show-all": "모든 아이콘 보기" }, "custom": { "widget-action": { From aa82a478cbc704a37a5449b844e752868ca9cd82 Mon Sep 17 00:00:00 2001 From: Chantsova Ekaterina Date: Tue, 6 Oct 2020 10:36:21 +0300 Subject: [PATCH 18/19] Fix wrong number of digits after floating point in legend --- ui-ngx/src/app/core/api/widget-subscription.ts | 4 ++-- .../src/app/modules/home/components/widget/lib/flot-widget.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 9d046bbf83..c574fd426a 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -47,7 +47,7 @@ import { import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; import { CancelAnimationFrame } from '@core/services/raf.service'; import { EntityType } from '@shared/models/entity-type.models'; -import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils'; +import { createLabelFromDatasource, deepClone, isDefined, isDefinedAndNotNull, isEqual } from '@core/utils'; import { EntityId } from '@app/shared/models/id/entity-id'; import * as moment_ from 'moment'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; @@ -1332,7 +1332,7 @@ export class WidgetSubscription implements IWidgetSubscription { private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) { const dataKey = this.legendData.keys.find(key => key.dataIndex === dataIndex).dataKey; - const decimals = isDefined(dataKey.decimals) ? dataKey.decimals : this.decimals; + const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : this.decimals; const units = dataKey.units && dataKey.units.length ? dataKey.units : this.units; const legendKeyData = this.legendData.data[dataIndex]; if (this.legendConfig.showMin) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index d334d5c99a..03f6d1b975 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -380,7 +380,7 @@ export class TbFlot { const yaxesMap: {[units: string]: TbFlotAxisOptions} = {}; const predefinedThresholds: TbFlotThresholdMarking[] = []; const thresholdsDatasources: Datasource[] = []; - if (this.settings.customLegendEnabled) { + if (this.settings.customLegendEnabled && this.settings.dataKeysListForLabels?.length) { this.labelPatternsSourcesData = []; const labelPatternsDatasources: Datasource[] = []; this.settings.dataKeysListForLabels.forEach((item) => { From fd602dec7f2fd754fde946b0727adebb3c2a093e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 6 Oct 2020 19:22:40 +0300 Subject: [PATCH 19/19] UI: Added device profile schedule setting for alarm setting --- .../src/main/resources/thingsboard.yml | 2 +- ui-ngx/angular.json | 5 +- ui-ngx/package.json | 2 + .../home/components/home-components.module.ts | 7 +- .../profile/alarm/alarm-rule.component.html | 4 +- .../profile/alarm/alarm-rule.component.ts | 1 + .../alarm/alarm-schedule.component.html | 224 +++++++++++++++ .../profile/alarm/alarm-schedule.component.ts | 259 ++++++++++++++++++ .../alarm/create-alarm-rules.component.html | 1 + .../time/timezone-select.component.html | 50 ++++ .../time/timezone-select.component.ts | 221 +++++++++++++++ ui-ngx/src/app/shared/models/device.models.ts | 31 +++ .../src/app/shared/models/time/time.models.ts | 2 - ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 27 +- ui-ngx/yarn.lock | 19 ++ 16 files changed, 848 insertions(+), 10 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts create mode 100644 ui-ngx/src/app/shared/components/time/timezone-select.component.html create mode 100644 ui-ngx/src/app/shared/components/time/timezone-select.component.ts diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 8e691ba48e..35808b1f85 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -478,7 +478,7 @@ spring: database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}" datasource: driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}" - url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" + url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard_32}" username: "${SPRING_DATASOURCE_USERNAME:postgres}" password: "${SPRING_DATASOURCE_PASSWORD:postgres}" hikari: diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 72886b1a8b..3cad30304a 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -137,7 +137,8 @@ "react-is", "hoist-non-react-statics", "classnames", - "raf" + "raf", + "moment-timezone" ] }, "configurations": { @@ -248,4 +249,4 @@ "cli": { "packageManager": "yarn" } -} \ No newline at end of file +} diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 45ad97bf81..619b2bed8b 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -63,6 +63,7 @@ "material-design-icons": "^3.0.1", "messageformat": "^2.3.0", "moment": "^2.27.0", + "moment-timezone": "^0.5.31", "ngx-clipboard": "^13.0.1", "ngx-color-picker": "^10.0.1", "ngx-daterangepicker-material": "^4.0.1", @@ -109,6 +110,7 @@ "@types/leaflet-polylinedecorator": "^1.6.0", "@types/leaflet.markercluster": "^1.4.2", "@types/lodash": "^4.14.159", + "@types/moment-timezone": "^0.5.30", "@types/raphael": "^2.3.0", "@types/react": "^16.9.46", "@types/react-dom": "^16.9.8", diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index d583369016..8a715c7406 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k import { FilterTextComponent } from './filter/filter-text.component'; import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; +import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; @NgModule({ declarations: @@ -196,7 +197,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp DeviceProfileComponent, DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, - RuleChainAutocompleteComponent + RuleChainAutocompleteComponent, + AlarmScheduleComponent ], imports: [ CommonModule, @@ -275,7 +277,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp DeviceProfileComponent, DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, - RuleChainAutocompleteComponent + RuleChainAutocompleteComponent, + AlarmScheduleComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index cab1e6aced..28c286f33c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -93,7 +93,9 @@ -
{{ 'device-profile.schedule' | translate }}
+ +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index a96b27a76e..7b63660362 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -95,6 +95,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] }) }, Validators.required), + schedule: [null], alarmDetails: [null] }); this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html new file mode 100644 index 0000000000..cad6e5cf03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html @@ -0,0 +1,224 @@ + +
+ + + + + {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} + + + + {{ 'device-profile.schedule-type-required' | translate }} + + +
+ + +
+
device-profile.schedule-days
+
+
+ + {{ 'device-profile.schedule-day.monday' | translate }} + + + {{ 'device-profile.schedule-day.tuesday' | translate }} + + + {{ 'device-profile.schedule-day.wednesday' | translate }} + + + {{ 'device-profile.schedule-day.thursday' | translate }} + +
+
+ + {{ 'device-profile.schedule-day.friday' | translate }} + + + {{ 'device-profile.schedule-day.saturday' | translate }} + + + {{ 'device-profile.schedule-day.sunday' | translate }} + +
+
+
device-profile.schedule-time
+
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
device-profile.schedule-days
+
+
+
+ + {{ 'device-profile.schedule-day.monday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.tuesday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.wednesday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.thursday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+ + {{ 'device-profile.schedule-day.friday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.saturday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+ + {{ 'device-profile.schedule-day.sunday' | translate }} + +
+ + device-profile.schedule-time-from + + + + + + device-profile.schedule-time-to + + + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts new file mode 100644 index 0000000000..8cf9dc8d30 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -0,0 +1,259 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import * as _moment from 'moment-timezone'; +import { MatCheckboxChange } from '@angular/material/checkbox'; + +@Component({ + selector: 'tb-alarm-schedule', + templateUrl: './alarm-schedule.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmScheduleComponent), + multi: true + }] +}) +export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { + @Input() + disabled: boolean; + + alarmScheduleForm: FormGroup; + + defaultTimezone = _moment.tz.guess(); + + alarmScheduleTypes = Object.keys(AlarmScheduleType); + alarmScheduleType = AlarmScheduleType; + alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; + + private modelValue: AlarmSchedule; + + private defaultItems = Array.from({length: 7}, (value, i) => ({ + enabled: true, + dayOfWeek: i + })); + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder) { + } + + ngOnInit(): void { + this.alarmScheduleForm = this.fb.group({ + type: [AlarmScheduleType.ANY_TIME, Validators.required], + timezone: [null, Validators.required], + daysOfWeek: this.fb.array(new Array(7).fill(false)), + startsOn: [0, Validators.required], + endsOn: [0, Validators.required], + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i))) + }); + this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { + this.alarmScheduleForm.reset({type, items: this.defaultItems}, {emitEvent: false}); + this.updateValidators(type, true); + this.alarmScheduleForm.updateValueAndValidity(); + }); + this.alarmScheduleForm.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmScheduleForm.disable({emitEvent: false}); + } else { + this.alarmScheduleForm.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmSchedule): void { + this.modelValue = value; + if (!isDefinedAndNotNull(this.modelValue)) { + this.modelValue = { + type: AlarmScheduleType.ANY_TIME + }; + } + switch (this.modelValue.type) { + case AlarmScheduleType.SPECIFIC_TIME: + let daysOfWeek = new Array(7).fill(false); + if (isDefined(this.modelValue.daysOfWeek)) { + daysOfWeek = daysOfWeek.map((item, index) => this.modelValue.daysOfWeek.indexOf(index + 1) > -1); + } + this.alarmScheduleForm.patchValue({ + type: this.modelValue.type, + timezone: this.modelValue.timezone, + daysOfWeek, + startsOn: this.timestampToTime(this.modelValue.startsOn), + endsOn: this.timestampToTime(this.modelValue.endsOn) + }, {emitEvent: false}); + break; + case AlarmScheduleType.CUSTOM: + if (this.modelValue.items) { + const alarmDays = []; + this.modelValue.items + .sort((a, b) => a.dayOfWeek - b.dayOfWeek) + .forEach((item, index) => { + if (item.enabled) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent: false}); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent: false}); + } + alarmDays.push({ + enabled: item.enabled, + startsOn: this.timestampToTime(item.startsOn), + endsOn: this.timestampToTime(item.endsOn) + }); + }); + this.alarmScheduleForm.patchValue({ + type: this.modelValue.type, + timezone: this.modelValue.timezone, + items: alarmDays + }, {emitEvent: false}); + } + break; + default: + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); + } + this.updateValidators(this.modelValue.type); + } + + validate(control: FormGroup): ValidationErrors | null { + return this.alarmScheduleForm.valid ? null : { + alarmScheduler: { + valid: false + } + }; + } + + weeklyRepeatControl(index: number): FormControl { + return (this.alarmScheduleForm.get('daysOfWeek') as FormArray).at(index) as FormControl; + } + + private updateValidators(type: AlarmScheduleType, changedType = false){ + switch (type){ + case AlarmScheduleType.ANY_TIME: + this.alarmScheduleForm.get('timezone').disable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('items').disable({emitEvent: false}); + break; + case AlarmScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').enable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('items').disable({emitEvent: false}); + break; + case AlarmScheduleType.CUSTOM: + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); + if (changedType) { + this.alarmScheduleForm.get('items').enable({emitEvent: false}); + } + break; + } + } + + private updateModel() { + const value = this.alarmScheduleForm.value; + if (this.modelValue) { + if (isDefined(value.daysOfWeek)) { + value.daysOfWeek = value.daysOfWeek + .map((day: boolean, index: number) => day ? index + 1 : null) + .filter(day => !!day); + } + if (isDefined(value.startsOn) && value.startsOn !== 0) { + value.startsOn = this.timeToTimestamp(value.startsOn); + } + if (isDefined(value.endsOn) && value.endsOn !== 0) { + value.endsOn = this.timeToTimestamp(value.endsOn); + } + if (isDefined(value.items)){ + value.items = this.alarmScheduleForm.getRawValue().items; + value.items = value.items.map((item) => { + return { ...item, startsOn: this.timeToTimestamp(item.startsOn), endsOn: this.timeToTimestamp(item.endsOn)}; + }); + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + private timeToTimestamp(date: Date | number): number { + if (typeof date === 'number' || date === null) { + return 0; + } + return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); + } + + private timestampToTime(time = 0): Date { + return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); + } + + private defaultItemsScheduler(index): FormGroup { + return this.fb.group({ + enabled: [true], + dayOfWeek: [index], + startsOn: [0, Validators.required], + endsOn: [0, Validators.required] + }); + } + + changeCustomScheduler($event: MatCheckboxChange, index: number) { + const value = $event.checked; + if (value) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable(); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable(); + } + } + + private get itemsSchedulerForm(): FormArray { + return this.alarmScheduleForm.get('items') as FormArray; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html index d83fc44807..6886e19152 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -34,6 +34,7 @@ {{ 'device-profile.alarm-severity-required' | translate }}
+ diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.html b/ui-ngx/src/app/shared/components/time/timezone-select.component.html new file mode 100644 index 0000000000..b5e063ac72 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.html @@ -0,0 +1,50 @@ + + + timezone.timezone + + + + + + + + + {{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} + + + + + {{ 'timezone.timezone-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.ts b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts new file mode 100644 index 0000000000..7dc89df7da --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.ts @@ -0,0 +1,221 @@ +/// +/// Copyright © 2016-2020 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, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import * as _moment from 'moment-timezone'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; + +interface TimezoneInfo { + id: string; + name: string; + offset: string; + nOffset: number; +} + +@Component({ + selector: 'tb-timezone-select', + templateUrl: './timezone-select.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TimezoneSelectComponent), + multi: true + }] +}) +export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectTimezoneFormGroup: FormGroup; + + modelValue: string | null; + + defaultTimezoneId: string = null; + + defaultTimezoneInfo: TimezoneInfo = null; + + timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => { + const tz = _moment.tz(zoneName); + return { + id: zoneName, + name: zoneName.replace(/_/g, ' '), + offset: `UTC${tz.format('Z')}`, + nOffset: tz.utcOffset() + } + }); + + @Input() + set defaultTimezone(timezone: string) { + if (this.defaultTimezoneId !== timezone) { + this.defaultTimezoneId = timezone; + if (this.defaultTimezoneId) { + this.defaultTimezoneInfo = + this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId); + } else { + this.defaultTimezoneInfo = null; + } + } + } + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('timezoneInput', {static: true, read: MatAutocompleteTrigger}) timezoneInputTrigger: MatAutocompleteTrigger; + + filteredTimezones: Observable>; + + searchText = ''; + + ignoreClosePanel = false; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private ngZone: NgZone, + private fb: FormBuilder) { + this.selectTimezoneFormGroup = this.fb.group({ + timezone: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredTimezones = this.selectTimezoneFormGroup.get('timezone').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchTimezones(name) ), + share() + ); + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectTimezoneFormGroup.disable({emitEvent: false}); + } else { + this.selectTimezoneFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: string | null): void { + this.searchText = ''; + let foundTimezone: TimezoneInfo = null; + if (value !== null) { + foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value); + } + if (foundTimezone !== null) { + this.modelValue = value; + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); + } else { + if (this.defaultTimezoneInfo) { + this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false}); + setTimeout(() => { + this.updateView(this.defaultTimezoneInfo.id); + }, 0); + } else { + this.modelValue = null; + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); + } + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectTimezoneFormGroup.get('timezone').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + onPanelClosed() { + if (this.ignoreClosePanel) { + this.ignoreClosePanel = false; + } else { + if (!this.modelValue && this.defaultTimezoneInfo) { + this.ngZone.run(() => { + this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true}); + }); + } + } + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayTimezoneFn(timezone?: TimezoneInfo): string | undefined { + return timezone ? `${timezone.name} (${timezone.offset})` : undefined; + } + + fetchTimezones(searchText?: string): Observable> { + this.searchText = searchText; + let result = this.timezones; + if (searchText && searchText.length) { + result = this.timezones.filter((timezoneInfo) => + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())); + } + return of(result); + } + + clear() { + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.timezoneInputTrigger.openPanel(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 091d15a1bb..addb726ed5 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -236,9 +236,40 @@ export interface AlarmCondition { spec?: AlarmConditionSpec; } +export enum AlarmScheduleType { + ANY_TIME = 'ANY_TIME', + SPECIFIC_TIME = 'SPECIFIC_TIME', + CUSTOM = 'CUSTOM' +} + +export const AlarmScheduleTypeTranslationMap = new Map( + [ + [AlarmScheduleType.ANY_TIME, 'device-profile.schedule-any-time'], + [AlarmScheduleType.SPECIFIC_TIME, 'device-profile.schedule-specific-time'], + [AlarmScheduleType.CUSTOM, 'device-profile.schedule-custom'] + ] +); + +export interface AlarmSchedule{ + type: AlarmScheduleType; + timezone?: string; + daysOfWeek?: number[]; + startsOn?: number; + endsOn?: number; + items?: CustomTimeSchedulerItem[]; +} + +export interface CustomTimeSchedulerItem{ + enabled: boolean; + dayOfWeek: number; + startsOn: number; + endsOn: number; +} + export interface AlarmRule { condition: AlarmCondition; alarmDetails?: string; + schedule?: AlarmSchedule; } export interface DeviceProfileAlarm { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 04b27a6865..b81952dbde 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -467,7 +467,6 @@ export const defaultTimeIntervals = new Array( ); export enum TimeUnit { - MILLISECONDS = 'MILLISECONDS', SECONDS = 'SECONDS', MINUTES = 'MINUTES', HOURS = 'HOURS', @@ -476,7 +475,6 @@ export enum TimeUnit { export const timeUnitTranslationMap = new Map( [ - [TimeUnit.MILLISECONDS, 'timeunit.milliseconds'], [TimeUnit.SECONDS, 'timeunit.seconds'], [TimeUnit.MINUTES, 'timeunit.minutes'], [TimeUnit.HOURS, 'timeunit.hours'], diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 38551578c4..d071869808 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -134,6 +134,7 @@ import { HistorySelectorComponent } from './components/time/history-selector/his import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { ContactComponent } from '@shared/components/contact.component'; +import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @NgModule({ providers: [ @@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component'; DashboardSelectPanelComponent, DatetimePeriodComponent, DatetimeComponent, + TimezoneSelectComponent, ValueInputComponent, DashboardAutocompleteComponent, EntitySubTypeAutocompleteComponent, @@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component'; DashboardSelectComponent, DatetimePeriodComponent, DatetimeComponent, + TimezoneSelectComponent, DashboardAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 8d07a597bd..0fd0e1f7a2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -856,7 +856,25 @@ "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-required": "Count of events is required.", - "schedule": "Schedule" + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": "Schedule", + "schedule-any-time": "Active all the time", + "schedule-specific-time": "Active at a specific time", + "schedule-custom": "Custom", + "schedule-day": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "schedule-days": "Days", + "schedule-time": "Time", + "schedule-time-from": "From", + "schedule-time-to": "To" }, "dialog": { "close": "Close dialog" @@ -1742,6 +1760,12 @@ "help": "Help", "reset-debug-mode": "Reset debug mode in all nodes" }, + "timezone": { + "timezone": "Timezone", + "select-timezone": "Select timezone", + "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", + "timezone-required": "Timezone is required." + }, "queue": { "select_name": "Select queue name", "name": "Queue Name", @@ -1821,7 +1845,6 @@ "advanced": "Advanced" }, "timeunit": { - "milliseconds": "Milliseconds", "seconds": "Seconds", "minutes": "Minutes", "hours": "Hours", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index ddc2e363b0..9f5f48efc4 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1428,6 +1428,13 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/moment-timezone@^0.5.30": + version "0.5.30" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7" + integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg== + dependencies: + moment-timezone "*" + "@types/mousetrap@^1.6.0": version "1.6.3" resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" @@ -6289,6 +6296,18 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment-timezone@*, moment-timezone@^0.5.31: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425" + integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA== + moment@^2.27.0: version "2.27.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"