Browse Source

Merge branch 'develop/3.2' of https://github.com/thingsboard/thingsboard into feature/device-provision-3.2-onlyProfileVersion

pull/3518/head
zbeacon 6 years ago
parent
commit
e8220e5102
  1. 2
      application/src/main/resources/thingsboard.yml
  2. 17
      dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
  3. 44
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  4. 5
      ui-ngx/angular.json
  5. 7
      ui-ngx/package.json
  6. 4
      ui-ngx/src/app/core/api/widget-subscription.ts
  7. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  8. 2
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  9. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  10. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  11. 224
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html
  12. 259
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  13. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  15. 27
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  16. 3
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  17. 2
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  18. 25
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  19. 2
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  20. 32
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.scss
  22. 42
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  23. 5
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
  24. 3
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  25. 5
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  26. 1
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  27. 50
      ui-ngx/src/app/shared/components/time/timezone-select.component.html
  28. 221
      ui-ngx/src/app/shared/components/time/timezone-select.component.ts
  29. 31
      ui-ngx/src/app/shared/models/device.models.ts
  30. 2
      ui-ngx/src/app/shared/models/time/time.models.ts
  31. 3
      ui-ngx/src/app/shared/shared.module.ts
  32. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  33. 571
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  34. 2
      ui-ngx/src/tsconfig.app.json
  35. 52
      ui-ngx/yarn.lock

2
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:

17
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_SYMBOLS = 4 * 1024;
@Autowired
public EventDao eventDao;
@ -47,6 +51,7 @@ public class BaseEventService implements EventService {
@Override
public ListenableFuture<Event> 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") && event.getBody() != null && event.getBody().has("data")) {
String dataStr = event.getBody().get("data").asText();
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);
}
}
}
@Override
public Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) {
if (tenantId == null) {

44
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<DashboardInfo> getCustomerDashboards(CustomerId customerId, TimePageLink pageLink) {
public PageData<DashboardInfo> getCustomerDashboards(CustomerId customerId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("customerId", customerId.getId().toString());
addPageLinkToParam(params, pageLink);
@ -1629,22 +1630,42 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return RestJsonConverter.toTimeseries(timeseries);
}
@Deprecated
public List<TsKvEntry> getTimeseries(EntityId entityId, List<String> keys, Long interval, Aggregation agg, TimePageLink pageLink) {
return getTimeseries(entityId, keys, interval, agg, pageLink, true);
}
@Deprecated
public List<TsKvEntry> getTimeseries(EntityId entityId, List<String> 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<TsKvEntry> getTimeseries(EntityId entityId, List<String> keys, Long interval, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) {
Map<String, String> 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<String, List<JsonNode>> 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<Map<String, List<JsonNode>>>() {
@ -1996,23 +2017,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;
}

5
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"
}
}
}

7
ui-ngx/package.json

@ -63,9 +63,10 @@
"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": "^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",
@ -105,11 +106,11 @@
"@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/moment-timezone": "^0.5.30",
"@types/raphael": "^2.3.0",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",

4
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) {

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

@ -108,6 +108,7 @@ 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 { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
@NgModule({
declarations:
@ -198,7 +199,8 @@ import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-p
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
DeviceProfileProvisionConfigurationComponent
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
],
imports: [
CommonModule,
@ -278,7 +280,8 @@ import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-p
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
DeviceProfileProvisionConfigurationComponent
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
],
providers: [
WidgetComponentService,

2
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];

4
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -93,7 +93,9 @@
</section>
</mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}">
<div class="row">{{ 'device-profile.schedule' | translate }}</div>
<tb-alarm-schedule fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule>
</mat-tab>
<mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<mat-form-field class="mat-block row">

1
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) => {

224
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html

@ -0,0 +1,224 @@
<!--
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.
-->
<section [formGroup]="alarmScheduleForm" fxLayout="column">
<mat-form-field class="mat-block" hideRequiredMarker floatLabel="always">
<mat-label> </mat-label>
<mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}">
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType">
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmScheduleForm.get('type').hasError('required')">
{{ 'device-profile.schedule-type-required' | translate }}
</mat-error>
</mat-form-field>
<div *ngIf="alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME">
<tb-timezone-select
[defaultTimezone]="defaultTimezone"
required
formControlName="timezone">
</tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<div fxLayout="row" fxLayoutGap="16px">
<mat-checkbox [formControl]="weeklyRepeatControl(0)">
{{ 'device-profile.schedule-day.monday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox>
</div>
<div fxLayout="row" fxLayoutGap="16px">
<mat-checkbox [formControl]="weeklyRepeatControl(4)">
{{ 'device-profile.schedule-day.friday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
</mat-checkbox>
</div>
</div>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div>
</section>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap.gt-sm="16px" formArrayName="items">
<div fxLayout="column" fxFlex fxFlex.gt-sm="50">
<div fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 0)">
{{ 'device-profile.schedule-day.monday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4">
</mat-form-field>
</div>
</div>
</div>
<div fxLayout="column" fxFlex fxFlex.gt-sm="50">
<div fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 4)">
{{ 'device-profile.schedule-day.friday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7">
</mat-form-field>
</div>
</div>
</div>
</div>
</section>
</div>
</section>

259
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;
}
}

1
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html

@ -34,6 +34,7 @@
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
</mat-form-field>
<mat-divider></mat-divider>
<tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule>
</div>

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

@ -61,7 +61,7 @@
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<table mat-table [dataSource]="alarmsDatasource"
<table mat-table [dataSource]="alarmsDatasource" [trackBy]="trackByRowIndex"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">

27
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 '';
}
@ -804,7 +817,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) {

3
ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts

@ -1002,7 +1002,8 @@ export abstract class TbAnalogueGauge<S extends AnalogueGaugeSettings, O extends
// animations
animation: settings.animation !== false && !this.ctx.isMobile,
animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500,
animationRule: settings.animationRule || 'cycle'
animationRule: settings.animationRule || 'cycle',
animatedValue: true
} as O;
this.prepareGaugeOptions(settings, gaugeData);

2
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -38,7 +38,7 @@
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<table mat-table [dataSource]="entityDatasource"
<table mat-table [dataSource]="entityDatasource" [trackBy]="trackByRowIndex"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>

25
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 '';
}

2
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) => {

32
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();
}

3
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;

42
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,20 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
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)) {
if (this.hideEmptyLines && isDefinedAndNotNull(value[1])) {
rows.push(value);
} else {
rows.push(rowsMap[t]);
rows.push(value);
}
}
return rows;
}
isEmpty(): Observable<boolean> {
return this.rowsSubject.pipe(
map((rows) => !rows.length)

5
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts

@ -96,8 +96,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
}
prepareFormValue(formValue: any): any {
formValue.configuration = {...(this.entity.configuration || {}), ...(formValue.configuration || {})};
return formValue;
const preparedValue = super.prepareFormValue(formValue);
preparedValue.configuration = {...(this.entity.configuration || {}), ...(preparedValue.configuration || {})};
return preparedValue;
}
onPublicLinkCopied($event) {

3
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -29,7 +29,8 @@
<mat-form-field fxFlex class="mat-block">
<mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')">
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')
|| ruleNodeFormGroup.get('name').hasError('pattern')">
{{ 'rulenode.name-required' | translate }}
</mat-error>
</mat-form-field>

5
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);
}
}

1
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -1550,6 +1550,7 @@ export class AddRuleNodeDialogComponent extends DialogComponent<AddRuleNodeDialo
add(): void {
this.submitted = true;
this.ruleNodeDetailsComponent.validate();
if (this.ruleNodeDetailsComponent.ruleNodeFormGroup.valid) {
this.dialogRef.close(this.ruleNode);

50
ui-ngx/src/app/shared/components/time/timezone-select.component.html

@ -0,0 +1,50 @@
<!--
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.
-->
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block">
<mat-label translate>timezone.timezone</mat-label>
<input matInput type="text" placeholder="{{ 'timezone.select-timezone' | translate }}"
#timezoneInput
formControlName="timezone"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="timezoneAutocomplete">
<button *ngIf="selectTimezoneFormGroup.get('timezone').value && !disabled"
type="button" style="margin-right: 1px"
matSuffix mat-button mat-icon-button aria-label="Clear"
(mousedown)="ignoreClosePanel = true"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#timezoneAutocomplete="matAutocomplete"
(closed)="onPanelClosed()"
(optionSelected)="ignoreClosePanel = true"
[displayWith]="displayTimezoneFn">
<mat-option *ngFor="let timezone of filteredTimezones | async" [value]="timezone">
<span [innerHTML]="displayTimezoneFn(timezone) | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredTimezones | async)?.length" [value]="null">
<span>
{{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectTimezoneFormGroup.get('timezone').hasError('required')">
{{ 'timezone.timezone-required' | translate }}
</mat-error>
</mat-form-field>

221
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<Array<TimezoneInfo>>;
searchText = '';
ignoreClosePanel = false;
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
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<Array<TimezoneInfo>> {
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);
}
}

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

@ -257,9 +257,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, string>(
[
[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 {

2
ui-ngx/src/app/shared/models/time/time.models.ts

@ -467,7 +467,6 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
);
export enum TimeUnit {
MILLISECONDS = 'MILLISECONDS',
SECONDS = 'SECONDS',
MINUTES = 'MINUTES',
HOURS = 'HOURS',
@ -476,7 +475,6 @@ export enum TimeUnit {
export const timeUnitTranslationMap = new Map<TimeUnit, string>(
[
[TimeUnit.MILLISECONDS, 'timeunit.milliseconds'],
[TimeUnit.SECONDS, 'timeunit.seconds'],
[TimeUnit.MINUTES, 'timeunit.minutes'],
[TimeUnit.HOURS, 'timeunit.hours'],

3
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,

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

@ -866,7 +866,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"
@ -1752,6 +1770,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",
@ -1831,7 +1855,6 @@
"advanced": "Advanced"
},
"timeunit": {
"milliseconds": "Milliseconds",
"seconds": "Seconds",
"minutes": "Minutes",
"hours": "Hours",

571
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": "접근 권한이 없습니다.!<br> 만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.",
"refresh-token-expired": "세션이 만료되었습니다.",
"refresh-token-failed": "세션을 새로 고칠 수 없습니다."
"access-forbidden-text": "접근 권한이 없습니다!<br> 만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.",
"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": {

2
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

52
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"
@ -1435,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"
@ -5632,7 +5632,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==
@ -6296,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"
@ -6421,10 +6433,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"

Loading…
Cancel
Save