Merge pull request #8928 from thingsboard/feature/timeseries-api-improvements
added new ui for deleting timeseries
This commit is contained in:
commit
91e94d5777
@ -462,7 +462,9 @@ public class TelemetryController extends BaseController {
|
|||||||
notes = "Delete time-series for selected entity based on entity id, entity type and keys." +
|
notes = "Delete time-series for selected entity based on entity id, entity type and keys." +
|
||||||
" Use 'deleteAllDataForKeys' to delete all time-series data." +
|
" Use 'deleteAllDataForKeys' to delete all time-series data." +
|
||||||
" Use 'startTs' and 'endTs' to specify time-range instead. " +
|
" Use 'startTs' and 'endTs' to specify time-range instead. " +
|
||||||
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) after deletion of the time range. " +
|
" Use 'deleteLatest' to delete latest value (stored in separate table for performance) if the value's timestamp matches the time-range. " +
|
||||||
|
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." +
|
||||||
|
" The replacement value will be fetched from the 'time-series' table, and its timestamp will be the most recent one before the defined time-range. " +
|
||||||
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
|
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
|
||||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
@ApiResponses(value = {
|
@ApiResponses(value = {
|
||||||
@ -486,14 +488,16 @@ public class TelemetryController extends BaseController {
|
|||||||
@RequestParam(name = "startTs", required = false) Long startTs,
|
@RequestParam(name = "startTs", required = false) Long startTs,
|
||||||
@ApiParam(value = "A long value representing the end timestamp of removal time range in milliseconds.")
|
@ApiParam(value = "A long value representing the end timestamp of removal time range in milliseconds.")
|
||||||
@RequestParam(name = "endTs", required = false) Long endTs,
|
@RequestParam(name = "endTs", required = false) Long endTs,
|
||||||
|
@ApiParam(value = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.")
|
||||||
|
@RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest,
|
||||||
@ApiParam(value = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
|
@ApiParam(value = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
|
||||||
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
|
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
|
||||||
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
|
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
|
||||||
return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted);
|
return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
|
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
|
||||||
Long startTs, Long endTs, boolean rewriteLatestIfDeleted) throws ThingsboardException {
|
Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException {
|
||||||
List<String> keys = toKeysList(keysStr);
|
List<String> keys = toKeysList(keysStr);
|
||||||
if (keys.isEmpty()) {
|
if (keys.isEmpty()) {
|
||||||
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
|
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
|
||||||
@ -517,7 +521,7 @@ public class TelemetryController extends BaseController {
|
|||||||
return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> {
|
return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> {
|
||||||
List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
|
List<DeleteTsKvQuery> deleteTsKvQueries = new ArrayList<>();
|
||||||
for (String key : keys) {
|
for (String key : keys) {
|
||||||
deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted));
|
deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted, deleteLatest));
|
||||||
}
|
}
|
||||||
tsSubService.deleteTimeseriesAndNotify(tenantId, entityId, keys, deleteTsKvQueries, new FutureCallback<>() {
|
tsSubService.deleteTimeseriesAndNotify(tenantId, entityId, keys, deleteTsKvQueries, new FutureCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -796,6 +796,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
|
|||||||
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
|
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected <T> T doDeleteAsync(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
|
||||||
|
return readResponse(doDeleteAsync(urlTemplate, DEFAULT_TIMEOUT, params).andExpect(status().isOk()), responseClass);
|
||||||
|
}
|
||||||
|
|
||||||
protected ResultActions doPost(String urlTemplate, String... params) throws Exception {
|
protected ResultActions doPost(String urlTemplate, String... params) throws Exception {
|
||||||
MockHttpServletRequestBuilder postRequest = post(urlTemplate);
|
MockHttpServletRequestBuilder postRequest = post(urlTemplate);
|
||||||
setJwtToken(postRequest);
|
setJwtToken(postRequest);
|
||||||
@ -828,6 +832,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
|
|||||||
return mockMvc.perform(deleteRequest);
|
return mockMvc.perform(deleteRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception {
|
||||||
|
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params);
|
||||||
|
setJwtToken(deleteRequest);
|
||||||
|
// populateParams(deleteRequest, params);
|
||||||
|
MvcResult result = mockMvc.perform(deleteRequest).andReturn();
|
||||||
|
result.getAsyncResult(timeout);
|
||||||
|
return mockMvc.perform(asyncDispatch(result));
|
||||||
|
}
|
||||||
|
|
||||||
protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
|
protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
|
||||||
if (params != null && params.length > 0) {
|
if (params != null && params.length > 0) {
|
||||||
Assert.assertEquals(0, params.length % 2);
|
Assert.assertEquals(0, params.length % 2);
|
||||||
|
|||||||
@ -15,15 +15,22 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.controller;
|
package org.thingsboard.server.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
import org.thingsboard.server.common.data.Device;
|
import org.thingsboard.server.common.data.Device;
|
||||||
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
|
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
|
||||||
|
import org.thingsboard.server.common.data.query.EntityKey;
|
||||||
|
import org.thingsboard.server.common.data.query.SingleEntityFilter;
|
||||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
|
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
|
||||||
import org.thingsboard.server.dao.service.DaoSqlTest;
|
import org.thingsboard.server.dao.service.DaoSqlTest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES;
|
||||||
|
|
||||||
@DaoSqlTest
|
@DaoSqlTest
|
||||||
@TestPropertySource(properties = {
|
@TestPropertySource(properties = {
|
||||||
@ -44,6 +51,92 @@ public class TelemetryControllerTest extends AbstractControllerTest {
|
|||||||
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteAllTelemetryWithLatest() throws Exception {
|
||||||
|
loginTenantAdmin();
|
||||||
|
Device device = createDevice();
|
||||||
|
|
||||||
|
SingleEntityFilter filter = new SingleEntityFilter();
|
||||||
|
filter.setSingleEntity(device.getId());
|
||||||
|
|
||||||
|
getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter);
|
||||||
|
|
||||||
|
getWsClient().registerWaitForUpdate(1);
|
||||||
|
|
||||||
|
long startTs = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String testBody = "{\"data\": \"value\"}";
|
||||||
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
|
||||||
|
|
||||||
|
long endTs = System.currentTimeMillis();
|
||||||
|
|
||||||
|
ObjectNode latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
|
||||||
|
|
||||||
|
Assert.assertNotNull(latest);
|
||||||
|
var data = latest.get("data");
|
||||||
|
Assert.assertNotNull(data);
|
||||||
|
|
||||||
|
Assert.assertEquals("value", data.get(0).get("value").asText());
|
||||||
|
|
||||||
|
ObjectNode timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
|
||||||
|
|
||||||
|
Assert.assertNotNull(timeseries);
|
||||||
|
|
||||||
|
Assert.assertEquals("value", timeseries.get("data").get(0).get("value").asText());
|
||||||
|
|
||||||
|
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=data&deleteAllDataForKeys=true", String.class);
|
||||||
|
|
||||||
|
latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
|
||||||
|
|
||||||
|
Assert.assertTrue(latest.get("data").get(0).get("value").isNull());
|
||||||
|
|
||||||
|
timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
|
||||||
|
|
||||||
|
Assert.assertTrue(timeseries.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteAllTelemetryWithoutLatest() throws Exception {
|
||||||
|
loginTenantAdmin();
|
||||||
|
Device device = createDevice();
|
||||||
|
|
||||||
|
SingleEntityFilter filter = new SingleEntityFilter();
|
||||||
|
filter.setSingleEntity(device.getId());
|
||||||
|
|
||||||
|
getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter);
|
||||||
|
|
||||||
|
getWsClient().registerWaitForUpdate(1);
|
||||||
|
|
||||||
|
long startTs = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String testBody = "{\"data\": \"value\"}";
|
||||||
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
|
||||||
|
|
||||||
|
long endTs = System.currentTimeMillis();
|
||||||
|
|
||||||
|
ObjectNode latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
|
||||||
|
|
||||||
|
Assert.assertNotNull(latest);
|
||||||
|
|
||||||
|
Assert.assertEquals("value", latest.get("data").get(0).get("value").asText());
|
||||||
|
|
||||||
|
ObjectNode timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
|
||||||
|
|
||||||
|
Assert.assertNotNull(timeseries);
|
||||||
|
|
||||||
|
Assert.assertEquals("value", timeseries.get("data").get(0).get("value").asText());
|
||||||
|
|
||||||
|
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete?keys=data&deleteAllDataForKeys=true&deleteLatest=false", String.class);
|
||||||
|
|
||||||
|
latest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data", ObjectNode.class);
|
||||||
|
|
||||||
|
Assert.assertEquals("value", latest.get("data").get(0).get("value").asText());
|
||||||
|
|
||||||
|
timeseries = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=data&startTs={startTs}&endTs={endTs}", ObjectNode.class, startTs, endTs);
|
||||||
|
|
||||||
|
Assert.assertTrue(timeseries.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValueConstraintValidator() throws Exception {
|
public void testValueConstraintValidator() throws Exception {
|
||||||
loginTenantAdmin();
|
loginTenantAdmin();
|
||||||
|
|||||||
@ -21,14 +21,20 @@ import lombok.Data;
|
|||||||
public class BaseDeleteTsKvQuery extends BaseTsKvQuery implements DeleteTsKvQuery {
|
public class BaseDeleteTsKvQuery extends BaseTsKvQuery implements DeleteTsKvQuery {
|
||||||
|
|
||||||
private final Boolean rewriteLatestIfDeleted;
|
private final Boolean rewriteLatestIfDeleted;
|
||||||
|
private final Boolean deleteLatest;
|
||||||
|
|
||||||
public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted) {
|
public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) {
|
||||||
super(key, startTs, endTs);
|
super(key, startTs, endTs);
|
||||||
this.rewriteLatestIfDeleted = rewriteLatestIfDeleted;
|
this.rewriteLatestIfDeleted = rewriteLatestIfDeleted;
|
||||||
|
this.deleteLatest = deleteLatest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseDeleteTsKvQuery(String key, long startTs, long endTs, boolean rewriteLatestIfDeleted) {
|
||||||
|
this(key, startTs, endTs, rewriteLatestIfDeleted, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseDeleteTsKvQuery(String key, long startTs, long endTs) {
|
public BaseDeleteTsKvQuery(String key, long startTs, long endTs) {
|
||||||
this(key, startTs, endTs, false);
|
this(key, startTs, endTs, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,4 +19,6 @@ public interface DeleteTsKvQuery extends TsKvQuery {
|
|||||||
|
|
||||||
Boolean getRewriteLatestIfDeleted();
|
Boolean getRewriteLatestIfDeleted();
|
||||||
|
|
||||||
|
Boolean getDeleteLatest();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -275,8 +275,10 @@ public class BaseTimeseriesService implements TimeseriesService {
|
|||||||
|
|
||||||
private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) {
|
private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) {
|
||||||
futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
|
futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
|
||||||
|
if (query.getDeleteLatest()) {
|
||||||
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
|
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void validate(EntityId entityId) {
|
private static void validate(EntityId entityId) {
|
||||||
Validator.validateEntityId(entityId, "Incorrect entityId " + entityId);
|
Validator.validateEntityId(entityId, "Incorrect entityId " + entityId);
|
||||||
|
|||||||
@ -2364,7 +2364,8 @@ public class RestClient implements Closeable {
|
|||||||
boolean deleteAllDataForKeys,
|
boolean deleteAllDataForKeys,
|
||||||
Long startTs,
|
Long startTs,
|
||||||
Long endTs,
|
Long endTs,
|
||||||
boolean rewriteLatestIfDeleted) {
|
boolean rewriteLatestIfDeleted,
|
||||||
|
boolean deleteLatest) {
|
||||||
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());
|
||||||
@ -2373,17 +2374,34 @@ public class RestClient implements Closeable {
|
|||||||
params.put("startTs", startTs.toString());
|
params.put("startTs", startTs.toString());
|
||||||
params.put("endTs", endTs.toString());
|
params.put("endTs", endTs.toString());
|
||||||
params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted));
|
params.put("rewriteLatestIfDeleted", String.valueOf(rewriteLatestIfDeleted));
|
||||||
|
params.put("deleteLatest", String.valueOf(deleteLatest));
|
||||||
|
|
||||||
return restTemplate
|
return restTemplate
|
||||||
.exchange(
|
.exchange(
|
||||||
baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}",
|
baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete?keys={keys}&deleteAllDataForKeys={deleteAllDataForKeys}&startTs={startTs}&endTs={endTs}&rewriteLatestIfDeleted={rewriteLatestIfDeleted}&deleteLatest={deleteLatest}",
|
||||||
HttpMethod.DELETE,
|
HttpMethod.DELETE,
|
||||||
HttpEntity.EMPTY,
|
HttpEntity.EMPTY,
|
||||||
Object.class,
|
Object.class,
|
||||||
params)
|
params)
|
||||||
.getStatusCode()
|
.getStatusCode()
|
||||||
.is2xxSuccessful();
|
.is2xxSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteEntityLatestTimeseries(EntityId entityId, List<String> keys) {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("entityType", entityId.getEntityType().name());
|
||||||
|
params.put("entityId", entityId.getId().toString());
|
||||||
|
params.put("keys", listToString(keys));
|
||||||
|
|
||||||
|
return restTemplate
|
||||||
|
.exchange(
|
||||||
|
baseURL + "/api/plugins/telemetry/{entityType}/{entityId}/timeseries/latest/delete?keys={keys}",
|
||||||
|
HttpMethod.DELETE,
|
||||||
|
HttpEntity.EMPTY,
|
||||||
|
Object.class,
|
||||||
|
params)
|
||||||
|
.getStatusCode()
|
||||||
|
.is2xxSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean deleteEntityAttributes(DeviceId deviceId, String scope, List<String> keys) {
|
public boolean deleteEntityAttributes(DeviceId deviceId, String scope, List<String> keys) {
|
||||||
|
|||||||
@ -50,10 +50,19 @@ export class AttributeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
|
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
|
||||||
startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> {
|
startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true,
|
||||||
|
config?: RequestConfig): Observable<any> {
|
||||||
const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(',');
|
const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(',');
|
||||||
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` +
|
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`;
|
||||||
`?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`;
|
if (isDefinedAndNotNull(deleteAllDataForKeys)) {
|
||||||
|
url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`;
|
||||||
|
}
|
||||||
|
if (isDefinedAndNotNull(rewriteLatestIfDeleted)) {
|
||||||
|
url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`;
|
||||||
|
}
|
||||||
|
if (isDefinedAndNotNull(deleteLatest)) {
|
||||||
|
url += `&deleteLatest=${deleteLatest}`;
|
||||||
|
}
|
||||||
if (isDefinedAndNotNull(startTs)) {
|
if (isDefinedAndNotNull(startTs)) {
|
||||||
url += `&startTs=${startTs}`;
|
url += `&startTs=${startTs}`;
|
||||||
}
|
}
|
||||||
@ -103,7 +112,8 @@ export class AttributeService {
|
|||||||
});
|
});
|
||||||
let deleteEntityTimeseriesObservable: Observable<any>;
|
let deleteEntityTimeseriesObservable: Observable<any>;
|
||||||
if (deleteTimeseries.length) {
|
if (deleteTimeseries.length) {
|
||||||
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config);
|
deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true,
|
||||||
|
null, null, false, true, config);
|
||||||
} else {
|
} else {
|
||||||
deleteEntityTimeseriesObservable = of(null);
|
deleteEntityTimeseriesObservable = of(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,12 +85,12 @@
|
|||||||
'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
|
'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
|
||||||
</span>
|
</span>
|
||||||
<span fxFlex></span>
|
<span fxFlex></span>
|
||||||
<button [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)"
|
<button [fxShow]="attributeScope !== attributeScopeTypes.CLIENT_SCOPE"
|
||||||
class="button-widget-action"
|
class="button-widget-action"
|
||||||
mat-icon-button [disabled]="isLoading$ | async"
|
mat-icon-button [disabled]="isLoading$ | async"
|
||||||
matTooltip="{{ 'action.delete' | translate }}"
|
matTooltip="{{ 'action.delete' | translate }}"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
(click)="deleteAttributes($event)">
|
(click)="deleteTelemetry($event)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-raised-button color="accent"
|
<button mat-raised-button color="accent"
|
||||||
@ -198,6 +198,10 @@
|
|||||||
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
|
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</span>
|
</span>
|
||||||
|
<button mat-icon-button *ngIf="attributeScope === latestTelemetryTypes.LATEST_TELEMETRY"
|
||||||
|
(click)="deleteTimeseries($event, attribute)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import {
|
|||||||
LatestTelemetry,
|
LatestTelemetry,
|
||||||
TelemetryType,
|
TelemetryType,
|
||||||
telemetryTypeTranslations,
|
telemetryTypeTranslations,
|
||||||
|
TimeseriesDeleteStrategy,
|
||||||
toTelemetryType
|
toTelemetryType
|
||||||
} from '@shared/models/telemetry/telemetry.models';
|
} from '@shared/models/telemetry/telemetry.models';
|
||||||
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
|
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
|
||||||
@ -82,10 +83,15 @@ import {
|
|||||||
AddWidgetToDashboardDialogComponent,
|
AddWidgetToDashboardDialogComponent,
|
||||||
AddWidgetToDashboardDialogData
|
AddWidgetToDashboardDialogData
|
||||||
} from '@home/components/attribute/add-widget-to-dashboard-dialog.component';
|
} from '@home/components/attribute/add-widget-to-dashboard-dialog.component';
|
||||||
import { deepClone } from '@core/utils';
|
import { deepClone, isUndefinedOrNull } from '@core/utils';
|
||||||
import { Filters } from '@shared/models/query/query.models';
|
import { Filters } from '@shared/models/query/query.models';
|
||||||
import { hidePageSizePixelValue } from '@shared/models/constants';
|
import { hidePageSizePixelValue } from '@shared/models/constants';
|
||||||
import { ResizeObserver } from '@juggle/resize-observer';
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
|
import {
|
||||||
|
DELETE_TIMESERIES_PANEL_DATA,
|
||||||
|
DeleteTimeseriesPanelComponent,
|
||||||
|
DeleteTimeseriesPanelData
|
||||||
|
} from '@home/components/attribute/delete-timeseries-panel.component';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -379,6 +385,82 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTimeseries($event: Event, telemetry?: AttributeData) {
|
||||||
|
if ($event) {
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
const isMultipleDeletion = isUndefinedOrNull(telemetry) && this.dataSource.selection.selected.length > 1;
|
||||||
|
const target = $event.target || $event.srcElement || $event.currentTarget;
|
||||||
|
const config = new OverlayConfig({
|
||||||
|
panelClass: 'tb-filter-panel',
|
||||||
|
backdropClass: 'cdk-overlay-transparent-backdrop',
|
||||||
|
hasBackdrop: true,
|
||||||
|
maxWidth: 488,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
const connectedPosition: ConnectedPosition = {
|
||||||
|
originX: 'start',
|
||||||
|
originY: 'top',
|
||||||
|
overlayX: 'end',
|
||||||
|
overlayY: 'top'
|
||||||
|
};
|
||||||
|
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
|
||||||
|
.withPositions([connectedPosition]);
|
||||||
|
const overlayRef = this.overlay.create(config);
|
||||||
|
overlayRef.backdropClick().subscribe(() => {
|
||||||
|
overlayRef.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const providers: StaticProvider[] = [
|
||||||
|
{
|
||||||
|
provide: DELETE_TIMESERIES_PANEL_DATA,
|
||||||
|
useValue: {
|
||||||
|
isMultipleDeletion
|
||||||
|
} as DeleteTimeseriesPanelData
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: OverlayRef,
|
||||||
|
useValue: overlayRef
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
|
||||||
|
const componentRef = overlayRef.attach(new ComponentPortal(DeleteTimeseriesPanelComponent,
|
||||||
|
this.viewContainerRef, injector));
|
||||||
|
componentRef.onDestroy(() => {
|
||||||
|
if (componentRef.instance.result !== null) {
|
||||||
|
const result = componentRef.instance.result;
|
||||||
|
const deleteTimeseries = telemetry ? [telemetry]: this.dataSource.selection.selected;
|
||||||
|
let deleteAllDataForKeys = false;
|
||||||
|
let rewriteLatestIfDeleted = false;
|
||||||
|
let startTs = null;
|
||||||
|
let endTs = null;
|
||||||
|
let deleteLatest = true;
|
||||||
|
switch (result.strategy) {
|
||||||
|
case TimeseriesDeleteStrategy.DELETE_ALL_DATA:
|
||||||
|
deleteAllDataForKeys = true;
|
||||||
|
break;
|
||||||
|
case TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE:
|
||||||
|
deleteAllDataForKeys = true;
|
||||||
|
deleteLatest = false;
|
||||||
|
break;
|
||||||
|
case TimeseriesDeleteStrategy.DELETE_LATEST_VALUE:
|
||||||
|
rewriteLatestIfDeleted = result.rewriteLatest;
|
||||||
|
startTs = deleteTimeseries[0].lastUpdateTs;
|
||||||
|
endTs = startTs + 1;
|
||||||
|
break;
|
||||||
|
case TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD:
|
||||||
|
startTs = result.startDateTime.getTime();
|
||||||
|
endTs = result.endDateTime.getTime();
|
||||||
|
rewriteLatestIfDeleted = result.rewriteLatest;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.attributeService.deleteEntityTimeseries(this.entityIdValue, deleteTimeseries, deleteAllDataForKeys,
|
||||||
|
startTs, endTs, rewriteLatestIfDeleted, deleteLatest)
|
||||||
|
.subscribe(() => this.reloadAttributes());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deleteAttributes($event: Event) {
|
deleteAttributes($event: Event) {
|
||||||
if ($event) {
|
if ($event) {
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
@ -403,6 +485,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTelemetry($event: Event) {
|
||||||
|
if (this.attributeScope === this.latestTelemetryTypes.LATEST_TELEMETRY) {
|
||||||
|
this.deleteTimeseries($event);
|
||||||
|
} else {
|
||||||
|
this.deleteAttributes($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enterWidgetMode() {
|
enterWidgetMode() {
|
||||||
this.mode = 'widget';
|
this.mode = 'widget';
|
||||||
this.widgetsList = [];
|
this.widgetsList = [];
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
Copyright © 2016-2023 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-toolbar>
|
||||||
|
<h2>{{ "attribute.delete-timeseries.delete-strategy" | translate }}</h2>
|
||||||
|
<span fxFlex></span>
|
||||||
|
<button mat-icon-button
|
||||||
|
type="button"
|
||||||
|
(click)="cancel()">
|
||||||
|
<mat-icon class="material-icons">close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-toolbar>
|
||||||
|
<form [formGroup]="deleteTimeseriesFormGroup" class="mat-content mat-padding tb-form-settings">
|
||||||
|
<mat-form-field class="mat-block" subscriptSizing="dynamic">
|
||||||
|
<mat-label translate>attribute.delete-timeseries.strategy</mat-label>
|
||||||
|
<mat-select formControlName="strategy">
|
||||||
|
<mat-option *ngFor="let strategy of strategiesTranslationsMap.keys()" [value]="strategy">
|
||||||
|
{{ strategiesTranslationsMap.get(strategy) | translate }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<div *ngIf="isPeriodStrategy()" class="tb-select-interval">
|
||||||
|
<mat-form-field class="mat-block" subscriptSizing="dynamic">
|
||||||
|
<mat-label translate>attribute.delete-timeseries.start-time</mat-label>
|
||||||
|
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
|
||||||
|
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
|
||||||
|
<input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field class="mat-block" subscriptSizing="dynamic">
|
||||||
|
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
|
||||||
|
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
|
||||||
|
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
|
||||||
|
<input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<mat-slide-toggle class="tb-slide-toggle" formControlName="rewriteLatest" *ngIf="isPeriodStrategy() || isDeleteLatestStrategy()">
|
||||||
|
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
|
||||||
|
</mat-slide-toggle>
|
||||||
|
</form>
|
||||||
|
<div fxLayout="row" class="tb-panel-actions">
|
||||||
|
<span fxFlex></span>
|
||||||
|
<button mat-button color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="cancel()" cdkFocusInitial>
|
||||||
|
{{ 'action.cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-button mat-raised-button color="primary"
|
||||||
|
type="button"
|
||||||
|
(click)="delete()"
|
||||||
|
[disabled]="deleteTimeseriesFormGroup.invalid">
|
||||||
|
{{ 'action.apply' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2023 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 '../../../../../scss/constants';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mat-toolbar {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-form-settings {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-select-interval {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
@media #{$mat-xs} {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-slide-toggle {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
///
|
||||||
|
/// Copyright © 2016-2023 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, Inject, InjectionToken, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { OverlayRef } from '@angular/cdk/overlay';
|
||||||
|
import {
|
||||||
|
TimeseriesDeleteStrategy,
|
||||||
|
timeseriesDeleteStrategyTranslations
|
||||||
|
} from '@shared/models/telemetry/telemetry.models';
|
||||||
|
import { MINUTE } from '@shared/models/time/time.models';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export const DELETE_TIMESERIES_PANEL_DATA = new InjectionToken<any>('DeleteTimeseriesPanelData');
|
||||||
|
|
||||||
|
export interface DeleteTimeseriesPanelData {
|
||||||
|
isMultipleDeletion: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteTimeseriesPanelResult {
|
||||||
|
strategy: TimeseriesDeleteStrategy;
|
||||||
|
startDateTime: Date;
|
||||||
|
endDateTime: Date;
|
||||||
|
rewriteLatest: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-delete-timeseries-panel',
|
||||||
|
templateUrl: './delete-timeseries-panel.component.html',
|
||||||
|
styleUrls: ['./delete-timeseries-panel.component.scss']
|
||||||
|
})
|
||||||
|
export class DeleteTimeseriesPanelComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
deleteTimeseriesFormGroup: FormGroup;
|
||||||
|
|
||||||
|
result: DeleteTimeseriesPanelResult = null;
|
||||||
|
|
||||||
|
strategiesTranslationsMap = timeseriesDeleteStrategyTranslations;
|
||||||
|
|
||||||
|
private multipleDeletionStrategies = new Set<TimeseriesDeleteStrategy>([
|
||||||
|
TimeseriesDeleteStrategy.DELETE_ALL_DATA,
|
||||||
|
TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE
|
||||||
|
]);
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(@Inject(DELETE_TIMESERIES_PANEL_DATA) private data: DeleteTimeseriesPanelData,
|
||||||
|
private overlayRef: OverlayRef,
|
||||||
|
private fb: FormBuilder) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const today = new Date();
|
||||||
|
if (this.data.isMultipleDeletion) {
|
||||||
|
this.strategiesTranslationsMap = new Map(Array.from(this.strategiesTranslationsMap)
|
||||||
|
.filter(([strategy]) => this.multipleDeletionStrategies.has(strategy)))
|
||||||
|
}
|
||||||
|
this.deleteTimeseriesFormGroup = this.fb.group({
|
||||||
|
strategy: [TimeseriesDeleteStrategy.DELETE_ALL_DATA],
|
||||||
|
startDateTime: [
|
||||||
|
{ value: new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()), disabled: true },
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
endDateTime: [{ value: today, disabled: true }, Validators.required],
|
||||||
|
rewriteLatest: [true]
|
||||||
|
})
|
||||||
|
|
||||||
|
this.deleteTimeseriesFormGroup.get('strategy').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => {
|
||||||
|
if (value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD) {
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime').enable({onlySelf: true, emitEvent: false});
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime').enable({onlySelf: true, emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime').disable({onlySelf: true, emitEvent: false});
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime').disable({onlySelf: true, emitEvent: false});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => this.onStartDateTimeChange(value));
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime').valueChanges.pipe(
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
).subscribe(value => this.onEndDateTimeChange(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(): void {
|
||||||
|
if (this.deleteTimeseriesFormGroup.valid) {
|
||||||
|
this.result = this.deleteTimeseriesFormGroup.value;
|
||||||
|
this.overlayRef.dispose();
|
||||||
|
} else {
|
||||||
|
this.deleteTimeseriesFormGroup.markAllAsTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.overlayRef.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
isPeriodStrategy(): boolean {
|
||||||
|
return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeleteLatestStrategy(): boolean {
|
||||||
|
return this.deleteTimeseriesFormGroup.get('strategy').value === TimeseriesDeleteStrategy.DELETE_LATEST_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStartDateTimeChange(newStartDateTime: Date) {
|
||||||
|
if (newStartDateTime) {
|
||||||
|
const endDateTimeTs = this.deleteTimeseriesFormGroup.get('endDateTime').value.getTime();
|
||||||
|
if (newStartDateTime.getTime() >= endDateTimeTs) {
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime')
|
||||||
|
.patchValue(new Date(endDateTimeTs - MINUTE), {onlySelf: true, emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.deleteTimeseriesFormGroup.get('startDateTime')
|
||||||
|
.patchValue(newStartDateTime, {onlySelf: true, emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEndDateTimeChange(newEndDateTime: Date) {
|
||||||
|
if (newEndDateTime) {
|
||||||
|
const startDateTimeTs = this.deleteTimeseriesFormGroup.get('startDateTime').value.getTime();
|
||||||
|
if (newEndDateTime.getTime() <= startDateTimeTs) {
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime')
|
||||||
|
.patchValue(new Date(startDateTimeTs + MINUTE), {onlySelf: true, emitEvent: false});
|
||||||
|
} else {
|
||||||
|
this.deleteTimeseriesFormGroup.get('endDateTime')
|
||||||
|
.patchValue(newEndDateTime, {onlySelf: true, emitEvent: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -177,6 +177,7 @@ import {
|
|||||||
} from '@home/components/widget/action/manage-widget-actions-dialog.component';
|
} from '@home/components/widget/action/manage-widget-actions-dialog.component';
|
||||||
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
|
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
|
||||||
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
|
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
|
||||||
|
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations:
|
declarations:
|
||||||
@ -205,6 +206,7 @@ import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/ba
|
|||||||
AttributeTableComponent,
|
AttributeTableComponent,
|
||||||
AddAttributeDialogComponent,
|
AddAttributeDialogComponent,
|
||||||
EditAttributeValuePanelComponent,
|
EditAttributeValuePanelComponent,
|
||||||
|
DeleteTimeseriesPanelComponent,
|
||||||
AliasesEntitySelectPanelComponent,
|
AliasesEntitySelectPanelComponent,
|
||||||
AliasesEntitySelectComponent,
|
AliasesEntitySelectComponent,
|
||||||
AliasesEntityAutocompleteComponent,
|
AliasesEntityAutocompleteComponent,
|
||||||
|
|||||||
@ -61,6 +61,13 @@ export enum TelemetryFeature {
|
|||||||
TIMESERIES = 'TIMESERIES'
|
TIMESERIES = 'TIMESERIES'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TimeseriesDeleteStrategy {
|
||||||
|
DELETE_ALL_DATA = 'DELETE_ALL_DATA',
|
||||||
|
DELETE_ALL_DATA_EXCEPT_LATEST_VALUE = 'DELETE_ALL_DATA_EXCEPT_LATEST_VALUE',
|
||||||
|
DELETE_LATEST_VALUE = 'DELETE_LATEST_VALUE',
|
||||||
|
DELETE_ALL_DATA_FOR_TIME_PERIOD = 'DELETE_ALL_DATA_FOR_TIME_PERIOD'
|
||||||
|
}
|
||||||
|
|
||||||
export type TelemetryType = LatestTelemetry | AttributeScope;
|
export type TelemetryType = LatestTelemetry | AttributeScope;
|
||||||
|
|
||||||
export const toTelemetryType = (val: string): TelemetryType => {
|
export const toTelemetryType = (val: string): TelemetryType => {
|
||||||
@ -73,7 +80,7 @@ export const toTelemetryType = (val: string): TelemetryType => {
|
|||||||
|
|
||||||
export const telemetryTypeTranslations = new Map<TelemetryType, string>(
|
export const telemetryTypeTranslations = new Map<TelemetryType, string>(
|
||||||
[
|
[
|
||||||
[LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-latest-telemetry'],
|
[LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-telemetry'],
|
||||||
[AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'],
|
[AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'],
|
||||||
[AttributeScope.SERVER_SCOPE, 'attribute.scope-server'],
|
[AttributeScope.SERVER_SCOPE, 'attribute.scope-server'],
|
||||||
[AttributeScope.SHARED_SCOPE, 'attribute.scope-shared']
|
[AttributeScope.SHARED_SCOPE, 'attribute.scope-shared']
|
||||||
@ -89,6 +96,15 @@ export const isClientSideTelemetryType = new Map<TelemetryType, boolean>(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const timeseriesDeleteStrategyTranslations = new Map<TimeseriesDeleteStrategy, string>(
|
||||||
|
[
|
||||||
|
[TimeseriesDeleteStrategy.DELETE_ALL_DATA, 'attribute.delete-timeseries.all-data'],
|
||||||
|
[TimeseriesDeleteStrategy.DELETE_ALL_DATA_EXCEPT_LATEST_VALUE, 'attribute.delete-timeseries.all-data-except-latest-value'],
|
||||||
|
[TimeseriesDeleteStrategy.DELETE_LATEST_VALUE, 'attribute.delete-timeseries.latest-value'],
|
||||||
|
[TimeseriesDeleteStrategy.DELETE_ALL_DATA_FOR_TIME_PERIOD, 'attribute.delete-timeseries.all-data-for-time-period']
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
export interface AttributeData {
|
export interface AttributeData {
|
||||||
lastUpdateTs?: number;
|
lastUpdateTs?: number;
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
@ -632,6 +632,7 @@
|
|||||||
"attributes": "Atributs",
|
"attributes": "Atributs",
|
||||||
"latest-telemetry": "Última telemetria",
|
"latest-telemetry": "Última telemetria",
|
||||||
"attributes-scope": "Abast dels atributs del dispositiu",
|
"attributes-scope": "Abast dels atributs del dispositiu",
|
||||||
|
"scope-telemetry": "Telemetria",
|
||||||
"scope-latest-telemetry": "Última telemetria",
|
"scope-latest-telemetry": "Última telemetria",
|
||||||
"scope-client": "Atributs del Client",
|
"scope-client": "Atributs del Client",
|
||||||
"scope-server": "Atributs del Servidor",
|
"scope-server": "Atributs del Servidor",
|
||||||
|
|||||||
@ -445,6 +445,7 @@
|
|||||||
"attributes": "Atributy",
|
"attributes": "Atributy",
|
||||||
"latest-telemetry": "Poslední telemetrie",
|
"latest-telemetry": "Poslední telemetrie",
|
||||||
"attributes-scope": "Rozsah atributů entity",
|
"attributes-scope": "Rozsah atributů entity",
|
||||||
|
"scope-telemetry": "Telemetrie",
|
||||||
"scope-latest-telemetry": "Poslední telemetrie",
|
"scope-latest-telemetry": "Poslední telemetrie",
|
||||||
"scope-client": "Atributy klienta",
|
"scope-client": "Atributy klienta",
|
||||||
"scope-server": "Atributy serveru",
|
"scope-server": "Atributy serveru",
|
||||||
|
|||||||
@ -453,6 +453,7 @@
|
|||||||
"attributes": "Attributter",
|
"attributes": "Attributter",
|
||||||
"latest-telemetry": "Seneste telemetri",
|
"latest-telemetry": "Seneste telemetri",
|
||||||
"attributes-scope": "Omfang af entitetsattributter",
|
"attributes-scope": "Omfang af entitetsattributter",
|
||||||
|
"scope-telemetry": "Telemetri",
|
||||||
"scope-latest-telemetry": "Seneste telemetri",
|
"scope-latest-telemetry": "Seneste telemetri",
|
||||||
"scope-client": "Klientattributter",
|
"scope-client": "Klientattributter",
|
||||||
"scope-server": "Serverattributter",
|
"scope-server": "Serverattributter",
|
||||||
|
|||||||
@ -324,6 +324,7 @@
|
|||||||
"attributes": "Eigenschaften",
|
"attributes": "Eigenschaften",
|
||||||
"latest-telemetry": "Neueste Telemetrie",
|
"latest-telemetry": "Neueste Telemetrie",
|
||||||
"attributes-scope": "Entitätseigenschaftsbereich",
|
"attributes-scope": "Entitätseigenschaftsbereich",
|
||||||
|
"scope-telemetry": "Telemetrie",
|
||||||
"scope-latest-telemetry": "Neueste Telemetrie",
|
"scope-latest-telemetry": "Neueste Telemetrie",
|
||||||
"scope-client": "Client Eigenschaften",
|
"scope-client": "Client Eigenschaften",
|
||||||
"scope-server": "Server Eigenschaften",
|
"scope-server": "Server Eigenschaften",
|
||||||
|
|||||||
@ -291,6 +291,7 @@
|
|||||||
"attributes": "Χαρακτηριστικά",
|
"attributes": "Χαρακτηριστικά",
|
||||||
"latest-telemetry": "Τελευταία τηλεμετρία",
|
"latest-telemetry": "Τελευταία τηλεμετρία",
|
||||||
"attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας",
|
"attributes-scope": "Πεδίο εφαρμογής Χαρακτηριστικών Οντότητας",
|
||||||
|
"scope-telemetry": "Τηλεμετρία",
|
||||||
"scope-latest-telemetry": "Τελευταία τηλεμετρία",
|
"scope-latest-telemetry": "Τελευταία τηλεμετρία",
|
||||||
"scope-client": "Χαρακτηριστικά Client",
|
"scope-client": "Χαρακτηριστικά Client",
|
||||||
"scope-server": "Χαρακτηριστικά Server",
|
"scope-server": "Χαρακτηριστικά Server",
|
||||||
|
|||||||
@ -699,6 +699,7 @@
|
|||||||
"latest-telemetry": "Latest telemetry",
|
"latest-telemetry": "Latest telemetry",
|
||||||
"no-latest-telemetry": "No latest telemetry",
|
"no-latest-telemetry": "No latest telemetry",
|
||||||
"attributes-scope": "Entity attributes scope",
|
"attributes-scope": "Entity attributes scope",
|
||||||
|
"scope-telemetry": "Telemetry",
|
||||||
"scope-latest-telemetry": "Latest telemetry",
|
"scope-latest-telemetry": "Latest telemetry",
|
||||||
"scope-client": "Client attributes",
|
"scope-client": "Client attributes",
|
||||||
"scope-server": "Server attributes",
|
"scope-server": "Server attributes",
|
||||||
@ -726,7 +727,19 @@
|
|||||||
"no-telemetry-text": "No telemetry found",
|
"no-telemetry-text": "No telemetry found",
|
||||||
"copy-key": "Copy key",
|
"copy-key": "Copy key",
|
||||||
"copy-value": "Copy value",
|
"copy-value": "Copy value",
|
||||||
"add-telemetry": "Add telemetry"
|
"add-telemetry": "Add telemetry",
|
||||||
|
"copy-value": "Copy value",
|
||||||
|
"delete-timeseries": {
|
||||||
|
"start-time": "Start time",
|
||||||
|
"ends-on": "Ends on",
|
||||||
|
"strategy": "Strategy",
|
||||||
|
"delete-strategy": "Delete strategy",
|
||||||
|
"all-data": "Delete all data",
|
||||||
|
"all-data-except-latest-value": "Delete all data except latest value",
|
||||||
|
"latest-value": "Delete latest value",
|
||||||
|
"all-data-for-time-period": "Delete all data for time period",
|
||||||
|
"rewrite-latest-value": "Rewrite latest value"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"api-usage": {
|
"api-usage": {
|
||||||
"api-features": "API features",
|
"api-features": "API features",
|
||||||
|
|||||||
@ -667,6 +667,7 @@
|
|||||||
"attributes": "Atributos",
|
"attributes": "Atributos",
|
||||||
"latest-telemetry": "Última telemetría",
|
"latest-telemetry": "Última telemetría",
|
||||||
"attributes-scope": "Alcance de los atributos del dispositivo",
|
"attributes-scope": "Alcance de los atributos del dispositivo",
|
||||||
|
"scope-telemetry": "Telemetría",
|
||||||
"scope-latest-telemetry": "Última telemetría",
|
"scope-latest-telemetry": "Última telemetría",
|
||||||
"scope-client": "Atributos de Cliente",
|
"scope-client": "Atributos de Cliente",
|
||||||
"scope-server": "Atributos de Servidor",
|
"scope-server": "Atributos de Servidor",
|
||||||
|
|||||||
@ -254,6 +254,7 @@
|
|||||||
"attributes": "ويژگي ها",
|
"attributes": "ويژگي ها",
|
||||||
"latest-telemetry": "آخرين سنجش",
|
"latest-telemetry": "آخرين سنجش",
|
||||||
"attributes-scope": "حوزه ويژگي هاي موجودي",
|
"attributes-scope": "حوزه ويژگي هاي موجودي",
|
||||||
|
"scope-telemetry": "تله متری",
|
||||||
"scope-latest-telemetry": "آخرين سنجش",
|
"scope-latest-telemetry": "آخرين سنجش",
|
||||||
"scope-client": "ويژگي هاي مشتري",
|
"scope-client": "ويژگي هاي مشتري",
|
||||||
"scope-server": "ويژگي هاي سِروِر",
|
"scope-server": "ويژگي هاي سِروِر",
|
||||||
|
|||||||
@ -459,6 +459,7 @@
|
|||||||
"next-widget": "Widget suivant",
|
"next-widget": "Widget suivant",
|
||||||
"prev-widget": "Widget précédent",
|
"prev-widget": "Widget précédent",
|
||||||
"scope-client": "Attributs du client",
|
"scope-client": "Attributs du client",
|
||||||
|
"scope-telemetry": "Télémétrie",
|
||||||
"scope-latest-telemetry": "Dernière télémétrie",
|
"scope-latest-telemetry": "Dernière télémétrie",
|
||||||
"scope-server": "Attributs du serveur",
|
"scope-server": "Attributs du serveur",
|
||||||
"scope-shared": "Attributs partagés",
|
"scope-shared": "Attributs partagés",
|
||||||
|
|||||||
@ -276,6 +276,7 @@
|
|||||||
"attributes": "Attributi",
|
"attributes": "Attributi",
|
||||||
"latest-telemetry": "Ultima telemetria",
|
"latest-telemetry": "Ultima telemetria",
|
||||||
"attributes-scope": "Visibilità attributi entità",
|
"attributes-scope": "Visibilità attributi entità",
|
||||||
|
"scope-telemetry": "Telemetria",
|
||||||
"scope-latest-telemetry": "Ultima telemetria",
|
"scope-latest-telemetry": "Ultima telemetria",
|
||||||
"scope-client": "Attributi client",
|
"scope-client": "Attributi client",
|
||||||
"scope-server": "Attributi server",
|
"scope-server": "Attributi server",
|
||||||
|
|||||||
@ -244,6 +244,7 @@
|
|||||||
"attributes": "属性",
|
"attributes": "属性",
|
||||||
"latest-telemetry": "最新テレメトリ",
|
"latest-telemetry": "最新テレメトリ",
|
||||||
"attributes-scope": "エンティティ属性のスコープ",
|
"attributes-scope": "エンティティ属性のスコープ",
|
||||||
|
"scope-telemetry": "テレメトリー",
|
||||||
"scope-latest-telemetry": "最新テレメトリ",
|
"scope-latest-telemetry": "最新テレメトリ",
|
||||||
"scope-client": "クライアントの属性",
|
"scope-client": "クライアントの属性",
|
||||||
"scope-server": "サーバーの属性",
|
"scope-server": "サーバーの属性",
|
||||||
|
|||||||
@ -290,6 +290,7 @@
|
|||||||
"attributes": "ატრიბუტები",
|
"attributes": "ატრიბუტები",
|
||||||
"latest-telemetry": "უახლესი ტელემეტრია",
|
"latest-telemetry": "უახლესი ტელემეტრია",
|
||||||
"attributes-scope": "ობიექტის ატრიბუტების ფარგლები",
|
"attributes-scope": "ობიექტის ატრიბუტების ფარგლები",
|
||||||
|
"scope-telemetry": "ტელემეტრია",
|
||||||
"scope-latest-telemetry": "უახლესი ტელემეტრია",
|
"scope-latest-telemetry": "უახლესი ტელემეტრია",
|
||||||
"scope-client": "კლიენტის ატრიბუტები",
|
"scope-client": "კლიენტის ატრიბუტები",
|
||||||
"scope-server": "სერვერის ატრიბუტები",
|
"scope-server": "სერვერის ატრიბუტები",
|
||||||
|
|||||||
@ -408,6 +408,7 @@
|
|||||||
"attributes": "속성",
|
"attributes": "속성",
|
||||||
"latest-telemetry": "최근 데이터",
|
"latest-telemetry": "최근 데이터",
|
||||||
"attributes-scope": "장치 속성 범위",
|
"attributes-scope": "장치 속성 범위",
|
||||||
|
"scope-telemetry": "원격 측정",
|
||||||
"scope-latest-telemetry": "최근 데이터",
|
"scope-latest-telemetry": "최근 데이터",
|
||||||
"scope-client": "클라이언트 속성",
|
"scope-client": "클라이언트 속성",
|
||||||
"scope-server": "서버 속성",
|
"scope-server": "서버 속성",
|
||||||
|
|||||||
@ -256,6 +256,7 @@
|
|||||||
"attributes": "Attribūti",
|
"attributes": "Attribūti",
|
||||||
"latest-telemetry": "Jaunākā telemetrija",
|
"latest-telemetry": "Jaunākā telemetrija",
|
||||||
"attributes-scope": "Vienības atribūtu darbības joma",
|
"attributes-scope": "Vienības atribūtu darbības joma",
|
||||||
|
"scope-telemetry": "Telemetrija",
|
||||||
"scope-latest-telemetry": "Jaunākā telemetrija",
|
"scope-latest-telemetry": "Jaunākā telemetrija",
|
||||||
"scope-client": "Klientu atribūti",
|
"scope-client": "Klientu atribūti",
|
||||||
"scope-server": "Servera atribūti",
|
"scope-server": "Servera atribūti",
|
||||||
|
|||||||
@ -309,6 +309,7 @@
|
|||||||
"attributes": "Atributos",
|
"attributes": "Atributos",
|
||||||
"latest-telemetry": "Última telemetria",
|
"latest-telemetry": "Última telemetria",
|
||||||
"attributes-scope": "Escopo de atributos de entidade",
|
"attributes-scope": "Escopo de atributos de entidade",
|
||||||
|
"scope-telemetry": "Telemetria",
|
||||||
"scope-latest-telemetry": "Última telemetria",
|
"scope-latest-telemetry": "Última telemetria",
|
||||||
"scope-client": "Atributos do cliente",
|
"scope-client": "Atributos do cliente",
|
||||||
"scope-server": "Atributos do servidor",
|
"scope-server": "Atributos do servidor",
|
||||||
|
|||||||
@ -285,6 +285,7 @@
|
|||||||
"attributes": "Atribute",
|
"attributes": "Atribute",
|
||||||
"latest-telemetry": "Ultimele Date Telemetrice",
|
"latest-telemetry": "Ultimele Date Telemetrice",
|
||||||
"attributes-scope": "Scop Atribute Entitate",
|
"attributes-scope": "Scop Atribute Entitate",
|
||||||
|
"scope-telemetry": "Telemetrie",
|
||||||
"scope-latest-telemetry": "Ultimele Date Telemetrice",
|
"scope-latest-telemetry": "Ultimele Date Telemetrice",
|
||||||
"scope-client": "Atribute Client",
|
"scope-client": "Atribute Client",
|
||||||
"scope-server": "Atribute Server",
|
"scope-server": "Atribute Server",
|
||||||
|
|||||||
@ -408,6 +408,7 @@
|
|||||||
"attributes": "Lastnosti",
|
"attributes": "Lastnosti",
|
||||||
"latest-telemetry": "Najnovejša telemetrija",
|
"latest-telemetry": "Najnovejša telemetrija",
|
||||||
"attributes-scope": "Obseg atributov entitete",
|
"attributes-scope": "Obseg atributov entitete",
|
||||||
|
"scope-telemetry": "Telemetrija",
|
||||||
"scope-latest-telemetry": "Najnovejša telemetrija",
|
"scope-latest-telemetry": "Najnovejša telemetrija",
|
||||||
"scope-client": "Atributi odjemalca",
|
"scope-client": "Atributi odjemalca",
|
||||||
"scope-server": "Atributi strežnika",
|
"scope-server": "Atributi strežnika",
|
||||||
|
|||||||
@ -445,6 +445,7 @@
|
|||||||
"attributes": "Öznitelikler",
|
"attributes": "Öznitelikler",
|
||||||
"latest-telemetry": "Son telemetri",
|
"latest-telemetry": "Son telemetri",
|
||||||
"attributes-scope": "Varlık öznitelik kapsamı",
|
"attributes-scope": "Varlık öznitelik kapsamı",
|
||||||
|
"scope-telemetry": "telemetri",
|
||||||
"scope-latest-telemetry": "Son telemetri",
|
"scope-latest-telemetry": "Son telemetri",
|
||||||
"scope-client": "İstemci öznitelikler",
|
"scope-client": "İstemci öznitelikler",
|
||||||
"scope-server": "Sunucu öznitelikler",
|
"scope-server": "Sunucu öznitelikler",
|
||||||
|
|||||||
@ -342,6 +342,7 @@
|
|||||||
"attributes": "Атрибути",
|
"attributes": "Атрибути",
|
||||||
"latest-telemetry": "Остання телеметрія",
|
"latest-telemetry": "Остання телеметрія",
|
||||||
"attributes-scope": "Область видимості атрибутів",
|
"attributes-scope": "Область видимості атрибутів",
|
||||||
|
"scope-telemetry": "Телеметрія",
|
||||||
"scope-latest-telemetry": "Остання телеметрія",
|
"scope-latest-telemetry": "Остання телеметрія",
|
||||||
"scope-client": "Клієнтські атрибути",
|
"scope-client": "Клієнтські атрибути",
|
||||||
"scope-server": "Серверні атрибути",
|
"scope-server": "Серверні атрибути",
|
||||||
|
|||||||
@ -590,6 +590,7 @@
|
|||||||
"attributes": "属性",
|
"attributes": "属性",
|
||||||
"latest-telemetry": "最新遥测数据",
|
"latest-telemetry": "最新遥测数据",
|
||||||
"attributes-scope": "设备属性范围",
|
"attributes-scope": "设备属性范围",
|
||||||
|
"scope-telemetry": "遥测",
|
||||||
"scope-latest-telemetry": "最新遥测数据",
|
"scope-latest-telemetry": "最新遥测数据",
|
||||||
"scope-client": "客户端属性",
|
"scope-client": "客户端属性",
|
||||||
"scope-server": "服务端属性",
|
"scope-server": "服务端属性",
|
||||||
|
|||||||
@ -519,6 +519,7 @@
|
|||||||
"attributes": "屬性",
|
"attributes": "屬性",
|
||||||
"latest-telemetry": "最新遙測",
|
"latest-telemetry": "最新遙測",
|
||||||
"attributes-scope": "設備屬性範圍",
|
"attributes-scope": "設備屬性範圍",
|
||||||
|
"scope-telemetry": "遙測",
|
||||||
"scope-latest-telemetry": "最新遙測",
|
"scope-latest-telemetry": "最新遙測",
|
||||||
"scope-client": "客戶端屬性",
|
"scope-client": "客戶端屬性",
|
||||||
"scope-server": "服務端屬性",
|
"scope-server": "服務端屬性",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user