Browse Source

Merge branch 'master' into develop/3.0

pull/2663/head
Igor Kulikov 7 years ago
parent
commit
1b0fabb7ab
  1. 123
      application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql
  2. 125
      application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql
  3. 11
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  4. 24
      application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java
  5. 66
      application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
  6. 54
      application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
  7. 89
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractTimeseriesCleanUpService.java
  8. 41
      application/src/main/java/org/thingsboard/server/service/ttl/PsqlTimeseriesCleanUpService.java
  9. 35
      application/src/main/java/org/thingsboard/server/service/ttl/TimescaleTimeseriesCleanUpService.java
  10. 4
      application/src/main/resources/thingsboard.yml
  11. 21
      common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java
  12. 120
      dao/src/main/resources/sql/schema-timescale.sql
  13. 271
      dao/src/main/resources/sql/schema-ts-psql.sql
  14. 5
      ui/src/app/api/entity.service.js
  15. 5
      ui/src/app/api/login.service.js
  16. 7
      ui/src/app/common/types.constant.js
  17. 6
      ui/src/app/components/react/json-form-ace-editor.jsx
  18. 2
      ui/src/app/components/react/json-form-array.jsx
  19. 2
      ui/src/app/components/react/json-form-fieldset.jsx
  20. 26
      ui/src/app/components/react/json-form-schema-form.jsx
  21. 3
      ui/src/app/components/react/json-form.scss
  22. 2
      ui/src/app/components/widget/widget.controller.js
  23. 4
      ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
  24. 6
      ui/src/app/entity/entity-select.directive.js
  25. 4
      ui/src/app/entity/entity-select.tpl.html
  26. 1
      ui/src/app/locale/locale.constant-cs_CZ.json
  27. 1
      ui/src/app/locale/locale.constant-en_US.json
  28. 1
      ui/src/app/locale/locale.constant-ru_RU.json
  29. 1
      ui/src/app/locale/locale.constant-uk_UA.json
  30. 8
      ui/src/app/login/login.controller.js
  31. 2
      ui/src/app/login/login.routes.js

123
application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql

@ -0,0 +1,123 @@
--
-- 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.
--
CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp;
partition_by_max_ttl_date varchar;
partition_month varchar;
partition_day varchar;
partition_year varchar;
partition varchar;
partition_to_delete varchar;
BEGIN
SELECT max(attribute_kv.long_v)
FROM tenant
INNER JOIN attribute_kv ON tenant.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_tenant_ttl;
SELECT max(attribute_kv.long_v)
FROM customer
INNER JOIN attribute_kv ON customer.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_customer_ttl;
max_ttl := GREATEST(system_ttl, max_customer_ttl, max_tenant_ttl);
if max_ttl IS NOT NULL AND max_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - (max_ttl / 1000));
partition_by_max_ttl_date := get_partition_by_max_ttl_date(partition_type, date);
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
LOOP
IF partition != partition_by_max_ttl_date THEN
IF partition_year IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 3)::integer < partition_year::integer THEN
partition_to_delete := partition;
ELSE
IF partition_month IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 4)::integer < partition_month::integer THEN
partition_to_delete := partition;
ELSE
IF partition_day IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 5)::integer < partition_day::integer THEN
partition_to_delete := partition;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
IF partition_to_delete IS NOT NULL THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete;
EXECUTE format('DROP TABLE %I', partition_to_delete);
deleted := deleted + 1;
END IF;
END LOOP;
END IF;
END IF;
END
$$;
CREATE OR REPLACE FUNCTION get_partition_by_max_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$
BEGIN
CASE
WHEN partition_type = 'DAYS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd');
WHEN partition_type = 'MONTHS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
WHEN partition_type = 'INDEFINITE' THEN
partition := NULL;
ELSE
partition := NULL;
END CASE;
IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT
FROM pg_tables
WHERE schemaname = 'public'
AND tablename = partition) THEN
partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;

125
application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql

@ -0,0 +1,125 @@
--
-- 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.
--
CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS
$$
BEGIN
uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) ||
'-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31),
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

11
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -199,6 +199,7 @@ public class AuthController extends BaseController {
@ResponseBody @ResponseBody
public JsonNode activateUser( public JsonNode activateUser(
@RequestBody JsonNode activateRequest, @RequestBody JsonNode activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
try { try {
String activateToken = activateRequest.get("activateToken").asText(); String activateToken = activateRequest.get("activateToken").asText();
@ -213,10 +214,12 @@ public class AuthController extends BaseController {
String loginUrl = String.format("%s/login", baseUrl); String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail(); String email = user.getEmail();
try { if (sendActivationMail) {
mailService.sendAccountActivatedEmail(loginUrl, email); try {
} catch (Exception e) { mailService.sendAccountActivatedEmail(loginUrl, email);
log.info("Unable to send account activation email [{}]", e.getMessage()); } catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
} }
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);

24
application/src/main/java/org/thingsboard/server/service/install/AbstractSqlTsDatabaseUpgradeService.java

@ -47,7 +47,7 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
@Autowired @Autowired
protected InstallScripts installScripts; protected InstallScripts installScripts;
protected abstract void loadSql(Connection conn); protected abstract void loadSql(Connection conn, String fileName);
protected void loadFunctions(Path sqlFile, Connection conn) throws Exception { protected void loadFunctions(Path sqlFile, Connection conn) throws Exception {
String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8); String sql = new String(Files.readAllBytes(sqlFile), StandardCharsets.UTF_8);
@ -70,6 +70,26 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
return versionValid; return versionValid;
} }
protected boolean isOldSchema(Connection conn, long fromVersion) {
boolean isOldSchema = true;
try {
Statement statement = conn.createStatement();
statement.execute("CREATE TABLE IF NOT EXISTS tb_schema_settings ( schema_version bigint NOT NULL, CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version));");
Thread.sleep(1000);
ResultSet resultSet = statement.executeQuery("SELECT schema_version FROM tb_schema_settings;");
if (resultSet.next()) {
isOldSchema = resultSet.getLong(1) <= fromVersion;
} else {
resultSet.close();
statement.execute("INSERT INTO tb_schema_settings (schema_version) VALUES (" + fromVersion + ")");
}
statement.close();
} catch (InterruptedException | SQLException e) {
log.info("Failed to check current PostgreSQL schema due to: {}", e.getMessage());
}
return isOldSchema;
}
protected void executeQuery(Connection conn, String query) { protected void executeQuery(Connection conn, String query) {
try { try {
Statement statement = conn.createStatement(); Statement statement = conn.createStatement();
@ -83,7 +103,7 @@ public abstract class AbstractSqlTsDatabaseUpgradeService {
nextWarning = nextWarning.getNextWarning(); nextWarning = nextWarning.getNextWarning();
} }
} }
Thread.sleep(5000); Thread.sleep(2000);
log.info("Successfully executed query: {}", query); log.info("Successfully executed query: {}", query);
} catch (InterruptedException | SQLException e) { } catch (InterruptedException | SQLException e) {
log.info("Failed to execute query: {} due to: {}", query, e.getMessage()); log.info("Failed to execute query: {} due to: {}", query, e.getMessage());

66
application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java

@ -34,6 +34,8 @@ import java.sql.DriverManager;
public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService { public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeService implements DatabaseTsUpgradeService {
private static final String LOAD_FUNCTIONS_SQL = "schema_update_psql_ts.sql"; private static final String LOAD_FUNCTIONS_SQL = "schema_update_psql_ts.sql";
private static final String LOAD_TTL_FUNCTIONS_SQL = "schema_update_ttl.sql";
private static final String LOAD_DROP_PARTITIONS_FUNCTIONS_SQL = "schema_update_psql_drop_partitions.sql";
private static final String TS_KV_OLD = "ts_kv_old;"; private static final String TS_KV_OLD = "ts_kv_old;";
private static final String TS_KV_LATEST_OLD = "ts_kv_latest_old;"; private static final String TS_KV_LATEST_OLD = "ts_kv_latest_old;";
@ -76,30 +78,39 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!"); throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!");
} else { } else {
log.info("PostgreSQL version is valid!"); log.info("PostgreSQL version is valid!");
log.info("Load upgrade functions ..."); if (isOldSchema(conn, 2004003)) {
loadSql(conn); log.info("Load upgrade functions ...");
log.info("Updating timeseries schema ..."); loadSql(conn, LOAD_FUNCTIONS_SQL);
executeQuery(conn, CALL_CREATE_PARTITION_TS_KV_TABLE); log.info("Updating timeseries schema ...");
executeQuery(conn, CALL_CREATE_PARTITIONS); executeQuery(conn, CALL_CREATE_PARTITION_TS_KV_TABLE);
executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, CALL_CREATE_PARTITIONS);
executeQuery(conn, CALL_INSERT_INTO_DICTIONARY); executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, CALL_INSERT_INTO_TS_KV); executeQuery(conn, CALL_INSERT_INTO_DICTIONARY);
executeQuery(conn, CALL_CREATE_NEW_TS_KV_LATEST_TABLE); executeQuery(conn, CALL_INSERT_INTO_TS_KV);
executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, CALL_CREATE_NEW_TS_KV_LATEST_TABLE);
executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, DROP_TABLE_TS_KV_OLD);
executeQuery(conn, DROP_TABLE_TS_KV_LATEST_OLD); executeQuery(conn, DROP_TABLE_TS_KV_OLD);
executeQuery(conn, DROP_TABLE_TS_KV_LATEST_OLD);
executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITION_TS_KV_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITIONS); executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITION_TS_KV_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, DROP_PROCEDURE_CREATE_PARTITIONS);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY); executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY);
executeQuery(conn, DROP_PROCEDURE_CREATE_NEW_TS_KV_LATEST_TABLE); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, DROP_PROCEDURE_CREATE_NEW_TS_KV_LATEST_TABLE);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN IF NOT EXISTS json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN IF NOT EXISTS json_v json;");
}
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
log.info("Load Drop Partitions functions ...");
loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL);
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000");
log.info("schema timeseries updated!"); log.info("schema timeseries updated!");
} }
@ -110,11 +121,12 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
} }
} }
protected void loadSql(Connection conn) { @Override
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", LOAD_FUNCTIONS_SQL); protected void loadSql(Connection conn, String fileName) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName);
try { try {
loadFunctions(schemaUpdateFile, conn); loadFunctions(schemaUpdateFile, conn);
log.info("Upgrade functions successfully loaded!"); log.info("Functions successfully loaded!");
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage()); log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
} }

54
application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java

@ -39,6 +39,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
private long chunkTimeInterval; private long chunkTimeInterval;
private static final String LOAD_FUNCTIONS_SQL = "schema_update_timescale_ts.sql"; private static final String LOAD_FUNCTIONS_SQL = "schema_update_timescale_ts.sql";
private static final String LOAD_TTL_FUNCTIONS_SQL = "schema_update_ttl.sql";
private static final String TENANT_TS_KV_OLD_TABLE = "tenant_ts_kv_old;"; private static final String TENANT_TS_KV_OLD_TABLE = "tenant_ts_kv_old;";
@ -79,31 +80,37 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!"); throw new RuntimeException("PostgreSQL version should be at least more than 11, please upgrade your PostgreSQL and restart the script!");
} else { } else {
log.info("PostgreSQL version is valid!"); log.info("PostgreSQL version is valid!");
log.info("Load upgrade functions ..."); if (isOldSchema(conn, 2004003)) {
loadSql(conn); log.info("Load upgrade functions ...");
log.info("Updating timescale schema ..."); loadSql(conn, LOAD_FUNCTIONS_SQL);
executeQuery(conn, CALL_CREATE_TS_KV_LATEST_TABLE); log.info("Updating timescale schema ...");
executeQuery(conn, CALL_CREATE_NEW_TENANT_TS_KV_TABLE); executeQuery(conn, CALL_CREATE_TS_KV_LATEST_TABLE);
executeQuery(conn, CALL_CREATE_NEW_TENANT_TS_KV_TABLE);
executeQuery(conn, "SELECT create_hypertable('ts_kv', 'ts', chunk_time_interval => " + chunkTimeInterval + ", if_not_exists => true);"); executeQuery(conn, "SELECT create_hypertable('ts_kv', 'ts', chunk_time_interval => " + chunkTimeInterval + ", if_not_exists => true);");
executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, CALL_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, CALL_INSERT_INTO_DICTIONARY); executeQuery(conn, CALL_INSERT_INTO_DICTIONARY);
executeQuery(conn, CALL_INSERT_INTO_TS_KV); executeQuery(conn, CALL_INSERT_INTO_TS_KV);
executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, CALL_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, DROP_OLD_TENANT_TS_KV_TABLE); executeQuery(conn, DROP_OLD_TENANT_TS_KV_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_LATEST_TABLE); executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_LATEST_TABLE);
executeQuery(conn, DROP_PROCEDURE_CREATE_TENANT_TS_KV_TABLE_COPY); executeQuery(conn, DROP_PROCEDURE_CREATE_TENANT_TS_KV_TABLE_COPY);
executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE); executeQuery(conn, DROP_PROCEDURE_CREATE_TS_KV_DICTIONARY_TABLE);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_DICTIONARY);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TENANT_TS_KV); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TENANT_TS_KV);
executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST); executeQuery(conn, DROP_PROCEDURE_INSERT_INTO_TS_KV_LATEST);
executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv ADD COLUMN IF NOT EXISTS json_v json;");
executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN json_v json;"); executeQuery(conn, "ALTER TABLE ts_kv_latest ADD COLUMN IF NOT EXISTS json_v json;");
}
log.info("Load TTL functions ...");
loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005000");
log.info("schema timescale updated!"); log.info("schema timescale updated!");
} }
} }
@ -113,13 +120,14 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
} }
} }
protected void loadSql(Connection conn) { @Override
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", LOAD_FUNCTIONS_SQL); protected void loadSql(Connection conn, String fileName) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.3", fileName);
try { try {
loadFunctions(schemaUpdateFile, conn); loadFunctions(schemaUpdateFile, conn);
log.info("Upgrade functions successfully loaded!"); log.info("Functions successfully loaded!");
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to load Timescale upgrade functions due to: {}", e.getMessage()); log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
} }
} }
} }

89
application/src/main/java/org/thingsboard/server/service/ttl/AbstractTimeseriesCleanUpService.java

@ -0,0 +1,89 @@
/**
* 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.
*/
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.thingsboard.server.dao.util.PsqlTsAnyDao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
@PsqlTsAnyDao
@Slf4j
public abstract class AbstractTimeseriesCleanUpService {
@Value("${sql.ttl.ts_key_value_ttl}")
protected long systemTtl;
@Value("${sql.ttl.enabled}")
private boolean ttlTaskExecutionEnabled;
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String dbUserName;
@Value("${spring.datasource.password}")
private String dbPassword;
@Scheduled(initialDelayString = "${sql.ttl.execution_interval_ms}", fixedDelayString = "${sql.ttl.execution_interval_ms}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
doCleanUp(conn);
} catch (SQLException e) {
log.error("SQLException occurred during TTL task execution ", e);
}
}
}
protected abstract void doCleanUp(Connection connection);
protected long executeQuery(Connection conn, String query) {
long removed = 0L;
try {
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery(query);
getWarnings(statement);
resultSet.next();
removed = resultSet.getLong(1);
log.debug("Successfully executed query: {}", query);
} catch (SQLException e) {
log.debug("Failed to execute query: {} due to: {}", query, e.getMessage());
}
return removed;
}
private void getWarnings(Statement statement) throws SQLException {
SQLWarning warnings = statement.getWarnings();
if (warnings != null) {
log.debug("{}", warnings.getMessage());
SQLWarning nextWarning = warnings.getNextWarning();
while (nextWarning != null) {
log.debug("{}", nextWarning.getMessage());
nextWarning = nextWarning.getNextWarning();
}
}
}
}

41
application/src/main/java/org/thingsboard/server/service/ttl/PsqlTimeseriesCleanUpService.java

@ -0,0 +1,41 @@
/**
* 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.
*/
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.PsqlTsDao;
import java.sql.Connection;
@PsqlTsDao
@Service
@Slf4j
public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Value("${sql.postgres.ts_key_value_partitioning}")
private String partitionType;
@Override
protected void doCleanUp(Connection connection) {
long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);");
log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved);
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID_STR + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}

35
application/src/main/java/org/thingsboard/server/service/ttl/TimescaleTimeseriesCleanUpService.java

@ -0,0 +1,35 @@
/**
* 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.
*/
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import java.sql.Connection;
@TimescaleDBTsDao
@Service
@Slf4j
public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Override
protected void doCleanUp(Connection connection) {
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID_STR + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}

4
application/src/main/resources/thingsboard.yml

@ -214,6 +214,10 @@ sql:
timescale: timescale:
# Specify Interval size for new data chunks storage. # Specify Interval size for new data chunks storage.
chunk_time_interval: "${SQL_TIMESCALE_CHUNK_TIME_INTERVAL:604800000}" chunk_time_interval: "${SQL_TIMESCALE_CHUNK_TIME_INTERVAL:604800000}"
ttl:
enabled: "${SQL_TTL_ENABLED:true}"
execution_interval_ms: "${SQL_TTL_EXECUTION_INTERVAL:86400000}" # Number of miliseconds
ts_key_value_ttl: "${SQL_TTL_TS_KEY_VALUE_TTL:0}" # Number of seconds
# Actor system parameters # Actor system parameters
actors: actors:

21
common/dao-api/src/main/java/org/thingsboard/server/dao/util/PsqlTsDao.java

@ -0,0 +1,21 @@
/**
* 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.
*/
package org.thingsboard.server.dao.util;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@ConditionalOnExpression("'${database.ts.type}'=='sql' && '${spring.jpa.database-platform}'=='org.hibernate.dialect.PostgreSQLDialect'")
public @interface PsqlTsDao { }

120
dao/src/main/resources/sql/schema-timescale.sql

@ -44,4 +44,122 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest (
dbl_v double precision, dbl_v double precision,
json_v json, json_v json,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );
CREATE TABLE IF NOT EXISTS tb_schema_settings
(
schema_version bigint NOT NULL,
CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version)
);
INSERT INTO tb_schema_settings (schema_version) VALUES (2005000);
CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS
$$
BEGIN
uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) ||
'-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31),
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

271
dao/src/main/resources/sql/schema-ts-psql.sql

@ -14,31 +14,260 @@
-- limitations under the License. -- limitations under the License.
-- --
CREATE TABLE IF NOT EXISTS ts_kv ( CREATE TABLE IF NOT EXISTS ts_kv
entity_id uuid NOT NULL, (
key int NOT NULL, entity_id uuid NOT NULL,
ts bigint NOT NULL, key int NOT NULL,
bool_v boolean, ts bigint NOT NULL,
str_v varchar(10000000), bool_v boolean,
long_v bigint, str_v varchar(10000000),
dbl_v double precision, long_v bigint,
json_v json dbl_v double precision,
json_v json
) PARTITION BY RANGE (ts); ) PARTITION BY RANGE (ts);
CREATE TABLE IF NOT EXISTS ts_kv_latest ( CREATE TABLE IF NOT EXISTS ts_kv_latest
entity_id uuid NOT NULL, (
key int NOT NULL, entity_id uuid NOT NULL,
ts bigint NOT NULL, key int NOT NULL,
bool_v boolean, ts bigint NOT NULL,
str_v varchar(10000000), bool_v boolean,
long_v bigint, str_v varchar(10000000),
dbl_v double precision, long_v bigint,
json_v json, dbl_v double precision,
json_v json,
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key) CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_id, key)
); );
CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( CREATE TABLE IF NOT EXISTS ts_kv_dictionary
key varchar(255) NOT NULL, (
key varchar(255) NOT NULL,
key_id serial UNIQUE, key_id serial UNIQUE,
CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) CONSTRAINT ts_key_id_pkey PRIMARY KEY (key)
); );
CREATE TABLE IF NOT EXISTS tb_schema_settings
(
schema_version bigint NOT NULL,
CONSTRAINT tb_schema_settings_pkey PRIMARY KEY (schema_version)
);
INSERT INTO tb_schema_settings (schema_version) VALUES (2005000);
CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp;
partition_by_max_ttl_date varchar;
partition_month varchar;
partition_day varchar;
partition_year varchar;
partition varchar;
partition_to_delete varchar;
BEGIN
SELECT max(attribute_kv.long_v)
FROM tenant
INNER JOIN attribute_kv ON tenant.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_tenant_ttl;
SELECT max(attribute_kv.long_v)
FROM customer
INNER JOIN attribute_kv ON customer.id = attribute_kv.entity_id
WHERE attribute_kv.attribute_key = 'TTL'
into max_customer_ttl;
max_ttl := GREATEST(system_ttl, max_customer_ttl, max_tenant_ttl);
if max_ttl IS NOT NULL AND max_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - (max_ttl / 1000));
partition_by_max_ttl_date := get_partition_by_max_ttl_date(partition_type, date);
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
LOOP
IF partition != partition_by_max_ttl_date THEN
IF partition_year IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 3)::integer < partition_year::integer THEN
partition_to_delete := partition;
ELSE
IF partition_month IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 4)::integer < partition_month::integer THEN
partition_to_delete := partition;
ELSE
IF partition_day IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 5)::integer < partition_day::integer THEN
partition_to_delete := partition;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
IF partition_to_delete IS NOT NULL THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete;
EXECUTE format('DROP TABLE %I', partition_to_delete);
deleted := deleted + 1;
END IF;
END LOOP;
END IF;
END IF;
END
$$;
CREATE OR REPLACE FUNCTION get_partition_by_max_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$
BEGIN
CASE
WHEN partition_type = 'DAYS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd');
WHEN partition_type = 'MONTHS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
WHEN partition_type = 'INDEFINITE' THEN
partition := NULL;
ELSE
partition := NULL;
END CASE;
IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT
FROM pg_tables
WHERE schemaname = 'public'
AND tablename = partition) THEN
partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS
$$
BEGIN
uuid_id := substring(entity_id, 8, 8) || '-' || substring(entity_id, 4, 4) || '-1' || substring(entity_id, 1, 3) ||
'-' || substring(entity_id, 16, 4) || '-' || substring(entity_id, 20, 12);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_device_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(device.id) as entity_id FROM device WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_asset_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(asset.id) as entity_id FROM asset WHERE tenant_id = %L and customer_id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_customer_records_from_ts_kv(tenant_id varchar, customer_id varchar, ttl bigint,
OUT deleted bigint) AS
$$
BEGIN
EXECUTE format(
'WITH deleted AS (DELETE FROM ts_kv WHERE entity_id IN (SELECT to_uuid(customer.id) as entity_id FROM customer WHERE tenant_id = %L and id = %L) AND ts < %L::bigint RETURNING *) SELECT count(*) FROM deleted',
tenant_id, customer_id, ttl) into deleted;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid varchar(31),
IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
tenant_id_record varchar;
customer_id_record varchar;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
tenant_ttl_ts bigint;
customer_ttl_ts bigint;
BEGIN
OPEN tenant_cursor;
FETCH tenant_cursor INTO tenant_id_record;
WHILE FOUND
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
tenant_id_record, 'TTL') INTO tenant_ttl;
if tenant_ttl IS NULL THEN
tenant_ttl := system_ttl;
END IF;
IF tenant_ttl > 0 THEN
tenant_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - tenant_ttl::bigint * 1000)::bigint;
deleted_for_entities := delete_device_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = %', deleted_for_entities, tenant_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record, null_uuid, tenant_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = %', deleted_for_entities, tenant_id_record;
END IF;
FOR customer_id_record IN
SELECT customer.id AS customer_id FROM customer WHERE customer.tenant_id = tenant_id_record
LOOP
EXECUTE format(
'select attribute_kv.long_v from attribute_kv where attribute_kv.entity_id = %L and attribute_kv.attribute_key = %L',
customer_id_record, 'TTL') INTO customer_ttl;
IF customer_ttl IS NULL THEN
customer_ttl_ts := tenant_ttl_ts;
ELSE
IF customer_ttl > 0 THEN
customer_ttl_ts :=
(EXTRACT(EPOCH FROM current_timestamp) * 1000 -
customer_ttl::bigint * 1000)::bigint;
END IF;
END IF;
IF customer_ttl_ts IS NOT NULL AND customer_ttl_ts > 0 THEN
deleted_for_entities :=
delete_customer_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for customer with id = % where tenant_id = %', deleted_for_entities, customer_id_record, tenant_id_record;
deleted_for_entities :=
delete_device_records_from_ts_kv(tenant_id_record, customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for devices where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
deleted_for_entities := delete_asset_records_from_ts_kv(tenant_id_record,
customer_id_record,
customer_ttl_ts);
deleted := deleted + deleted_for_entities;
RAISE NOTICE '% telemetry removed for assets where tenant_id = % and customer_id = %', deleted_for_entities, tenant_id_record, customer_id_record;
END IF;
END LOOP;
FETCH tenant_cursor INTO tenant_id_record;
END LOOP;
END
$$;

5
ui/src/app/api/entity.service.js

@ -435,6 +435,10 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
if (user.authority === 'CUSTOMER_USER') { if (user.authority === 'CUSTOMER_USER') {
entityId.id = user.customerId; entityId.id = user.customerId;
} }
} else if (entityType === types.aliasEntityType.current_tenant){
let user = userService.getCurrentUser();
entityId.entityType = types.entityType.tenant;
entityId.id = user.tenantId;
} }
return entityId; return entityId;
} }
@ -806,6 +810,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
entityTypes.dashboard = types.entityType.dashboard; entityTypes.dashboard = types.entityType.dashboard;
if (useAliasEntityTypes) { if (useAliasEntityTypes) {
entityTypes.current_customer = types.aliasEntityType.current_customer; entityTypes.current_customer = types.aliasEntityType.current_customer;
entityTypes.current_tenant = types.aliasEntityType.current_tenant;
} }
break; break;
case 'CUSTOMER_USER': case 'CUSTOMER_USER':

5
ui/src/app/api/login.service.js

@ -85,9 +85,12 @@ function LoginService($http, $q) {
return deferred.promise; return deferred.promise;
} }
function activate(activateToken, password) { function activate(activateToken, password, sendActivationMail) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/noauth/activate'; var url = '/api/noauth/activate';
if(sendActivationMail === true || sendActivationMail === false) {
url += '?sendActivationMail=' + sendActivationMail;
}
$http.post(url, {activateToken: activateToken, password: password}).then(function success(response) { $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
deferred.resolve(response); deferred.resolve(response);
}, function fail() { }, function fail() {

7
ui/src/app/common/types.constant.js

@ -408,7 +408,8 @@ export default angular.module('thingsboard.types', [])
} }
}, },
aliasEntityType: { aliasEntityType: {
current_customer: "CURRENT_CUSTOMER" current_customer: "CURRENT_CUSTOMER",
current_tenant: "CURRENT_TENANT"
}, },
entityTypeTranslations: { entityTypeTranslations: {
"DEVICE": { "DEVICE": {
@ -474,6 +475,10 @@ export default angular.module('thingsboard.types', [])
"CURRENT_CUSTOMER": { "CURRENT_CUSTOMER": {
type: 'entity.type-current-customer', type: 'entity.type-current-customer',
list: 'entity.type-current-customer' list: 'entity.type-current-customer'
},
"CURRENT_TENANT": {
type: 'entity.type-current-tenant',
list: 'entity.type-current-tenant'
} }
}, },
entityField: { entityField: {

6
ui/src/app/components/react/json-form-ace-editor.jsx

@ -83,9 +83,9 @@ class ThingsboardAceEditor extends React.Component {
fixAceEditor(editor); fixAceEditor(editor);
} }
onToggleFull(groupId) { onToggleFull() {
this.setState({ isFull: !this.state.isFull }); this.setState({ isFull: !this.state.isFull });
this.props.onToggleFullscreen(groupId); this.props.onToggleFullscreen();
this.updateAceEditorSize = true; this.updateAceEditorSize = true;
} }
@ -140,7 +140,7 @@ class ThingsboardAceEditor extends React.Component {
<div className="title-panel"> <div className="title-panel">
<label>{this.props.mode}</label> <label>{this.props.mode}</label>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/> <FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={() => this.onToggleFull(this.props.groupId)}/> <FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={this.onToggleFull}/>
</div> </div>
<AceEditor mode={this.props.mode} <AceEditor mode={this.props.mode}
height={this.state.isFull ? "100%" : "150px"} height={this.state.isFull ? "100%" : "150px"}

2
ui/src/app/components/react/json-form-array.jsx

@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
} }
let forms = this.props.form.items.map(function(form, index){ let forms = this.props.form.items.map(function(form, index){
var copy = this.copyWithIndex(form, i); var copy = this.copyWithIndex(form, i);
return this.props.builder(copy, this.props.groupId, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this)); }.bind(this));
arrays.push( arrays.push(
<li key={keys[i]} className="list-group-item"> <li key={keys[i]} className="list-group-item">

2
ui/src/app/components/react/json-form-fieldset.jsx

@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
render() { render() {
let forms = this.props.form.items.map(function(form, index){ let forms = this.props.form.items.map(function(form, index){
return this.props.builder(form, this.props.groupId, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this)); }.bind(this));
return ( return (

26
ui/src/app/components/react/json-form-schema-form.jsx

@ -40,9 +40,6 @@ class ThingsboardSchemaForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
groupId: null,
};
this.mapper = { this.mapper = {
'number': ThingsboardNumber, 'number': ThingsboardNumber,
@ -88,15 +85,12 @@ class ThingsboardSchemaForm extends React.Component {
this.props.onIconClick(event); this.props.onIconClick(event);
} }
onToggleFullscreen(groupId) { onToggleFullscreen() {
this.setState({
groupId: groupId
});
this.props.onToggleFullscreen(); this.props.onToggleFullscreen();
} }
builder(form, groupId, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) { builder(form, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) {
var type = form.type; var type = form.type;
let Field = this.mapper[type]; let Field = this.mapper[type];
if(!Field) { if(!Field) {
@ -109,21 +103,21 @@ class ThingsboardSchemaForm extends React.Component {
return null; return null;
} }
} }
return <Field model={model} groupId={groupId} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/> return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
} }
createSchema(theForm, groupId) { createSchema(theForm) {
let merged = utils.merge(this.props.schema, theForm, this.props.ignore, this.props.option); let merged = utils.merge(this.props.schema, theForm, this.props.ignore, this.props.option);
let mapper = this.mapper; let mapper = this.mapper;
if(this.props.mapper) { if(this.props.mapper) {
mapper = _.merge(this.mapper, this.props.mapper); mapper = _.merge(this.mapper, this.props.mapper);
} }
let forms = merged.map(function(form, index) { let forms = merged.map(function(form, index) {
return this.builder(form, groupId, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper); return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper);
}.bind(this)); }.bind(this));
let formClass = 'SchemaForm'; let formClass = 'SchemaForm';
if (this.props.isFullscreen && groupId === this.state.groupId) { if (this.props.isFullscreen) {
formClass += ' SchemaFormFullscreen'; formClass += ' SchemaFormFullscreen';
} }
@ -136,7 +130,7 @@ class ThingsboardSchemaForm extends React.Component {
if(this.props.groupInfoes&&this.props.groupInfoes.length>0){ if(this.props.groupInfoes&&this.props.groupInfoes.length>0){
let content=[]; let content=[];
for(let info of this.props.groupInfoes){ for(let info of this.props.groupInfoes){
let forms = this.createSchema(this.props.form[info.formIndex], info.formIndex); let forms = this.createSchema(this.props.form[info.formIndex]);
let item = <ThingsboardSchemaGroup key={content.length} forms={forms} info={info}></ThingsboardSchemaGroup>; let item = <ThingsboardSchemaGroup key={content.length} forms={forms} info={info}></ThingsboardSchemaGroup>;
content.push(item); content.push(item);
} }
@ -166,8 +160,8 @@ class ThingsboardSchemaGroup extends React.Component{
render() { render() {
let theCla = "pull-right fa fa-chevron-down md-toggle-icon"+(this.state.showGroup?"":" tb-toggled") let theCla = "pull-right fa fa-chevron-down md-toggle-icon"+(this.state.showGroup?"":" tb-toggled")
return (<section className="md-whiteframe-z1" style={{marginTop: '10px'}}> return (<section className="md-whiteframe-z1" style={{marginTop: '10px'}}>
<div className='SchemaGroupname md-button-toggle' onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div> <div className='SchemaGroupname md-button-toggle' onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
<div style={{padding: '20px'}} className={this.state.showGroup?"":"invisible"}>{this.props.forms}</div> <div style={{padding: '20px'}} className={this.state.showGroup?"":"invisible"}>{this.props.forms}</div>
</section>); </section>);
} }
} }

3
ui/src/app/components/react/json-form.scss

@ -24,15 +24,12 @@ $input-label-float-scale: .75 !default;
.tb-fullscreen { .tb-fullscreen {
[name="ReactSchemaForm"] { [name="ReactSchemaForm"] {
.SchemaForm { .SchemaForm {
display: none;
&.SchemaFormFullscreen { &.SchemaFormFullscreen {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
display: block;
> div:not(.fullscreen-form-field) { > div:not(.fullscreen-form-field) {
display: none !important; display: none !important;

2
ui/src/app/components/widget/widget.controller.js

@ -152,7 +152,7 @@ export default function WidgetController($scope, $state, $timeout, $window, $ocL
var entityInfo = getActiveEntityInfo(); var entityInfo = getActiveEntityInfo();
var entityId = entityInfo ? entityInfo.entityId : null; var entityId = entityInfo ? entityInfo.entityId : null;
var entityName = entityInfo ? entityInfo.entityName : null; var entityName = entityInfo ? entityInfo.entityName : null;
var entityLabel = entityInfo && entityInfo.label ? entityInfo.label : null; var entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null;
handleWidgetAction($event, this.descriptor, entityId, entityName, null, entityLabel); handleWidgetAction($event, this.descriptor, entityId, entityName, null, entityLabel);
} }
widgetContext.customHeaderActions.push(headerAction); widgetContext.customHeaderActions.push(headerAction);

4
ui/src/app/dashboard/states/manage-dashboard-states.tpl.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<md-dialog aria-label="{{ 'dashboard.manage-states' | translate }}" style="width: 620px;"> <md-dialog aria-label="{{ 'dashboard.manage-states' | translate }}" style="min-width: 600px;">
<form name="theForm" ng-submit="vm.save()"> <form name="theForm" ng-submit="vm.save()">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
@ -72,7 +72,7 @@
</md-toolbar> </md-toolbar>
<md-table-container> <md-table-container>
<table md-table> <table md-table>
<thead fix-head md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row> <tr md-row>
<th md-column md-order-by="name"><span translate>dashboard.state-name</span></th> <th md-column md-order-by="name"><span translate>dashboard.state-name</span></th>
<th md-column md-order-by="id"><span translate>dashboard.state-id</span></th> <th md-column md-order-by="id"><span translate>dashboard.state-id</span></th>

6
ui/src/app/entity/entity-select.directive.js

@ -22,13 +22,14 @@ import entitySelectTemplate from './entity-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function EntitySelect($compile, $templateCache, entityService) { export default function EntitySelect($compile, $templateCache, entityService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) { var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySelectTemplate); var template = $templateCache.get(entitySelectTemplate);
element.html(template); element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.entityTypeCurrentTenant = types.aliasEntityType.current_tenant;
var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes, scope.useAliasEntityTypes); var entityTypes = entityService.prepareAllowedEntityTypesList(scope.allowedEntityTypes, scope.useAliasEntityTypes);
@ -48,7 +49,8 @@ export default function EntitySelect($compile, $templateCache, entityService) {
scope.updateView = function () { scope.updateView = function () {
if (!scope.disabled) { if (!scope.disabled) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
if (scope.model && scope.model.entityType && scope.model.entityId) { if (scope.model && scope.model.entityType &&
(scope.model.entityId || scope.model.entityType === scope.entityTypeCurrentTenant)) {
if (!value) { if (!value) {
value = {}; value = {};
} }

4
ui/src/app/entity/entity-select.tpl.html

@ -25,11 +25,11 @@
allowed-entity-types="allowedEntityTypes" allowed-entity-types="allowedEntityTypes"
ng-model="model.entityType"> ng-model="model.entityType">
</tb-entity-type-select> </tb-entity-type-select>
<tb-entity-autocomplete flex ng-if="model.entityType" <tb-entity-autocomplete flex ng-if="model.entityType && model.entityType !== entityTypeCurrentTenant"
the-form="theForm" the-form="theForm"
ng-disabled="disabled" ng-disabled="disabled"
tb-required="tbRequired" tb-required="tbRequired"
entity-type="model.entityType" entity-type="model.entityType"
ng-model="model.entityId"> ng-model="model.entityId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
</div> </div>

1
ui/src/app/locale/locale.constant-cs_CZ.json

@ -770,6 +770,7 @@
"list-of-rulenodes": "{ count, plural, 1 {Jeden uzel pravidla} other {Seznam # uzlů pravidel} }", "list-of-rulenodes": "{ count, plural, 1 {Jeden uzel pravidla} other {Seznam # uzlů pravidel} }",
"rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'", "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'",
"type-current-customer": "Stávající zákazník", "type-current-customer": "Stávající zákazník",
"type-current-tenant": "Stávající tenant",
"search": "Vyhledat entity", "search": "Vyhledat entity",
"selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno", "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno",
"entity-name": "Název entity", "entity-name": "Název entity",

1
ui/src/app/locale/locale.constant-en_US.json

@ -811,6 +811,7 @@
"list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }", "list-of-rulenodes": "{ count, plural, 1 {One rule node} other {List of # rule nodes} }",
"rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'", "rulenode-name-starts-with": "Rule nodes whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer", "type-current-customer": "Current Customer",
"type-current-tenant": "Current Tenant",
"search": "Search entities", "search": "Search entities",
"selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected", "selected-entities": "{ count, plural, 1 {1 entity} other {# entities} } selected",
"entity-name": "Entity name", "entity-name": "Entity name",

1
ui/src/app/locale/locale.constant-ru_RU.json

@ -809,6 +809,7 @@
"list-of-rulenodes": "{ count, plural, 1 {Одно правило} other {Список из # правил} }", "list-of-rulenodes": "{ count, plural, 1 {Одно правило} other {Список из # правил} }",
"rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'", "rulenode-name-starts-with": "Правила, чьи названия начинаются с '{{prefix}}'",
"type-current-customer": "Текущий клиент", "type-current-customer": "Текущий клиент",
"type-current-tenant": "Текущий владелец",
"search": "Поиск объектов", "search": "Поиск объектов",
"selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }", "selected-entities": "Выбран(ы) { count, plural, 1 {1 объект} few {# объекта} other {# объектов} }",
"entity-name": "Название объекта", "entity-name": "Название объекта",

1
ui/src/app/locale/locale.constant-uk_UA.json

@ -942,6 +942,7 @@
"list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }", "list-of-rulenodes": "{ count, plural, 1 {Одне правило} other {Список # правил} }",
"rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'", "rulenode-name-starts-with": "Список правил, імена яких починаються '{{prefix}}'",
"type-current-customer": "Поточний клієнт", "type-current-customer": "Поточний клієнт",
"type-current-tenant": "Поточний власник",
"search": "Пошук сутностей", "search": "Пошук сутностей",
"selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано", "selected-entities": "{ count, plural, 1 {1 сутність} other {# сутності} } вибрано",
"entity-name": "Ім'я сутності", "entity-name": "Ім'я сутності",

8
ui/src/app/login/login.controller.js

@ -20,7 +20,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function LoginController(toast, loginService, userService, types, $state/*, $rootScope, $log, $translate*/) { export default function LoginController(toast, loginService, userService, types, $state, $stateParams/*, $rootScope, $log, $translate*/) {
var vm = this; var vm = this;
vm.logoSvg = logoSvg; vm.logoSvg = logoSvg;
@ -32,6 +32,12 @@ export default function LoginController(toast, loginService, userService, types,
vm.login = login; vm.login = login;
if ($stateParams.username && $stateParams.password) {
vm.user.name = $stateParams.username;
vm.user.password = $stateParams.password;
doLogin();
}
function doLogin() { function doLogin() {
loginService.login(vm.user).then(function success(response) { loginService.login(vm.user).then(function success(response) {
var token = response.data.token; var token = response.data.token;

2
ui/src/app/login/login.routes.js

@ -25,7 +25,7 @@ import createPasswordTemplate from './create-password.tpl.html';
/*@ngInject*/ /*@ngInject*/
export default function LoginRoutes($stateProvider) { export default function LoginRoutes($stateProvider) {
$stateProvider.state('login', { $stateProvider.state('login', {
url: '/login', url: '/login?username&password',
module: 'public', module: 'public',
views: { views: {
"@": { "@": {

Loading…
Cancel
Save