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. 3
      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}" database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
datasource: datasource:
driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}" 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}" username: "${SPRING_DATASOURCE_USERNAME:postgres}"
password: "${SPRING_DATASOURCE_PASSWORD:postgres}" password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
hikari: hikari:

17
dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.event; package org.thingsboard.server.dao.event;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; 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.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -35,6 +37,8 @@ import java.util.Optional;
@Slf4j @Slf4j
public class BaseEventService implements EventService { public class BaseEventService implements EventService {
private static final int MAX_DEBUG_EVENT_SYMBOLS = 4 * 1024;
@Autowired @Autowired
public EventDao eventDao; public EventDao eventDao;
@ -47,6 +51,7 @@ public class BaseEventService implements EventService {
@Override @Override
public ListenableFuture<Event> saveAsync(Event event) { public ListenableFuture<Event> saveAsync(Event event) {
eventValidator.validate(event, Event::getTenantId); eventValidator.validate(event, Event::getTenantId);
checkAndTruncateDebugEvent(event);
return eventDao.saveAsync(event); return eventDao.saveAsync(event);
} }
@ -56,9 +61,21 @@ public class BaseEventService implements EventService {
if (StringUtils.isEmpty(event.getUid())) { if (StringUtils.isEmpty(event.getUid())) {
throw new DataValidationException("Event uid should be specified!."); throw new DataValidationException("Event uid should be specified!.");
} }
checkAndTruncateDebugEvent(event);
return eventDao.saveIfNotExists(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 @Override
public Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) { public Optional<Event> findEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) {
if (tenantId == null) { 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.UpdateMessage;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; 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.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; 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.audit.AuditLog;
import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; 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.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId; 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.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; 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.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
@ -890,7 +891,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}, params).getBody(); }, params).getBody();
} }
public PageData<DashboardInfo> getCustomerDashboards(CustomerId customerId, TimePageLink pageLink) { public PageData<DashboardInfo> getCustomerDashboards(CustomerId customerId, PageLink pageLink) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
params.put("customerId", customerId.getId().toString()); params.put("customerId", customerId.getId().toString());
addPageLinkToParam(params, pageLink); addPageLinkToParam(params, pageLink);
@ -1629,22 +1630,42 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return RestJsonConverter.toTimeseries(timeseries); return RestJsonConverter.toTimeseries(timeseries);
} }
@Deprecated
public List<TsKvEntry> getTimeseries(EntityId entityId, List<String> keys, Long interval, Aggregation agg, TimePageLink pageLink) { public List<TsKvEntry> getTimeseries(EntityId entityId, List<String> keys, Long interval, Aggregation agg, TimePageLink pageLink) {
return getTimeseries(entityId, keys, interval, agg, pageLink, true); 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) { 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<>(); Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name()); params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString()); params.put("entityId", entityId.getId().toString());
params.put("keys", listToString(keys)); params.put("keys", listToString(keys));
params.put("interval", interval == null ? "0" : interval.toString()); params.put("interval", interval == null ? "0" : interval.toString());
params.put("agg", agg == null ? "NONE" : agg.name()); 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)); 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( 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, HttpMethod.GET,
HttpEntity.EMPTY, HttpEntity.EMPTY,
new ParameterizedTypeReference<Map<String, List<JsonNode>>>() { new ParameterizedTypeReference<Map<String, List<JsonNode>>>() {
@ -1996,23 +2017,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
} }
private String getTimeUrlParams(TimePageLink pageLink) { 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}"; String urlParams = "limit={limit}&ascOrder={ascOrder}";
if (pageLink.getStartTime() != null) { if (pageLink.getStartTime() != null) {
urlParams += "&" + startTime + "={startTime}"; urlParams += "&startTime={startTime}";
} }
if (pageLink.getEndTime() != null) { if (pageLink.getEndTime() != null) {
urlParams += "&" + endTime + "={endTime}"; urlParams += "&endTime={endTime}";
} }
return urlParams; return urlParams;
} }

3
ui-ngx/angular.json

@ -137,7 +137,8 @@
"react-is", "react-is",
"hoist-non-react-statics", "hoist-non-react-statics",
"classnames", "classnames",
"raf" "raf",
"moment-timezone"
] ]
}, },
"configurations": { "configurations": {

7
ui-ngx/package.json

@ -63,9 +63,10 @@
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"messageformat": "^2.3.0", "messageformat": "^2.3.0",
"moment": "^2.27.0", "moment": "^2.27.0",
"moment-timezone": "^0.5.31",
"ngx-clipboard": "^13.0.1", "ngx-clipboard": "^13.0.1",
"ngx-color-picker": "^10.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-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
"ngx-hm-carousel": "^2.0.0-rc.1", "ngx-hm-carousel": "^2.0.0-rc.1",
"ngx-sharebuttons": "^8.0.1", "ngx-sharebuttons": "^8.0.1",
@ -105,11 +106,11 @@
"@types/jquery": "^3.5.1", "@types/jquery": "^3.5.1",
"@types/js-beautify": "^1.11.0", "@types/js-beautify": "^1.11.0",
"@types/jstree": "^3.3.40", "@types/jstree": "^3.3.40",
"@types/jszip": "^3.4.1",
"@types/leaflet": "^1.5.17", "@types/leaflet": "^1.5.17",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-polylinedecorator": "^1.6.0", "@types/leaflet-polylinedecorator": "^1.6.0",
"@types/leaflet.markercluster": "^1.4.2",
"@types/lodash": "^4.14.159", "@types/lodash": "^4.14.159",
"@types/moment-timezone": "^0.5.30",
"@types/raphael": "^2.3.0", "@types/raphael": "^2.3.0",
"@types/react": "^16.9.46", "@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8", "@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 { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service'; import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models'; 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 { EntityId } from '@app/shared/models/id/entity-id';
import * as moment_ from 'moment'; import * as moment_ from 'moment';
import { emptyPageData, PageData } from '@shared/models/page/page-data'; 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) { private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) {
const dataKey = this.legendData.keys.find(key => key.dataIndex === dataIndex).dataKey; 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 units = dataKey.units && dataKey.units.length ? dataKey.units : this.units;
const legendKeyData = this.legendData.data[dataIndex]; const legendKeyData = this.legendData.data[dataIndex];
if (this.legendConfig.showMin) { 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 { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component"; import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -198,7 +199,8 @@ import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-p
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent, RuleChainAutocompleteComponent,
DeviceProfileProvisionConfigurationComponent DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -278,7 +280,8 @@ import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-p
DeviceProfileDialogComponent, DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent, AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent, RuleChainAutocompleteComponent,
DeviceProfileProvisionConfigurationComponent DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
], ],
providers: [ providers: [
WidgetComponentService, 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) { public exportJSZip(data: object, filename: string) {
const jsZip: JSZip = new JSZip(); const jsZip = new JSZip();
for (const keyName in data) { for (const keyName in data) {
if (data.hasOwnProperty(keyName)) { if (data.hasOwnProperty(keyName)) {
const valueData = data[keyName]; const valueData = data[keyName];

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

@ -93,7 +93,9 @@
</section> </section>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}"> <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>
<mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}"> <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<mat-form-field class="mat-block row"> <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]*')]] count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
}) })
}, Validators.required), }, Validators.required),
schedule: [null],
alarmDetails: [null] alarmDetails: [null]
}); });
this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { 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 }} {{ 'device-profile.alarm-severity-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-divider></mat-divider>
<tb-alarm-rule formControlName="alarmRule" required fxFlex> <tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule> </tb-alarm-rule>
</div> </div>

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

@ -61,7 +61,7 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<div fxFlex class="table-container"> <div fxFlex class="table-container">
<table mat-table [dataSource]="alarmsDatasource" <table mat-table [dataSource]="alarmsDatasource" [trackBy]="trackByRowIndex"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky> <ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;"> <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() { public onDataUpdated() {
this.ngZone.run(() => { this.updateTitle(true);
this.updateTitle(true); this.alarmsDatasource.updateAlarms();
this.alarmsDatasource.updateAlarms();
this.ctx.detectChanges();
});
} }
public pageLinkSortDirection(): SortDirection { public pageLinkSortDirection(): SortDirection {
@ -565,6 +562,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
return column.def; return column.def;
} }
public trackByRowIndex(index: number) {
return index;
}
public headerStyle(key: EntityColumn): any { public headerStyle(key: EntityColumn): any {
const columnWidth = this.columnWidth[key.def]; const columnWidth = this.columnWidth[key.def];
return widthStyle(columnWidth); return widthStyle(columnWidth);
@ -606,7 +607,19 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} else { } else {
content = this.defaultContent(key, contentInfo, value); 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 { } else {
return ''; return '';
} }
@ -804,7 +817,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
const alarmField = alarmFields[key.name]; const alarmField = alarmFields[key.name];
if (alarmField) { if (alarmField) {
if (alarmField.time) { 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) { } else if (alarmField.value === alarmFields.severity.value) {
return this.translate.instant(alarmSeverityTranslations.get(value)); return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField.value === alarmFields.status.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 // animations
animation: settings.animation !== false && !this.ctx.isMobile, animation: settings.animation !== false && !this.ctx.isMobile,
animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500, animationDuration: (isDefined(settings.animationDuration) && settings.animationDuration !== null) ? settings.animationDuration : 500,
animationRule: settings.animationRule || 'cycle' animationRule: settings.animationRule || 'cycle',
animatedValue: true
} as O; } as O;
this.prepareGaugeOptions(settings, gaugeData); this.prepareGaugeOptions(settings, gaugeData);

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

@ -38,7 +38,7 @@
</div> </div>
</mat-toolbar> </mat-toolbar>
<div fxFlex class="table-container"> <div fxFlex class="table-container">
<table mat-table [dataSource]="entityDatasource" <table mat-table [dataSource]="entityDatasource" [trackBy]="trackByRowIndex"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;"> <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> <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() { public onDataUpdated() {
this.ngZone.run(() => { this.updateTitle(true);
this.updateTitle(true); this.entityDatasource.dataUpdated();
this.entityDatasource.dataUpdated();
this.ctx.detectChanges();
});
} }
public pageLinkSortDirection(): SortDirection { public pageLinkSortDirection(): SortDirection {
@ -488,6 +485,10 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
return column.def; return column.def;
} }
public trackByRowIndex(index: number) {
return index;
}
public headerStyle(key: EntityColumn): any { public headerStyle(key: EntityColumn): any {
const columnWidth = this.columnWidth[key.def]; const columnWidth = this.columnWidth[key.def];
return widthStyle(columnWidth); return widthStyle(columnWidth);
@ -529,7 +530,19 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} else { } else {
content = this.defaultContent(key, contentInfo, value); 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 { } else {
return ''; 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 yaxesMap: {[units: string]: TbFlotAxisOptions} = {};
const predefinedThresholds: TbFlotThresholdMarking[] = []; const predefinedThresholds: TbFlotThresholdMarking[] = [];
const thresholdsDatasources: Datasource[] = []; const thresholdsDatasources: Datasource[] = [];
if (this.settings.customLegendEnabled) { if (this.settings.customLegendEnabled && this.settings.dataKeysListForLabels?.length) {
this.labelPatternsSourcesData = []; this.labelPatternsSourcesData = [];
const labelPatternsDatasources: Datasource[] = []; const labelPatternsDatasources: Datasource[] = [];
this.settings.dataKeysListForLabels.forEach((item) => { 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 = ''; title = '';
minValue: number; minValue: number;
maxValue: number; maxValue: number;
newValue = 0;
private startDeg = -1; private startDeg = -1;
private currentDeg = 0; private currentDeg = 0;
@ -175,16 +176,15 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy {
const offset = this.knob.offset(); const offset = this.knob.offset();
const center = { const center = {
y : offset.top + this.knob.height()/2, y: offset.top + this.knob.height() / 2,
x: offset.left + this.knob.width()/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 t: Touch = ((e.originalEvent as any).touches) ? (e.originalEvent as any).touches[0] : e;
const a = center.y - t.pageY; const a = center.y - t.pageY;
const b = center.x - t.pageX; const b = center.x - t.pageX;
let deg = Math.atan2(a,b)*rad2deg; let deg = Math.atan2(a, b) * rad2deg;
if(deg < 0){ if (deg < 0) {
deg = 360 + deg; deg = 360 + deg;
} }
if (deg > this.maxDeg) { if (deg > this.maxDeg) {
@ -196,13 +196,17 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy {
} }
this.currentDeg = deg; this.currentDeg = deg;
this.lastDeg = 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.turn(this.degreeToRatio(this.currentDeg));
this.rotation = this.currentDeg; this.rotation = this.currentDeg;
this.startDeg = -1; this.startDeg = -1;
this.rpcUpdateValue(this.newValue);
}); });
this.knob.on('mousedown touchstart', (e) => { this.knob.on('mousedown touchstart', (e) => {
this.moving = false;
e.preventDefault(); e.preventDefault();
const offset = this.knob.offset(); const offset = this.knob.offset();
const center = { const center = {
@ -211,7 +215,7 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy {
}; };
const rad2deg = 180/Math.PI; const rad2deg = 180/Math.PI;
this.knob.on('mousemove.rem touchmove.rem', (ev) => { $(document).on('mousemove.rem touchmove.rem', (ev) => {
this.moving = true; this.moving = true;
const t: Touch = ((ev.originalEvent as any).touches) ? (ev.originalEvent as any).touches[0] : ev; 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',() => { $(document).on('mouseup.rem touchend.rem',() => {
if(this.newValue !== this.rpcValue && this.moving) {
this.rpcUpdateValue(this.newValue);
}
this.knob.off('.rem'); this.knob.off('.rem');
$(document).off('.rem'); $(document).off('.rem');
this.rotation = this.currentDeg; this.rotation = this.currentDeg;
@ -308,12 +315,12 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy {
} }
private turn(ratio: number) { private turn(ratio: number) {
const value = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals)); this.newValue = Number((this.minValue + (this.maxValue - this.minValue)*ratio).toFixed(this.ctx.decimals));
if (this.canvasBar.value !== value) { if (this.canvasBar.value !== this.newValue) {
this.canvasBar.value = value; this.canvasBar.value = this.newValue;
} }
this.updateColor(this.canvasBar.getValueColor()); this.updateColor(this.canvasBar.getValueColor());
this.onValue(value); this.onValue(this.newValue);
} }
private resize() { private resize() {
@ -379,7 +386,6 @@ export class KnobComponent extends PageComponent implements OnInit, OnDestroy {
private onValue(value: number) { private onValue(value: number) {
this.value = this.formatValue(value); this.value = this.formatValue(value);
this.checkValueSize(); this.checkValueSize();
this.rpcUpdateValue(value);
this.ctx.detectChanges(); 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%; height: 90%;
} }
.mat-slide-toggle-label{
height: 100%;
}
.mat-slide-toggle-thumb { .mat-slide-toggle-thumb {
top: 0; top: 0;
left: 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'; } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; 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 cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -197,11 +197,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
public onDataUpdated() { public onDataUpdated() {
this.ngZone.run(() => { this.sources.forEach((source) => {
this.sources.forEach((source) => { source.timeseriesDatasource.dataUpdated(this.data);
source.timeseriesDatasource.dataUpdated(this.data);
});
this.ctx.detectChanges();
}); });
} }
@ -410,7 +407,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const units = contentInfo.units || this.ctx.widgetConfig.units; const units = contentInfo.units || this.ctx.widgetConfig.units;
content = this.ctx.utils.formatValue(value, decimals, units, true); 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]; row[d + 1] = cellData[1];
}); });
} }
const rows: TimeseriesRow[] = []; const rows: TimeseriesRow[] = [];
for (const t of Object.keys(rowsMap)) {
if (this.hideEmptyLines) { for (const value of Object.values(rowsMap)) {
let hideLine = true; if (this.hideEmptyLines && isDefinedAndNotNull(value[1])) {
for (let c = 0; (c < data.length) && hideLine; c++) { rows.push(value);
if (rowsMap[t][c + 1]) {
hideLine = false;
}
}
if (!hideLine) {
rows.push(rowsMap[t]);
}
} else { } else {
rows.push(rowsMap[t]); rows.push(value);
} }
} }
return rows; return rows;
} }
isEmpty(): Observable<boolean> { isEmpty(): Observable<boolean> {
return this.rowsSubject.pipe( return this.rowsSubject.pipe(
map((rows) => !rows.length) 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 { prepareFormValue(formValue: any): any {
formValue.configuration = {...(this.entity.configuration || {}), ...(formValue.configuration || {})}; const preparedValue = super.prepareFormValue(formValue);
return formValue; preparedValue.configuration = {...(this.entity.configuration || {}), ...(preparedValue.configuration || {})};
return preparedValue;
} }
onPublicLinkCopied($event) { 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-form-field fxFlex class="mat-block">
<mat-label translate>rulenode.name</mat-label> <mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required> <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 }} {{ 'rulenode.name-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </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) {
if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) {
this.ruleNodeFormGroup = this.fb.group({ 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, []], debugMode: [this.ruleNode.debugMode, []],
configuration: [this.ruleNode.configuration, [Validators.required]], configuration: [this.ruleNode.configuration, [Validators.required]],
additionalInfo: this.fb.group( additionalInfo: this.fb.group(
@ -102,6 +103,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
private updateRuleNode() { private updateRuleNode() {
const formValue = this.ruleNodeFormGroup.value || {}; const formValue = this.ruleNodeFormGroup.value || {};
if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) { if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) {
const targetRuleChainId: string = formValue.targetRuleChainId; const targetRuleChainId: string = formValue.targetRuleChainId;
if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) { if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) {
@ -115,6 +117,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
Object.assign(this.ruleNode, formValue); Object.assign(this.ruleNode, formValue);
} }
} else { } else {
formValue.name = formValue.name.trim();
Object.assign(this.ruleNode, formValue); 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 { add(): void {
this.submitted = true; this.submitted = true;
this.ruleNodeDetailsComponent.validate(); this.ruleNodeDetailsComponent.validate();
if (this.ruleNodeDetailsComponent.ruleNodeFormGroup.valid) { if (this.ruleNodeDetailsComponent.ruleNodeFormGroup.valid) {
this.dialogRef.close(this.ruleNode); 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; 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 { export interface AlarmRule {
condition: AlarmCondition; condition: AlarmCondition;
alarmDetails?: string; alarmDetails?: string;
schedule?: AlarmSchedule;
} }
export interface DeviceProfileAlarm { 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 { export enum TimeUnit {
MILLISECONDS = 'MILLISECONDS',
SECONDS = 'SECONDS', SECONDS = 'SECONDS',
MINUTES = 'MINUTES', MINUTES = 'MINUTES',
HOURS = 'HOURS', HOURS = 'HOURS',
@ -476,7 +475,6 @@ export enum TimeUnit {
export const timeUnitTranslationMap = new Map<TimeUnit, string>( export const timeUnitTranslationMap = new Map<TimeUnit, string>(
[ [
[TimeUnit.MILLISECONDS, 'timeunit.milliseconds'],
[TimeUnit.SECONDS, 'timeunit.seconds'], [TimeUnit.SECONDS, 'timeunit.seconds'],
[TimeUnit.MINUTES, 'timeunit.minutes'], [TimeUnit.MINUTES, 'timeunit.minutes'],
[TimeUnit.HOURS, 'timeunit.hours'], [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 { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
import { ContactComponent } from '@shared/components/contact.component'; import { ContactComponent } from '@shared/components/contact.component';
import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component';
DashboardSelectPanelComponent, DashboardSelectPanelComponent,
DatetimePeriodComponent, DatetimePeriodComponent,
DatetimeComponent, DatetimeComponent,
TimezoneSelectComponent,
ValueInputComponent, ValueInputComponent,
DashboardAutocompleteComponent, DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent, EntitySubTypeAutocompleteComponent,
@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component';
DashboardSelectComponent, DashboardSelectComponent,
DatetimePeriodComponent, DatetimePeriodComponent,
DatetimeComponent, DatetimeComponent,
TimezoneSelectComponent,
DashboardAutocompleteComponent, DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent, EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent, 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-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-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.", "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": { "dialog": {
"close": "Close dialog" "close": "Close dialog"
@ -1752,6 +1770,12 @@
"help": "Help", "help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes" "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": { "queue": {
"select_name": "Select queue name", "select_name": "Select queue name",
"name": "Queue Name", "name": "Queue Name",
@ -1831,7 +1855,6 @@
"advanced": "Advanced" "advanced": "Advanced"
}, },
"timeunit": { "timeunit": {
"milliseconds": "Milliseconds",
"seconds": "Seconds", "seconds": "Seconds",
"minutes": "Minutes", "minutes": "Minutes",
"hours": "Hours", "hours": "Hours",

571
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -1,12 +1,14 @@
{ {
"access": { "access": {
"unauthorized": "권한 없음.", "unauthorized": "승인되지 않음",
"unauthorized-access": "허가되지 않은 접근", "unauthorized-access": "승인되지 않은 접근",
"unauthorized-access-text": "이 리소스에 접근하려면 로그인해야 합니다!", "unauthorized-access-text": "이 리소스에 접근하려면 로그인해야 합니다!",
"access-forbidden": "접근 금지", "access-forbidden": "접근 금지",
"access-forbidden-text": "접근 권한이 없습니다.!<br> 만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.", "access-forbidden-text": "접근 권한이 없습니다!<br> 만일 이 페이지에 계속 접근하려면 다른 사용자로 로그인 하세요.",
"refresh-token-expired": "세션이 만료되었습니다.", "refresh-token-expired": "세션이 만료되었습니다",
"refresh-token-failed": "세션을 새로 고칠 수 없습니다." "refresh-token-failed": "세션을 새로 고칠 수 없습니다.",
"permission-denied": "권한이 없습니다",
"permission-denied-text": "이 작업을 수행할 권한이 없습니다!"
}, },
"action": { "action": {
"activate": "활설화", "activate": "활설화",
@ -22,11 +24,11 @@
"update": "업데이트", "update": "업데이트",
"remove": "제거", "remove": "제거",
"search": "검색", "search": "검색",
"clear-search": "Clear search", "clear-search": "검색 초기화",
"assign": "할당", "assign": "할당",
"unassign": "비할당", "unassign": "비할당",
"share": "Share", "share": "Share",
"make-private": "Make private", "make-private": "비공개로 설정",
"apply": "적용", "apply": "적용",
"apply-changes": "변경사항 적용", "apply-changes": "변경사항 적용",
"edit-mode": "수정 모드", "edit-mode": "수정 모드",
@ -44,8 +46,8 @@
"undo": "취소", "undo": "취소",
"copy": "복사", "copy": "복사",
"paste": "붙여넣기", "paste": "붙여넣기",
"copy-reference": "Copy reference", "copy-reference": "참조 복사",
"paste-reference": "Paste reference", "paste-reference": "참조 붙여넣기",
"import": "가져오기", "import": "가져오기",
"export": "내보내기", "export": "내보내기",
"share-via": "Share via {{provider}}" "share-via": "Share via {{provider}}"
@ -79,26 +81,26 @@
"smtp-port": "SMTP 포트", "smtp-port": "SMTP 포트",
"smtp-port-required": "SMTP 포트를 입력해야 합니다.", "smtp-port-required": "SMTP 포트를 입력해야 합니다.",
"smtp-port-invalid": "올바른 SMTP 포트가 아닙니다.", "smtp-port-invalid": "올바른 SMTP 포트가 아닙니다.",
"timeout-msec": "제한시간 (msec)", "timeout-msec": "제한시간 (ms)",
"timeout-required": "제한시간을 입력해야 합니다.", "timeout-required": "제한시이 입력되지 않았습니다.",
"timeout-invalid": "올바른 제한시간이 아닙니다.", "timeout-invalid": "제한시간이 올바르게 입력되지 않았습니다.",
"enable-tls": "TLS 사용", "enable-tls": "TLS 사용",
"tls-version" : "TLS 버전", "tls-version" : "TLS 버전",
"send-test-mail": "테스트 메일 보내기" "send-test-mail": "테스트 메일 보내기"
}, },
"alarm": { "alarm": {
"alarm": "Alarm", "alarm": "알람",
"alarms": "Alarms", "alarms": "알람",
"select-alarm": "Select alarm", "select-alarm": "알람 선택",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.", "no-alarms-matching": "'{{entity}}'에 대한 알람이 존재하지 않습니다.",
"alarm-required": "Alarm is required", "alarm-required": "알람이 필요합니다",
"alarm-status": "Alarm status", "alarm-status": "알람 상태",
"search-status": { "search-status": {
"ANY": "Any", "ANY": "Any",
"ACTIVE": "Active", "ACTIVE": "활성",
"CLEARED": "Cleared", "CLEARED": "Cleared",
"ACK": "Acknowledged", "ACK": "수용",
"UNACK": "Unacknowledged" "UNACK": "불수용"
}, },
"display-status": { "display-status": {
"ACTIVE_UNACK": "Active Unacknowledged", "ACTIVE_UNACK": "Active Unacknowledged",
@ -107,28 +109,28 @@
"CLEARED_ACK": "Cleared Acknowledged" "CLEARED_ACK": "Cleared Acknowledged"
}, },
"no-alarms-prompt": "No alarms found", "no-alarms-prompt": "No alarms found",
"created-time": "Created time", "created-time": "생성된 시간",
"type": "Type", "type": "종류",
"severity": "Severity", "severity": "심각도",
"originator": "Originator", "originator": "창시자",
"originator-type": "Originator type", "originator-type": "창시자 종류",
"details": "Details", "details": "자세히",
"status": "Status", "status": "상태",
"alarm-details": "Alarm details", "alarm-details": "Alarm details",
"start-time": "Start time", "start-time": "시작 시각",
"end-time": "End time", "end-time": "마지막 시각",
"ack-time": "Acknowledged time", "ack-time": "Acknowledged time",
"clear-time": "Cleared time", "clear-time": "Cleared time",
"severity-critical": "Critical", "severity-critical": "심각한",
"severity-major": "Major", "severity-major": "주요한",
"severity-minor": "Minor", "severity-minor": "작은",
"severity-warning": "Warning", "severity-warning": "경고",
"severity-indeterminate": "Indeterminate", "severity-indeterminate": "중간",
"acknowledge": "Acknowledge", "acknowledge": "수용",
"clear": "Clear", "clear": "지우기",
"search": "Search alarms", "search": "알람 검색",
"selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } selected", "selected-alarms": "{ count, plural, 1 {1 alarm} other {# alarms} } 선택됨",
"no-data": "No data to display", "no-data": "표시할 데이터가 없습니다",
"polling-interval": "Alarms polling interval (sec)", "polling-interval": "Alarms polling interval (sec)",
"polling-interval-required": "Alarms polling interval is required.", "polling-interval-required": "Alarms polling interval is required.",
"min-polling-interval-message": "At least 1 sec polling interval is allowed.", "min-polling-interval-message": "At least 1 sec polling interval is allowed.",
@ -178,46 +180,46 @@
"any-relation": "any" "any-relation": "any"
}, },
"asset": { "asset": {
"asset": "Asset", "asset": "자산",
"assets": "Assets", "assets": "자산",
"management": "Asset management", "management": "자산 관리",
"view-assets": "View Assets", "view-assets": "자산 보기",
"add": "Add Asset", "add": "자산 추가",
"assign-to-customer": "Assign to customer", "assign-to-customer": "고객에게 자산 지정",
"assign-asset-to-customer": "Assign Asset(s) To Customer", "assign-asset-to-customer": "자산을 고객에게 지정",
"assign-asset-to-customer-text": "Please select the assets to assign to the customer", "assign-asset-to-customer-text": "고객에게 지정할 자산을 선택하세요",
"no-assets-text": "No assets found", "no-assets-text": "아무 자산도 없습니다",
"assign-to-customer-text": "Please select the customer to assign the asset(s)", "assign-to-customer-text": "자산에 지정될 고객을 선택하세요",
"public": "Public", "public": "공개",
"assignedToCustomer": "Assigned to customer", "assignedToCustomer": "지정된 고객",
"make-public": "Make asset public", "make-public": "자산을 공개로 설정",
"make-private": "Make asset private", "make-private": "자산을 비공개로 설정",
"unassign-from-customer": "Unassign from customer", "unassign-from-customer": "고객 지정 해제",
"delete": "Delete asset", "delete": "자산 삭제",
"asset-public": "Asset is public", "asset-public": "공개된 자산",
"asset-type": "Asset type", "asset-type": "자산 종류",
"asset-type-required": "Asset type is required.", "asset-type-required": "자산 종류를 선택하세요.",
"select-asset-type": "Select asset type", "select-asset-type": "자산 종류 선택",
"enter-asset-type": "Enter asset type", "enter-asset-type": "자산 종류 입력",
"any-asset": "Any asset", "any-asset": "모든 자산",
"no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.", "no-asset-types-matching": "'{{entitySubtype}}'과 일치하는 자산 종류가 없습니다.",
"asset-type-list-empty": "No asset types selected.", "asset-type-list-empty": "아무 자산 종류도 선택되지 않았습니다.",
"asset-types": "Asset types", "asset-types": "자산 종류",
"name": "Name", "name": "이름",
"name-required": "Name is required.", "name-required": "이름을 입력하세요.",
"description": "Description", "description": "설명",
"type": "Type", "type": "종류",
"type-required": "Type is required.", "type-required": "종류를 입력하세요.",
"details": "Details", "details": "자세히",
"events": "Events", "events": "이벤트",
"add-asset-text": "Add new asset", "add-asset-text": "새로운 자산 추가",
"asset-details": "Asset details", "asset-details": "자산 자세히",
"assign-assets": "Assign assets", "assign-assets": "자산 지정",
"assign-assets-text": "Assign { count, plural, 1 {1 asset} other {# assets} } to customer", "assign-assets-text": "자산 { count, plural, 1 {1 asset} other {# assets} }을 고객에게 지정",
"delete-assets": "Delete assets", "delete-assets": "자산 삭제",
"unassign-assets": "Unassign assets", "unassign-assets": "자산 지정 해제",
"unassign-assets-action-title": "Unassign { count, plural, 1 {1 asset} other {# assets} } from customer", "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-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-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} }?", "delete-assets-title": "Are you sure you want to delete { count, plural, 1 {1 asset} other {# assets} }?",
@ -248,10 +250,11 @@
"scope-server": "서버 속성", "scope-server": "서버 속성",
"scope-shared": "공유 속성", "scope-shared": "공유 속성",
"add": "속성 추가", "add": "속성 추가",
"key": "Key", "key": "키",
"key-required": "속성 key를 입력하세요.", "last-update-time": "마지막 수정된 시간",
"key-required": "속성 키를 입력하세요.",
"value": "Value", "value": "Value",
"value-required": "속성 value를 입력하세요.", "value-required": "속성 값을 입력하세요.",
"delete-attributes-title": "{ count, plural, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", "delete-attributes-title": "{ count, plural, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??",
"delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.",
"delete-attributes": "속성 삭제", "delete-attributes": "속성 삭제",
@ -264,38 +267,40 @@
"add-widget-to-dashboard": "대시보드에 위젯 추가", "add-widget-to-dashboard": "대시보드에 위젯 추가",
"selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨", "selected-attributes": "{ count, plural, 1 {속성 1개} other {속성 #개} } 선택됨",
"selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" "selected-telemetry": "{ count, plural, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨"
"no-attributes-text": "아무 속성도 찾을 수 없습니다",
"no-telemetry-text": "아무 텔레메트리도 찾을 수 없습니다."
}, },
"audit-log": { "audit-log": {
"audit": "Audit", "audit": "감사",
"audit-logs": "Audit Logs", "audit-logs": "감사 로그",
"timestamp": "Timestamp", "timestamp": "타임스탬프",
"entity-type": "Entity Type", "entity-type": "기체 종류",
"entity-name": "Entity Name", "entity-name": "개체 이름",
"user": "User", "user": "사용자",
"type": "Type", "type": "종류",
"status": "Status", "status": "상태",
"details": "Details", "details": "자세히",
"type-added": "Added", "type-added": "추가됨",
"type-deleted": "Deleted", "type-deleted": "삭제됨",
"type-updated": "Updated", "type-updated": "수정됨",
"type-attributes-updated": "Attributes updated", "type-attributes-updated": "속성이 수정되었습니다",
"type-attributes-deleted": "Attributes deleted", "type-attributes-deleted": "속성이 삭제되었습니다",
"type-rpc-call": "RPC call", "type-rpc-call": "RPC call",
"type-credentials-updated": "Credentials updated", "type-credentials-updated": "자격 증명이 갱신되었습니다",
"type-assigned-to-customer": "Assigned to Customer", "type-assigned-to-customer": "고객에게 지정",
"type-unassigned-from-customer": "Unassigned from Customer", "type-unassigned-from-customer": "지정된 고객 해제",
"type-activated": "Activated", "type-activated": "활성",
"type-suspended": "Suspended", "type-suspended": "일시 중지",
"type-credentials-read": "Credentials read", "type-credentials-read": "자격 증명 읽기",
"type-attributes-read": "Attributes read", "type-attributes-read": "속성 읽기",
"status-success": "Success", "status-success": "성공",
"status-failure": "Failure", "status-failure": "실패",
"audit-log-details": "Audit log details", "audit-log-details": "감사 로그 세부 사항",
"no-audit-logs-prompt": "No logs found", "no-audit-logs-prompt": "아무 로그도 없습니다.",
"action-data": "Action data", "action-data": "액션 데이터",
"failure-details": "Failure details", "failure-details": "실패 세부 사항",
"search": "Search audit logs", "search": "감사 로그 검색",
"clear-search": "Clear search" "clear-search": "검색 초기화"
}, },
"confirm-on-exit": { "confirm-on-exit": {
"message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?", "message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?",
@ -323,8 +328,8 @@
}, },
"content-type": { "content-type": {
"json": "Json", "json": "Json",
"text": "Text", "text": "텍스트",
"binary": "Binary (Base64)" "binary": "바이너리 (Base64)"
}, },
"customer": { "customer": {
"customers": "커스터머", "customers": "커스터머",
@ -337,10 +342,10 @@
"manage-customer-users": "커스터머 사용자 관리", "manage-customer-users": "커스터머 사용자 관리",
"manage-customer-devices": "커스터머 디바이스 관리", "manage-customer-devices": "커스터머 디바이스 관리",
"manage-customer-dashboards": "커스터머 대시보드 관리", "manage-customer-dashboards": "커스터머 대시보드 관리",
"manage-public-devices": "Manage public devices", "manage-public-devices": "공개된 디바이스 관리",
"manage-public-dashboards": "Manage public dashboards", "manage-public-dashboards": "공개된 대시보드 관리",
"manage-customer-assets": "Manage customer assets", "manage-customer-assets": "고객 자산 관리",
"manage-public-assets": "Manage public assets", "manage-public-assets": "공개된 자산 관리",
"add-customer-text": "커스터머 추가", "add-customer-text": "커스터머 추가",
"no-customers-text": "커스터머가 없습니다.", "no-customers-text": "커스터머가 없습니다.",
"customer-details": "커스터머 상세정보", "customer-details": "커스터머 상세정보",
@ -355,16 +360,16 @@
"title": "타이틀", "title": "타이틀",
"title-required": "타이틀을 입력하세요.", "title-required": "타이틀을 입력하세요.",
"description": "설명", "description": "설명",
"details": "Details", "details": "자세히",
"events": "Events", "events": "이벤트",
"copyId": "Copy customer Id", "copyId": "고객 ID 복사",
"idCopiedMessage": "Customer Id has been copied to clipboard", "idCopiedMessage": "고객 ID가 클립 보드에 복사되었습니다.",
"select-customer": "Select customer", "select-customer": "선택된 고객",
"no-customers-matching": "No customers matching '{{entity}}' were found.", "no-customers-matching": "'{{entity}}'에 해당하는 고객을 찾을 수 없습니다.",
"customer-required": "Customer is required", "customer-required": "고객을 입력하세요.",
"select-default-customer": "Select default customer", "select-default-customer": "기본 고객 선택",
"default-customer": "Default customer", "default-customer": "기본 고객",
"default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" "default-customer-required": "테넌트 수준에서 대시보드를 디버그 하기 위해서는 기본 고객이 필요합니다."
}, },
"datetime": { "datetime": {
"date-from": "시작 날짜", "date-from": "시작 날짜",
@ -522,8 +527,8 @@
"assign-to-customer-text": "디바이스를 할당할 커스터머를 선택하세요.", "assign-to-customer-text": "디바이스를 할당할 커스터머를 선택하세요.",
"device-details": "디바이스 상세정보", "device-details": "디바이스 상세정보",
"add-device-text": "디바이스 추가", "add-device-text": "디바이스 추가",
"credentials": "크리덴셜", "credentials": "자격 증명",
"manage-credentials": "크리덴셜 관리", "manage-credentials": "자격 증명 관리",
"delete": "디바이스 삭제", "delete": "디바이스 삭제",
"assign-devices": "디바이스 할당", "assign-devices": "디바이스 할당",
"assign-devices-text": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커서터머에 할당", "assign-devices-text": "{ count, plural, 1 {디바이스 1개} other {디바이스 #개} }를 커서터머에 할당",
@ -575,8 +580,8 @@
"unknown-error": "알 수 없는 오류" "unknown-error": "알 수 없는 오류"
}, },
"entity": { "entity": {
"entity": "Entity", "entity": "개체",
"entities": "Entities", "entities": "개체",
"aliases": "Entity aliases", "aliases": "Entity aliases",
"entity-alias": "Entity alias", "entity-alias": "Entity alias",
"unable-delete-entity-alias-title": "Unable to delete entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias",
@ -588,70 +593,70 @@
"alias-required": "Entity alias is required.", "alias-required": "Entity alias is required.",
"remove-alias": "Remove entity alias", "remove-alias": "Remove entity alias",
"add-alias": "Add entity alias", "add-alias": "Add entity alias",
"entity-list": "Entity list", "entity-list": "개체 목록",
"entity-type": "Entity type", "entity-type": "개체 종류",
"entity-types": "Entity types", "entity-types": "개체 종류",
"entity-type-list": "Entity type list", "entity-type-list": "개체 종류 목록",
"any-entity": "Any entity", "any-entity": "모든 개체",
"enter-entity-type": "Enter entity type", "enter-entity-type": "개체 종류 입력",
"no-entities-matching": "No entities matching '{{entity}}' were found.", "no-entities-matching": "No entities matching '{{entity}}' were found.",
"no-entity-types-matching": "No entity types matching '{{entityType}}' 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", "use-entity-name-filter": "Use filter",
"entity-list-empty": "No entities selected.", "entity-list-empty": "아무 개체도 선택되지 않았습니다.",
"entity-type-list-empty": "No entity types selected.", "entity-type-list-empty": "개체 종류가 선택되지 않았습니다.",
"entity-name-filter-required": "Entity name filter is required.", "entity-name-filter-required": "Entity name filter is required.",
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.", "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All", "all-subtypes": "모두",
"select-entities": "Select entities", "select-entities": "선택된 개체",
"no-aliases-found": "No aliases found.", "no-aliases-found": "No aliases found.",
"no-alias-matching": "'{{alias}}' not found.", "no-alias-matching": "'{{alias}}' not found.",
"create-new-alias": "Create a new one!", "create-new-alias": "생성 완료!",
"key": "Key", "key": "키",
"key-name": "Key name", "key-name": "키 이름",
"no-keys-found": "No keys found.", "no-keys-found": "아무 키도 찾을 수 없습니다..",
"no-key-matching": "'{{key}}' not found.", "no-key-matching": "'{{key}}'를 찾을 수 없습니다.",
"create-new-key": "Create a new one!", "create-new-key": "생성 완료!",
"type": "Type", "type": "종류",
"type-required": "Entity type is required.", "type-required": "개체의 종류를 입력하세요.",
"type-device": "Device", "type-device": "장치",
"type-devices": "Devices", "type-devices": "장치",
"list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }", "list-of-devices": "{ count, plural, 1 {One device} other {List of # devices} }",
"device-name-starts-with": "Devices whose names start with '{{prefix}}'", "device-name-starts-with": "Devices whose names start with '{{prefix}}'",
"type-asset": "Asset", "type-asset": "자산",
"type-assets": "Assets", "type-assets": "자산",
"list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }",
"asset-name-starts-with": "Assets whose names start with '{{prefix}}'", "asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
"type-rule": "Rule", "type-rule": "규칙",
"type-rules": "Rules", "type-rules": "규칙",
"list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }",
"rule-name-starts-with": "Rules whose names start with '{{prefix}}'", "rule-name-starts-with": "Rules whose names start with '{{prefix}}'",
"type-plugin": "Plugin", "type-plugin": "플러그인",
"type-plugins": "Plugins", "type-plugins": "플러그인",
"list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }", "list-of-plugins": "{ count, plural, 1 {One plugin} other {List of # plugins} }",
"plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'", "plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'",
"type-tenant": "Tenant", "type-tenant": "테넌트",
"type-tenants": "Tenants", "type-tenants": "테넌트",
"list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }", "list-of-tenants": "{ count, plural, 1 {One tenant} other {List of # tenants} }",
"tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'", "tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'",
"type-customer": "Customer", "type-customer": "고객",
"type-customers": "Customers", "type-customers": "고객",
"list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }", "list-of-customers": "{ count, plural, 1 {One customer} other {List of # customers} }",
"customer-name-starts-with": "Customers whose names start with '{{prefix}}'", "customer-name-starts-with": "Customers whose names start with '{{prefix}}'",
"type-user": "User", "type-user": "사용자",
"type-users": "Users", "type-users": "사용자",
"list-of-users": "{ count, plural, 1 {One user} other {List of # users} }", "list-of-users": "{ count, plural, 1 {One user} other {List of # users} }",
"user-name-starts-with": "Users whose names start with '{{prefix}}'", "user-name-starts-with": "Users whose names start with '{{prefix}}'",
"type-dashboard": "Dashboard", "type-dashboard": "대시보드",
"type-dashboards": "Dashboards", "type-dashboards": "대시보드",
"list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }", "list-of-dashboards": "{ count, plural, 1 {One dashboard} other {List of # dashboards} }",
"dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'", "dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
"type-alarm": "Alarm", "type-alarm": "알람",
"type-alarms": "Alarms", "type-alarms": "알람",
"list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }", "list-of-alarms": "{ count, plural, 1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'", "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
"type-rulechain": "Rule chain", "type-rulechain": "규칙 사슬",
"type-rulechains": "Rule chains", "type-rulechains": "규칙 사슬",
"list-of-rulechains": "{ count, plural, 1 {One rule chain} other {List of # rule chains} }", "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}}'", "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer", "type-current-customer": "Current Customer",
@ -667,23 +672,23 @@
"type-error": "에러", "type-error": "에러",
"type-lc-event": "주기적 이벤트", "type-lc-event": "주기적 이벤트",
"type-stats": "통계", "type-stats": "통계",
"type-debug-rule-node": "Debug", "type-debug-rule-node": "디버그",
"type-debug-rule-chain": "Debug", "type-debug-rule-chain": "디버그",
"no-events-prompt": "이벤트 없음", "no-events-prompt": "이벤트 없음",
"error": "에러", "error": "에러",
"alarm": "알람", "alarm": "알람",
"event-time": "이벤트 발생 시간", "event-time": "이벤트 발생 시간",
"server": "서버", "server": "서버",
"body": "Body", "body": "Body",
"method": "Method", "method": "방법",
"type": "Type", "type": "종류",
"entity": "Entity", "entity": "개체",
"message-id": "Message Id", "message-id": "메시지 ID",
"message-type": "Message Type", "message-type": "메시지 종류",
"data-type": "Data Type", "data-type": "데이터 종류",
"relation-type": "Relation Type", "relation-type": "관계 종류",
"metadata": "Metadata", "metadata": "메타데이터",
"data": "Data", "data": "데이터",
"event": "이벤트", "event": "이벤트",
"status": "상태", "status": "상태",
"success": "성공", "success": "성공",
@ -692,11 +697,11 @@
"errors-occurred": "오류가 발생했습니다" "errors-occurred": "오류가 발생했습니다"
}, },
"extension": { "extension": {
"extensions": "Extensions", "extensions": "확장",
"selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected", "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } selected",
"type": "Type", "type": "종류",
"key": "Key", "key": "키",
"value": "Value", "value": "값",
"id": "Id", "id": "Id",
"extension-id": "Extension id", "extension-id": "Extension id",
"extension-type": "Extension type", "extension-type": "Extension type",
@ -992,14 +997,14 @@
"invalid-additional-info": "Unable to parse additional info json." "invalid-additional-info": "Unable to parse additional info json."
}, },
"rulechain": { "rulechain": {
"rulechain": "Rule chain", "rulechain": "규칙 사슬",
"rulechains": "Rule chains", "rulechains": "규칙 사슬",
"root": "Root", "root": "Root",
"delete": "Delete rule chain", "delete": "Delete rule chain",
"name": "Name", "name": "이름",
"name-required": "Name is required.", "name-required": "이름을 입력하세요.",
"description": "Description", "description": "설명",
"add": "Add Rule Chain", "add": "규칙 사슬 추가",
"set-root": "Make rule chain root", "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-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.", "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-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-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.", "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", "add-rulechain-text": "새로운 규칙 사슬 추가",
"no-rulechains-text": "No rule chains found", "no-rulechains-text": "아무 규칙 사슬도 없습니다.",
"rulechain-details": "Rule chain details", "rulechain-details": "규칙 사슬 상세 정보",
"details": "Details", "details": "자세히",
"events": "Events", "events": "이벤트",
"system": "System", "system": "시스템",
"import": "Import rule chain", "import": "규칙 사슬 불러오기",
"export": "Export rule chain", "export": "규칙 사슬 내보내기",
"export-failed-error": "Unable to export rule chain: {{error}}", "export-failed-error": "Unable to export rule chain: {{error}}",
"create-new-rulechain": "Create new rule chain", "create-new-rulechain": "Create new rule chain",
"rulechain-file": "Rule chain file", "rulechain-file": "Rule chain file",
@ -1029,70 +1034,70 @@
"debug-mode": "Debug mode" "debug-mode": "Debug mode"
}, },
"rulenode": { "rulenode": {
"details": "Details", "details": "자세히",
"events": "Events", "events": "이벤트",
"search": "Search nodes", "search": "노드 검색",
"open-node-library": "Open node library", "open-node-library": "노드 라이브러리 열기",
"add": "Add rule node", "add": "규칙 노드 추가",
"name": "Name", "name": "이름",
"name-required": "Name is required.", "name-required": "이름을 입력하세요.",
"type": "Type", "type": "종류",
"description": "Description", "description": "설명",
"delete": "Delete rule node", "delete": "규칙 노드 삭제",
"select-all-objects": "Select all nodes and connections", "select-all-objects": "모든 노드와 연결을 선택",
"deselect-all-objects": "Deselect all nodes and connections", "deselect-all-objects": "모든 노드와 연결을 선택 해제",
"delete-selected-objects": "Delete selected nodes and connections", "delete-selected-objects": "선택된 노드와 연결을 삭제",
"delete-selected": "Delete selected", "delete-selected": "선택 삭제",
"select-all": "Select all", "select-all": "모두 선택",
"copy-selected": "Copy selected", "copy-selected": "선택 복사",
"deselect-all": "Deselect all", "deselect-all": "선택 해제",
"rulenode-details": "Rule node details", "rulenode-details": "규칙 노드 상세 정보",
"debug-mode": "Debug mode", "debug-mode": "디버그 모드",
"configuration": "Configuration", "configuration": "설정",
"link": "Link", "link": "링크",
"link-details": "Rule node link details", "link-details": "규칙 노드 링크 상세 정보",
"add-link": "Add link", "add-link": "링크 추가",
"link-label": "Link label", "link-label": "링크 라벨",
"link-label-required": "Link label is required.", "link-label-required": "링크 라벨을 입력하세요.",
"custom-link-label": "Custom link label", "custom-link-label": "링크 라벨 사용자 정의",
"custom-link-label-required": "Custom link label is required.", "custom-link-label-required": "링크 라벨 사용자 정의를 입력하세요.",
"type-filter": "Filter", "type-filter": "필터",
"type-filter-details": "Filter incoming messages with configured conditions", "type-filter-details": "Filter incoming messages with configured conditions",
"type-enrichment": "Enrichment", "type-enrichment": "Enrichment",
"type-enrichment-details": "Add additional information into Message Metadata", "type-enrichment-details": "Add additional information into Message Metadata",
"type-transformation": "Transformation", "type-transformation": "Transformation",
"type-transformation-details": "Change Message payload and Metadata", "type-transformation-details": "Change Message payload and Metadata",
"type-action": "Action", "type-action": "",
"type-action-details": "Perform special action", "type-action-details": "Perform special action",
"type-external": "External", "type-external": "외부",
"type-external-details": "Interacts with external system", "type-external-details": "Interacts with external system",
"type-rule-chain": "Rule Chain", "type-rule-chain": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified 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", "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.", "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
"ui-resources-load-error": "Failed to load configuration ui resources.", "ui-resources-load-error": "Failed to load configuration ui resources.",
"invalid-target-rulechain": "Unable to resolve target rule chain!", "invalid-target-rulechain": "Unable to resolve target rule chain!",
"test-script-function": "Test script function", "test-script-function": "Test script function",
"message": "Message", "message": "메시지",
"message-type": "Message type", "message-type": "메시지 종류",
"message-type-required": "Message type is required", "message-type-required": "메시지 종류를 입력하세요.",
"metadata": "Metadata", "metadata": "메타데이터",
"metadata-required": "Metadata entries can't be empty.", "metadata-required": "메타데이터 엔트리를 입력하세요.",
"output": "Output", "output": "출력",
"test": "Test", "test": "테스트",
"help": "Help" "help": "도움말"
}, },
"tenant": { "tenant": {
"tenants": "테넌트", "tenants": "테넌트",
"management": "테넌트 관리", "management": "테넌트 관리",
"add": "테넌트 추가", "add": "테넌트 추가",
"admins": "Admins", "admins": "관리자",
"manage-tenant-admins": "테넌트 관리자 관리", "manage-tenant-admins": "테넌트 관리자 관리",
"delete": "테넌트 삭제", "delete": "테넌트 삭제",
"add-tenant-text": "테넌트 추가", "add-tenant-text": "테넌트 추가",
"no-tenants-text": "테넌트가 없습니다.", "no-tenants-text": "테넌트가 없습니다.",
"tenant-details": "테넌트 상세정보", "tenant-details": "테넌트 상세 정보",
"delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?", "delete-tenant-title": "'{{tenantTitle}}' 테넌트를 삭제하시겠습니까?",
"delete-tenant-text": "테넌트와 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", "delete-tenant-text": "테넌트와 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.",
"delete-tenants-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} }를 삭제하시겠습니까?", "delete-tenants-title": "{ count, plural, 1 {테넌트 1개} other {테넌트 #개} }를 삭제하시겠습니까?",
@ -1101,23 +1106,23 @@
"title": "타이틀", "title": "타이틀",
"title-required": "타이틀을 입력하세요.", "title-required": "타이틀을 입력하세요.",
"description": "설명", "description": "설명",
"details": "Details", "details": "자세히",
"events": "Events", "events": "이벤트",
"copyId": "Copy tenant Id", "copyId": "테넌트 ID 복사",
"idCopiedMessage": "Tenant Id has been copied to clipboard", "idCopiedMessage": "테넌트 ID를 클립보드로 복사",
"select-tenant": "Select tenant", "select-tenant": "테넌트 선택",
"no-tenants-matching": "No tenants matching '{{entity}}' were found.", "no-tenants-matching": "No tenants matching '{{entity}}' were found.",
"tenant-required": "Tenant is required" "tenant-required": "테넌트가 필요합니다."
}, },
"timeinterval": { "timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",
"minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }", "minutes-interval": "{ minutes, plural, 1 {1 minute} other {# minutes} }",
"hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }", "hours-interval": "{ hours, plural, 1 {1 hour} other {# hours} }",
"days-interval": "{ days, plural, 1 {1 day} other {# days} }", "days-interval": "{ days, plural, 1 {1 day} other {# days} }",
"days": "Days", "days": "일",
"hours": "Hours", "hours": "시간",
"minutes": "Minutes", "minutes": "분",
"seconds": "Seconds", "seconds": "초",
"advanced": "고급" "advanced": "고급"
}, },
"timewindow": { "timewindow": {
@ -1125,13 +1130,13 @@
"hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }", "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }",
"minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }", "minutes": "{ minutes, plural, 0 { minute } 1 {1 minute } other {# minutes } }",
"seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }", "seconds": "{ seconds, plural, 0 { second } 1 {1 second } other {# seconds } }",
"realtime": "Realtime", "realtime": "실시간",
"history": "History", "history": "기록",
"last-prefix": "last", "last-prefix": "과거",
"period": "from {{ startTime }} to {{ endTime }}", "period": "{{ startTime }}부터 {{ endTime }}까지",
"edit": "타임윈도우 편집", "edit": "타임윈도우 편집",
"date-range": "날짜 범위", "date-range": "날짜 범위",
"last": "Last", "last": "과거",
"time-period": "기간" "time-period": "기간"
}, },
"user": { "user": {
@ -1146,7 +1151,7 @@
"delete": "사용자 삭제", "delete": "사용자 삭제",
"add-user-text": "새로운 사용자 추가", "add-user-text": "새로운 사용자 추가",
"no-users-text": "사용자가 없습니다.", "no-users-text": "사용자가 없습니다.",
"user-details": "사용자 상세정보", "user-details": "사용자 상세 정보",
"delete-user-title": "'{{userEmail}}' 사용자를 삭제하시겠습니까?", "delete-user-title": "'{{userEmail}}' 사용자를 삭제하시겠습니까?",
"delete-user-text": "사용자와 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-user-text": "사용자와 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.",
"delete-users-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} }을 삭제하시겠니까?", "delete-users-title": "{ count, plural, 1 {사용자 1명} other {사용자 #명} }을 삭제하시겠니까?",
@ -1242,10 +1247,10 @@
"update-dashboard-state": "Update current dashboard state", "update-dashboard-state": "Update current dashboard state",
"open-dashboard": "Navigate to other dashboard", "open-dashboard": "Navigate to other dashboard",
"custom": "Custom action", "custom": "Custom action",
"target-dashboard-state": "Target dashboard state", "target-dashboard-state": "대상 대시보드 상태",
"target-dashboard-state-required": "Target dashboard state is required", "target-dashboard-state-required": "대상 대시보드 상태가 필요합니다.",
"set-entity-from-widget": "Set entity from widget", "set-entity-from-widget": "위젯으로 부터 객체 설정",
"target-dashboard": "Target dashboard", "target-dashboard": "대상 대시보드",
"open-right-layout": "Open right dashboard layout (mobile view)" "open-right-layout": "Open right dashboard layout (mobile view)"
}, },
"widgets-bundle": { "widgets-bundle": {
@ -1253,13 +1258,13 @@
"widgets-bundles": "위젯 번들", "widgets-bundles": "위젯 번들",
"add": "위젯 번들 추가", "add": "위젯 번들 추가",
"delete": "위젯 번들 삭제", "delete": "위젯 번들 삭제",
"title": "타이틀", "title": "제목",
"title-required": "타이틀을 입력하세요.", "title-required": "제목을 입력하세요.",
"add-widgets-bundle-text": "위젯 번들 추가", "add-widgets-bundle-text": "위젯 번들 추가",
"no-widgets-bundles-text": "위젯 번들이 없습니다.", "no-widgets-bundles-text": "위젯 번들이 없습니다.",
"empty": "위젯 번들이 비어있습니다.", "empty": "위젯 번들이 비어있습니다.",
"details": "상세", "details": "상세",
"widgets-bundle-details": "위젯 번들 상세정보", "widgets-bundle-details": "위젯 번들 상세 정보",
"delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' 위젯 번들을 삭제하시겠습니까?", "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' 위젯 번들을 삭제하시겠습니까?",
"delete-widgets-bundle-text": "위젯 번들과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "delete-widgets-bundle-text": "위젯 번들과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.",
"delete-widgets-bundles-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} }를 삭제하시겠습니까?", "delete-widgets-bundles-title": "{ count, plural, 1 {위젯 번들 1개} other {위젯 번들 #개} }를 삭제하시겠습니까?",
@ -1279,15 +1284,15 @@
"data": "데이터", "data": "데이터",
"settings": "설정", "settings": "설정",
"advanced": "고급", "advanced": "고급",
"title": "타이틀", "title": "제목",
"general-settings": "일반 설정", "general-settings": "일반 설정",
"display-title": "타이틀 표시", "display-title": "제목 표시",
"drop-shadow": "그림자", "drop-shadow": "그림자",
"enable-fullscreen": "전체화면 사용 ", "enable-fullscreen": "전체화면 사용 ",
"background-color": "배경 색", "background-color": "배경 색",
"text-color": "글자 색", "text-color": "글자 색",
"padding": "패딩", "padding": "패딩",
"title-style": "타이틀 스타일", "title-style": "제목 스타일",
"mobile-mode-settings": "모바일 모드 설정", "mobile-mode-settings": "모바일 모드 설정",
"order": "순서", "order": "순서",
"height": "높이", "height": "높이",
@ -1333,18 +1338,18 @@
"Oct": "10월", "Oct": "10월",
"Nov": "11월", "Nov": "11월",
"Dec": "12월", "Dec": "12월",
"January": "일월", "January": "1월",
"February": "이월", "February": "2월",
"March": "행진", "March": "3월",
"April": "4 월", "April": "4월",
"June": "유월", "June": "6월",
"July": "칠월", "July": "7월",
"August": "팔월", "August": "8월",
"September": "구월", "September": "9월",
"October": "십월", "October": "10월",
"November": "십일월", "November": "11월",
"December": "12 월", "December": "12월",
"Custom Date Range": "맞춤 기간", "Custom Date Range": "임의 기간 범위",
"Date Range Template": "기간 템플릿", "Date Range Template": "기간 템플릿",
"Today": "오늘", "Today": "오늘",
"Yesterday": "어제", "Yesterday": "어제",
@ -1359,22 +1364,22 @@
"Hour": "시간", "Hour": "시간",
"Day": "일", "Day": "일",
"Week": "주", "Week": "주",
"2 weeks": "이주", "2 weeks": "2 주",
"Month": "달", "Month": "달",
"3 months": "3 개월", "3 months": "3 개월",
"6 months": "6 개월", "6 months": "6 개월",
"Custom interval": "사용자 지정 간격", "Custom interval": "사용자 지정 간격",
"Interval": "간격", "Interval": "간격",
"Step size": "단계 크기", "Step size": "단계 크기",
"Ok": "Ok" "Ok": "확인"
} }
} }
}, },
"icon": { "icon": {
"icon": "Icon", "icon": "아이콘",
"select-icon": "Select icon", "select-icon": "선택된 아이콘",
"material-icons": "Material icons", "material-icons": "Material icons",
"show-all": "Show all icons" "show-all": "모든 아이콘 보기"
}, },
"custom": { "custom": {
"widget-action": { "widget-action": {

2
ui-ngx/src/tsconfig.app.json

@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "../out-tsc/app", "outDir": "../out-tsc/app",
"types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", "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": { "angularCompilerOptions": {
"fullTemplateTypeCheck": true "fullTemplateTypeCheck": true

52
ui-ngx/yarn.lock

@ -1397,20 +1397,6 @@
dependencies: dependencies:
"@types/jquery" "*" "@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": "@types/leaflet-polylinedecorator@^1.6.0":
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#1572131ffedb3154c6e18e682d2fb700e203af19" resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#1572131ffedb3154c6e18e682d2fb700e203af19"
@ -1418,6 +1404,13 @@
dependencies: dependencies:
"@types/leaflet" "*" "@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": "@types/leaflet@*", "@types/leaflet@^1.5.17":
version "1.5.17" version "1.5.17"
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b" 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" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== 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": "@types/mousetrap@^1.6.0":
version "1.6.3" version "1.6.3"
resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d"
@ -5632,7 +5632,7 @@ jstree@^3.3.10:
dependencies: dependencies:
jquery ">=1.9.1" 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" version "3.5.0"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6"
integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== 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" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 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: moment@^2.27.0:
version "2.27.0" version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
@ -6421,10 +6433,12 @@ ngx-color-picker@^10.0.1:
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
ngx-daterangepicker-material@^3.0.4: ngx-daterangepicker-material@^4.0.1:
version "3.0.4" version "4.0.1"
resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-3.0.4.tgz#af759e52fd587fcc9bce1fbcfc8cde828df6a471" resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-4.0.1.tgz#788c2e32eb4717629d4a0e60a60bf8d6430d8c13"
integrity sha512-pDg8kdXx/h8es8dpjBI+xbsxQbS0dV3uSPgfsx39t9LIw3Dv50h8T1achT5jUWSzSU7855ywTk+NlNBDTgkeNg== integrity sha512-0gY6DGU+dgYdmoAKrIJSB9xnDqBvj91Yis3II/ZJxxMfZVTG4qMMatck6w8FzdU+CYT64ArCq+Uwa6hJRHX6Nw==
dependencies:
tslib "^1.10.0"
"ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": "ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master":
version "0.0.0" version "0.0.0"

Loading…
Cancel
Save