67 changed files with 2258 additions and 241 deletions
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* 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.install; |
||||
|
|
||||
|
import org.springframework.context.annotation.Profile; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.dao.util.HsqlDao; |
||||
|
import org.thingsboard.server.dao.util.SqlDao; |
||||
|
|
||||
|
@Service |
||||
|
@HsqlDao |
||||
|
@SqlDao |
||||
|
@Profile("install") |
||||
|
public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService |
||||
|
implements EntityDatabaseSchemaService { |
||||
|
protected HsqlEntityDatabaseSchemaService() { |
||||
|
super("schema-entities-hsql.sql", "schema-entities-idx.sql"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,253 @@ |
|||||
|
package org.thingsboard.server.service.install.migrate; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.annotation.Profile; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
||||
|
import org.thingsboard.server.dao.util.SqlDao; |
||||
|
import org.thingsboard.server.service.install.EntityDatabaseSchemaService; |
||||
|
|
||||
|
import java.sql.Connection; |
||||
|
import java.sql.DriverManager; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
|
||||
|
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.bigintColumn; |
||||
|
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.booleanColumn; |
||||
|
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.doubleColumn; |
||||
|
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.enumToIntColumn; |
||||
|
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.idColumn; |
||||
|
import static org.thingsboard.server.service.install.migrate.CassandraToSqlColumn.stringColumn; |
||||
|
|
||||
|
@Service |
||||
|
@Profile("install") |
||||
|
@SqlDao |
||||
|
@Slf4j |
||||
|
public class CassandraEntitiesToSqlMigrateService implements EntitiesMigrateService { |
||||
|
|
||||
|
@Autowired |
||||
|
private EntityDatabaseSchemaService entityDatabaseSchemaService; |
||||
|
|
||||
|
@Autowired |
||||
|
protected CassandraCluster cluster; |
||||
|
|
||||
|
@Value("${spring.datasource.url}") |
||||
|
protected String dbUrl; |
||||
|
|
||||
|
@Value("${spring.datasource.username}") |
||||
|
protected String dbUserName; |
||||
|
|
||||
|
@Value("${spring.datasource.password}") |
||||
|
protected String dbPassword; |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public void migrate() throws Exception { |
||||
|
log.info("Performing migration of entities data from cassandra to SQL database ..."); |
||||
|
entityDatabaseSchemaService.createDatabaseSchema(); |
||||
|
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { |
||||
|
conn.setAutoCommit(true); |
||||
|
for (CassandraToSqlTable table: tables) { |
||||
|
table.migrateToSql(cluster.getSession(), conn); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.error("Unexpected error during ThingsBoard entities data migration!", e); |
||||
|
throw e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static List<CassandraToSqlTable> tables = Arrays.asList( |
||||
|
new CassandraToSqlTable("admin_settings", |
||||
|
idColumn("id"), |
||||
|
stringColumn("key"), |
||||
|
stringColumn("json_value")), |
||||
|
new CassandraToSqlTable("alarm", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
stringColumn("type"), |
||||
|
idColumn("originator_id"), |
||||
|
enumToIntColumn("originator_type", EntityType.class), |
||||
|
stringColumn("severity"), |
||||
|
stringColumn("status"), |
||||
|
bigintColumn("start_ts"), |
||||
|
bigintColumn("end_ts"), |
||||
|
bigintColumn("ack_ts"), |
||||
|
bigintColumn("clear_ts"), |
||||
|
stringColumn("details", "additional_info"), |
||||
|
booleanColumn("propagate"), |
||||
|
stringColumn("propagate_relation_types")), |
||||
|
new CassandraToSqlTable("asset", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
idColumn("customer_id"), |
||||
|
stringColumn("name"), |
||||
|
stringColumn("type"), |
||||
|
stringColumn("label"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("audit_log_by_tenant_id", "audit_log", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
idColumn("customer_id"), |
||||
|
idColumn("entity_id"), |
||||
|
stringColumn("entity_type"), |
||||
|
stringColumn("entity_name"), |
||||
|
idColumn("user_id"), |
||||
|
stringColumn("user_name"), |
||||
|
stringColumn("action_type"), |
||||
|
stringColumn("action_data"), |
||||
|
stringColumn("action_status"), |
||||
|
stringColumn("action_failure_details")), |
||||
|
new CassandraToSqlTable("attributes_kv_cf", "attribute_kv", |
||||
|
idColumn("entity_id"), |
||||
|
stringColumn("entity_type"), |
||||
|
stringColumn("attribute_type"), |
||||
|
stringColumn("attribute_key"), |
||||
|
booleanColumn("bool_v"), |
||||
|
stringColumn("str_v"), |
||||
|
bigintColumn("long_v"), |
||||
|
doubleColumn("dbl_v"), |
||||
|
stringColumn("json_v"), |
||||
|
bigintColumn("last_update_ts")), |
||||
|
new CassandraToSqlTable("component_descriptor", |
||||
|
idColumn("id"), |
||||
|
stringColumn("type"), |
||||
|
stringColumn("scope"), |
||||
|
stringColumn("name"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("clazz"), |
||||
|
stringColumn("configuration_descriptor"), |
||||
|
stringColumn("actions")), |
||||
|
new CassandraToSqlTable("customer", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
stringColumn("title"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("country"), |
||||
|
stringColumn("state"), |
||||
|
stringColumn("city"), |
||||
|
stringColumn("address"), |
||||
|
stringColumn("address2"), |
||||
|
stringColumn("zip"), |
||||
|
stringColumn("phone"), |
||||
|
stringColumn("email"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("dashboard", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
stringColumn("title"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("assigned_customers"), |
||||
|
stringColumn("configuration")), |
||||
|
new CassandraToSqlTable("device", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
idColumn("customer_id"), |
||||
|
stringColumn("name"), |
||||
|
stringColumn("type"), |
||||
|
stringColumn("label"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("device_credentials", |
||||
|
idColumn("id"), |
||||
|
idColumn("device_id"), |
||||
|
stringColumn("credentials_type"), |
||||
|
stringColumn("credentials_id"), |
||||
|
stringColumn("credentials_value")), |
||||
|
new CassandraToSqlTable("event", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
idColumn("entity_id"), |
||||
|
stringColumn("entity_type"), |
||||
|
stringColumn("event_type"), |
||||
|
stringColumn("event_uid"), |
||||
|
stringColumn("body")), |
||||
|
new CassandraToSqlTable("relation", |
||||
|
idColumn("from_id"), |
||||
|
stringColumn("from_type"), |
||||
|
idColumn("to_id"), |
||||
|
stringColumn("to_type"), |
||||
|
stringColumn("relation_type_group"), |
||||
|
stringColumn("relation_type"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("user", "tb_user", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
idColumn("customer_id"), |
||||
|
stringColumn("email"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("authority"), |
||||
|
stringColumn("first_name"), |
||||
|
stringColumn("last_name"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("tenant", |
||||
|
idColumn("id"), |
||||
|
stringColumn("title"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("region"), |
||||
|
stringColumn("country"), |
||||
|
stringColumn("state"), |
||||
|
stringColumn("city"), |
||||
|
stringColumn("address"), |
||||
|
stringColumn("address2"), |
||||
|
stringColumn("zip"), |
||||
|
stringColumn("phone"), |
||||
|
stringColumn("email"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("user_credentials", |
||||
|
idColumn("id"), |
||||
|
idColumn("user_id"), |
||||
|
booleanColumn("enabled"), |
||||
|
stringColumn("password"), |
||||
|
stringColumn("activate_token"), |
||||
|
stringColumn("reset_token")), |
||||
|
new CassandraToSqlTable("widget_type", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
stringColumn("bundle_alias"), |
||||
|
stringColumn("alias"), |
||||
|
stringColumn("name"), |
||||
|
stringColumn("descriptor")), |
||||
|
new CassandraToSqlTable("widgets_bundle", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
stringColumn("alias"), |
||||
|
stringColumn("title"), |
||||
|
stringColumn("search_text")), |
||||
|
new CassandraToSqlTable("rule_chain", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
stringColumn("name"), |
||||
|
stringColumn("search_text"), |
||||
|
idColumn("first_rule_node_id"), |
||||
|
booleanColumn("root"), |
||||
|
booleanColumn("debug_mode"), |
||||
|
stringColumn("configuration"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("rule_node", |
||||
|
idColumn("id"), |
||||
|
idColumn("rule_chain_id"), |
||||
|
stringColumn("type"), |
||||
|
stringColumn("name"), |
||||
|
booleanColumn("debug_mode"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("configuration"), |
||||
|
stringColumn("additional_info")), |
||||
|
new CassandraToSqlTable("entity_view", |
||||
|
idColumn("id"), |
||||
|
idColumn("tenant_id"), |
||||
|
idColumn("customer_id"), |
||||
|
idColumn("entity_id"), |
||||
|
stringColumn("entity_type"), |
||||
|
stringColumn("name"), |
||||
|
stringColumn("type"), |
||||
|
stringColumn("keys"), |
||||
|
bigintColumn("start_ts"), |
||||
|
bigintColumn("end_ts"), |
||||
|
stringColumn("search_text"), |
||||
|
stringColumn("additional_info")) |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
package org.thingsboard.server.service.install.migrate; |
||||
|
|
||||
|
import com.datastax.driver.core.Row; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.UUIDConverter; |
||||
|
|
||||
|
import java.sql.PreparedStatement; |
||||
|
import java.sql.SQLException; |
||||
|
import java.sql.Types; |
||||
|
|
||||
|
@Data |
||||
|
public class CassandraToSqlColumn { |
||||
|
|
||||
|
private String cassandraColumnName; |
||||
|
private String sqlColumnName; |
||||
|
private CassandraToSqlColumnType type; |
||||
|
private int sqlType; |
||||
|
private Class<? extends Enum> enumClass; |
||||
|
|
||||
|
public static CassandraToSqlColumn idColumn(String name) { |
||||
|
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ID); |
||||
|
} |
||||
|
|
||||
|
public static CassandraToSqlColumn stringColumn(String name) { |
||||
|
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.STRING); |
||||
|
} |
||||
|
|
||||
|
public static CassandraToSqlColumn stringColumn(String cassandraColumnName, String sqlColumnName) { |
||||
|
return new CassandraToSqlColumn(cassandraColumnName, sqlColumnName); |
||||
|
} |
||||
|
|
||||
|
public static CassandraToSqlColumn bigintColumn(String name) { |
||||
|
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BIGINT); |
||||
|
} |
||||
|
|
||||
|
public static CassandraToSqlColumn doubleColumn(String name) { |
||||
|
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.DOUBLE); |
||||
|
} |
||||
|
|
||||
|
public static CassandraToSqlColumn booleanColumn(String name) { |
||||
|
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.BOOLEAN); |
||||
|
} |
||||
|
|
||||
|
public static CassandraToSqlColumn enumToIntColumn(String name, Class<? extends Enum> enumClass) { |
||||
|
return new CassandraToSqlColumn(name, CassandraToSqlColumnType.ENUM_TO_INT, enumClass); |
||||
|
} |
||||
|
|
||||
|
public CassandraToSqlColumn(String columnName) { |
||||
|
this(columnName, columnName, CassandraToSqlColumnType.STRING, null); |
||||
|
} |
||||
|
|
||||
|
public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type) { |
||||
|
this(columnName, columnName, type, null); |
||||
|
} |
||||
|
|
||||
|
public CassandraToSqlColumn(String columnName, CassandraToSqlColumnType type, Class<? extends Enum> enumClass) { |
||||
|
this(columnName, columnName, type, enumClass); |
||||
|
} |
||||
|
|
||||
|
public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName) { |
||||
|
this(cassandraColumnName, sqlColumnName, CassandraToSqlColumnType.STRING, null); |
||||
|
} |
||||
|
|
||||
|
public CassandraToSqlColumn(String cassandraColumnName, String sqlColumnName, CassandraToSqlColumnType type, |
||||
|
Class<? extends Enum> enumClass) { |
||||
|
this.cassandraColumnName = cassandraColumnName; |
||||
|
this.sqlColumnName = sqlColumnName; |
||||
|
this.type = type; |
||||
|
this.enumClass = enumClass; |
||||
|
switch (this.type) { |
||||
|
case ID: |
||||
|
case STRING: |
||||
|
this.sqlType = Types.VARCHAR; |
||||
|
break; |
||||
|
case DOUBLE: |
||||
|
this.sqlType = Types.DOUBLE; |
||||
|
break; |
||||
|
case INTEGER: |
||||
|
case ENUM_TO_INT: |
||||
|
this.sqlType = Types.INTEGER; |
||||
|
break; |
||||
|
case FLOAT: |
||||
|
this.sqlType = Types.FLOAT; |
||||
|
break; |
||||
|
case BIGINT: |
||||
|
this.sqlType = Types.BIGINT; |
||||
|
break; |
||||
|
case BOOLEAN: |
||||
|
this.sqlType = Types.BOOLEAN; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void prepareColumnValue(Row row, PreparedStatement sqlInsertStatement, int index) throws SQLException { |
||||
|
String value = this.getColumnValue(row, index); |
||||
|
this.setColumnValue(sqlInsertStatement, index, value); |
||||
|
} |
||||
|
|
||||
|
private String getColumnValue(Row row, int index) { |
||||
|
if (row.isNull(index)) { |
||||
|
return null; |
||||
|
} else { |
||||
|
switch (this.type) { |
||||
|
case ID: |
||||
|
return UUIDConverter.fromTimeUUID(row.getUUID(index)); |
||||
|
case DOUBLE: |
||||
|
return Double.toString(row.getDouble(index)); |
||||
|
case INTEGER: |
||||
|
return Integer.toString(row.getInt(index)); |
||||
|
case FLOAT: |
||||
|
return Float.toString(row.getFloat(index)); |
||||
|
case BIGINT: |
||||
|
return Long.toString(row.getLong(index)); |
||||
|
case BOOLEAN: |
||||
|
return Boolean.toString(row.getBool(index)); |
||||
|
case STRING: |
||||
|
case ENUM_TO_INT: |
||||
|
default: |
||||
|
return row.getString(index); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void setColumnValue(PreparedStatement sqlInsertStatement, int index, String value) throws SQLException { |
||||
|
if (value == null) { |
||||
|
sqlInsertStatement.setNull(index, this.sqlType); |
||||
|
} else { |
||||
|
switch (this.type) { |
||||
|
case DOUBLE: |
||||
|
sqlInsertStatement.setDouble(index, Double.parseDouble(value)); |
||||
|
break; |
||||
|
case INTEGER: |
||||
|
sqlInsertStatement.setInt(index, Integer.parseInt(value)); |
||||
|
break; |
||||
|
case FLOAT: |
||||
|
sqlInsertStatement.setFloat(index, Float.parseFloat(value)); |
||||
|
break; |
||||
|
case BIGINT: |
||||
|
sqlInsertStatement.setLong(index, Long.parseLong(value)); |
||||
|
break; |
||||
|
case BOOLEAN: |
||||
|
sqlInsertStatement.setBoolean(index, Boolean.parseBoolean(value)); |
||||
|
break; |
||||
|
case ENUM_TO_INT: |
||||
|
Enum enumVal = Enum.valueOf(this.enumClass, value); |
||||
|
int intValue = enumVal.ordinal(); |
||||
|
sqlInsertStatement.setInt(index, intValue); |
||||
|
break; |
||||
|
case STRING: |
||||
|
case ID: |
||||
|
default: |
||||
|
sqlInsertStatement.setString(index, value); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
@ -0,0 +1,12 @@ |
|||||
|
package org.thingsboard.server.service.install.migrate; |
||||
|
|
||||
|
public enum CassandraToSqlColumnType { |
||||
|
ID, |
||||
|
DOUBLE, |
||||
|
INTEGER, |
||||
|
FLOAT, |
||||
|
BIGINT, |
||||
|
BOOLEAN, |
||||
|
STRING, |
||||
|
ENUM_TO_INT |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
package org.thingsboard.server.service.install.migrate; |
||||
|
|
||||
|
import com.datastax.driver.core.ResultSet; |
||||
|
import com.datastax.driver.core.Row; |
||||
|
import com.datastax.driver.core.Session; |
||||
|
import com.datastax.driver.core.SimpleStatement; |
||||
|
import com.datastax.driver.core.Statement; |
||||
|
import lombok.Data; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
import java.sql.Connection; |
||||
|
import java.sql.PreparedStatement; |
||||
|
import java.sql.SQLException; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.Iterator; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
@Slf4j |
||||
|
public class CassandraToSqlTable { |
||||
|
|
||||
|
private String cassandraCf; |
||||
|
private String sqlTableName; |
||||
|
|
||||
|
private List<CassandraToSqlColumn> columns; |
||||
|
|
||||
|
public CassandraToSqlTable(String tableName, CassandraToSqlColumn... columns) { |
||||
|
this(tableName, tableName, columns); |
||||
|
} |
||||
|
|
||||
|
public CassandraToSqlTable(String cassandraCf, String sqlTableName, CassandraToSqlColumn... columns) { |
||||
|
this.cassandraCf = cassandraCf; |
||||
|
this.sqlTableName = sqlTableName; |
||||
|
this.columns = Arrays.asList(columns); |
||||
|
} |
||||
|
|
||||
|
public void migrateToSql(Session session, Connection conn) throws SQLException { |
||||
|
log.info("Migrating data from cassandra '{}' Column Family to '{}' SQL table...", this.cassandraCf, this.sqlTableName); |
||||
|
PreparedStatement sqlInsertStatement = createSqlInsertStatement(conn); |
||||
|
Statement cassandraSelectStatement = createCassandraSelectStatement(); |
||||
|
cassandraSelectStatement.setFetchSize(100); |
||||
|
ResultSet rs = session.execute(cassandraSelectStatement); |
||||
|
Iterator<Row> iter = rs.iterator(); |
||||
|
int rowCounter = 0; |
||||
|
while (iter.hasNext()) { |
||||
|
Row row = iter.next(); |
||||
|
if (row != null) { |
||||
|
this.migrateRowToSql(row, sqlInsertStatement); |
||||
|
rowCounter++; |
||||
|
if (rowCounter % 100 == 0) { |
||||
|
sqlInsertStatement.executeBatch(); |
||||
|
log.info("{} records migrated so far...", rowCounter); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (rowCounter % 100 > 0) { |
||||
|
sqlInsertStatement.executeBatch(); |
||||
|
} |
||||
|
sqlInsertStatement.close(); |
||||
|
log.info("{} total records migrated.", rowCounter); |
||||
|
log.info("Finished migration data from cassandra '{}' Column Family to '{}' SQL table.", this.cassandraCf, this.sqlTableName); |
||||
|
} |
||||
|
|
||||
|
private void migrateRowToSql(Row row, PreparedStatement sqlInsertStatement) throws SQLException { |
||||
|
for (int i=0; i<this.columns.size();i++) { |
||||
|
CassandraToSqlColumn column = this.columns.get(i); |
||||
|
column.prepareColumnValue(row, sqlInsertStatement, i); |
||||
|
} |
||||
|
sqlInsertStatement.addBatch(); |
||||
|
} |
||||
|
|
||||
|
private Statement createCassandraSelectStatement() { |
||||
|
StringBuilder selectStatementBuilder = new StringBuilder(); |
||||
|
selectStatementBuilder.append("SELECT "); |
||||
|
for (CassandraToSqlColumn column : columns) { |
||||
|
selectStatementBuilder.append(column.getCassandraColumnName()).append(","); |
||||
|
} |
||||
|
selectStatementBuilder.deleteCharAt(selectStatementBuilder.length() - 1); |
||||
|
selectStatementBuilder.append(" FROM ").append(cassandraCf); |
||||
|
return new SimpleStatement(selectStatementBuilder.toString()); |
||||
|
} |
||||
|
|
||||
|
private PreparedStatement createSqlInsertStatement(Connection conn) throws SQLException { |
||||
|
StringBuilder insertStatementBuilder = new StringBuilder(); |
||||
|
insertStatementBuilder.append("INSERT INTO ").append(this.sqlTableName).append(" ("); |
||||
|
for (CassandraToSqlColumn column : columns) { |
||||
|
insertStatementBuilder.append(column.getSqlColumnName()).append(","); |
||||
|
} |
||||
|
insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); |
||||
|
insertStatementBuilder.append(") VALUES ("); |
||||
|
for (CassandraToSqlColumn ignored : columns) { |
||||
|
insertStatementBuilder.append("?").append(","); |
||||
|
} |
||||
|
insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); |
||||
|
insertStatementBuilder.append(")"); |
||||
|
return conn.prepareStatement(insertStatementBuilder.toString()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
package org.thingsboard.server.service.install.migrate; |
||||
|
|
||||
|
public interface EntitiesMigrateService { |
||||
|
|
||||
|
void migrate() throws Exception; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<form #formContainer class="tb-multiple-input" |
||||
|
#multipleInputForm="ngForm" |
||||
|
[formGroup]="multipleInputFormGroup" |
||||
|
tb-toast toastTarget="{{ toastTargetId }}" |
||||
|
(ngSubmit)="save()" novalidate autocomplete="off"> |
||||
|
<div style="padding: 0 8px;" *ngIf="entityDetected && isAllParametersValid"> |
||||
|
<fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}"> |
||||
|
<legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }} |
||||
|
</legend> |
||||
|
<div fxLayout="row" class="layout-wrap" |
||||
|
[ngClass]="{'vertical-alignment': isVerticalAlignment || changeAlignment}"> |
||||
|
<div *ngFor="let key of visibleKeys(source)" |
||||
|
[ngStyle]="{width: (isVerticalAlignment || changeAlignment) ? '100%' : inputWidthSettings}"> |
||||
|
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'string'"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label>{{key.label}}</mat-label> |
||||
|
<input matInput |
||||
|
formControlName="{{key.formId}}" |
||||
|
[required]="key.settings.required" |
||||
|
[readonly]="key.settings.isEditable === 'readonly'" |
||||
|
type="text" |
||||
|
(focus)="key.isFocused = true; focusInputElement($event)" |
||||
|
(blur)="key.isFocused = false; inputChanged(source, key)"> |
||||
|
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon> |
||||
|
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> |
||||
|
{{key.settings.requiredErrorMessage}} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'double' || |
||||
|
key.settings.dataKeyValueType === 'integer'"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label>{{key.label}}</mat-label> |
||||
|
<input matInput |
||||
|
formControlName="{{key.formId}}" |
||||
|
[required]="key.settings.required" |
||||
|
[readonly]="key.settings.isEditable === 'readonly'" |
||||
|
type="number" |
||||
|
step="{{key.settings.step}}" |
||||
|
(focus)="key.isFocused = true; focusInputElement($event)" |
||||
|
(blur)="key.isFocused = false; inputChanged(source, key)"> |
||||
|
<mat-icon *ngIf="key.settings.icon" matPrefix>{{key.settings.icon}}</mat-icon> |
||||
|
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> |
||||
|
{{key.settings.requiredErrorMessage}} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'"> |
||||
|
<mat-checkbox formControlName="{{key.formId}}" |
||||
|
(change)="inputChanged(source, key)"> |
||||
|
{{key.label}} |
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
|
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'"> |
||||
|
<mat-slide-toggle formControlName="{{key.formId}}" |
||||
|
(change)="inputChanged(source, key)"> |
||||
|
{{key.label}} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="input-field mat-block date-time-input" *ngIf="(key.settings.dataKeyValueType === 'dateTime') || |
||||
|
(key.settings.dataKeyValueType === 'date') || |
||||
|
(key.settings.dataKeyValueType === 'time')" fxLayout="column"> |
||||
|
<div fxLayout="row" [ngClass]="{'vertically-aligned': smallWidthContainer}" fxLayoutGap="16px"> |
||||
|
<mat-form-field> |
||||
|
<mat-placeholder>{{key.label}}</mat-placeholder> |
||||
|
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}" |
||||
|
openOnFocus="true"></mat-datetimepicker> |
||||
|
<input matInput formControlName="{{key.formId}}" |
||||
|
[required]="key.settings.required" |
||||
|
[readonly]="key.settings.isEditable === 'readonly'" |
||||
|
[matDatetimepicker]="datePicker" |
||||
|
(focus)="key.isFocused = true;" |
||||
|
(blur)="key.isFocused = false;" |
||||
|
(dateChange)="inputChanged(source, key)"> |
||||
|
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')"> |
||||
|
{{key.settings.requiredErrorMessage}} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
<div class="mat-padding" fxLayout="row" fxLayoutAlign="end center" |
||||
|
*ngIf="entityDetected && settings.showActionButtons"> |
||||
|
<button mat-button color="primary" type="button" |
||||
|
(click)="discardAll()" style="max-height: 50px; margin-right:20px;" |
||||
|
[disabled]="!multipleInputForm.dirty"> |
||||
|
{{ 'action.undo' | translate }} |
||||
|
</button> |
||||
|
<button mat-button mat-raised-button color="primary" type="submit" |
||||
|
style="max-height: 50px; margin-right:20px;" |
||||
|
[disabled]="!multipleInputForm.dirty || multipleInputForm.invalid"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-multiple-input__errors" fxLayout="column" fxLayoutAlign="center center" style="height: 100%;" |
||||
|
*ngIf="!entityDetected || !isAllParametersValid"> |
||||
|
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" [fxHide]="entityDetected"> |
||||
|
{{ 'widgets.input-widgets.no-entity-selected' | translate }} |
||||
|
</div> |
||||
|
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" |
||||
|
[fxShow]="entityDetected && !isAllParametersValid"> |
||||
|
{{ 'widgets.input-widgets.not-allowed-entity' | translate }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,75 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
|
||||
|
.tb-multiple-input { |
||||
|
height: 100%; |
||||
|
overflow-x: hidden; |
||||
|
overflow-y: auto; |
||||
|
|
||||
|
.fields-group { |
||||
|
padding: 0 8px; |
||||
|
margin: 10px 0; |
||||
|
border: 1px groove rgba(0, 0, 0, .25); |
||||
|
|
||||
|
legend { |
||||
|
color: rgba(0, 0, 0, .7); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.input-field { |
||||
|
padding-right: 10px; |
||||
|
|
||||
|
mat-form-field { |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
mat-checkbox, |
||||
|
mat-slide-toggle { |
||||
|
display: block; |
||||
|
margin-top: 20px; |
||||
|
margin-bottom: 16px; |
||||
|
white-space: normal; |
||||
|
} |
||||
|
|
||||
|
.date-time-input { |
||||
|
mat-form-field { |
||||
|
width: 100%; |
||||
|
margin: 2px 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.vertical-alignment { |
||||
|
flex-direction: column; |
||||
|
|
||||
|
mat-checkbox, |
||||
|
mat-slide-toggle { |
||||
|
margin-top: 18px; |
||||
|
} |
||||
|
|
||||
|
mat-slide-toggle { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.vertically-aligned { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,483 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Overlay } from '@angular/cdk/overlay'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; |
||||
|
import { IWidgetSubscription } from '@core/api/widget-api.models'; |
||||
|
import { isDefined, isEqual, isUndefined } from '@core/utils'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import * as _moment from 'moment'; |
||||
|
import { FormBuilder, FormGroup, NgForm, ValidatorFn, Validators } from '@angular/forms'; |
||||
|
import { RequestConfig } from '@core/http/http-utils'; |
||||
|
import { AttributeService } from '@core/http/attribute.service'; |
||||
|
import { AttributeData, AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { forkJoin, Observable } from 'rxjs'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
|
||||
|
type FieldAlignment = 'row' | 'column'; |
||||
|
|
||||
|
type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; |
||||
|
type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | |
||||
|
'booleanCheckbox' | 'booleanSwitch' | |
||||
|
'dateTime' | 'date' | 'time'; |
||||
|
type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; |
||||
|
|
||||
|
interface MultipleInputWidgetSettings { |
||||
|
widgetTitle: string; |
||||
|
showActionButtons: boolean; |
||||
|
updateAllValues: boolean; |
||||
|
showResultMessage: boolean; |
||||
|
showGroupTitle: boolean; |
||||
|
groupTitle: string; |
||||
|
fieldsAlignment: FieldAlignment; |
||||
|
fieldsInRow: number; |
||||
|
attributesShared?: boolean; |
||||
|
} |
||||
|
|
||||
|
interface MultipleInputWidgetDataKeySettings { |
||||
|
dataKeyType: MultipleInputWidgetDataKeyType; |
||||
|
dataKeyValueType: MultipleInputWidgetDataKeyValueType; |
||||
|
required: boolean; |
||||
|
isEditable: MultipleInputWidgetDataKeyEditableType; |
||||
|
disabledOnDataKey: string; |
||||
|
dataKeyHidden: boolean; |
||||
|
step: number; |
||||
|
requiredErrorMessage: string; |
||||
|
icon: string; |
||||
|
inputTypeNumber?: boolean; |
||||
|
readOnly?: boolean; |
||||
|
disabledOnCondition?: boolean; |
||||
|
} |
||||
|
|
||||
|
interface MultipleInputWidgetDataKey extends DataKey { |
||||
|
formId?: string; |
||||
|
settings: MultipleInputWidgetDataKeySettings; |
||||
|
isFocused: boolean; |
||||
|
value?: any; |
||||
|
} |
||||
|
|
||||
|
interface MultipleInputWidgetSource { |
||||
|
datasource: Datasource; |
||||
|
keys: MultipleInputWidgetDataKey[]; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-multiple-input-widget ', |
||||
|
templateUrl: './multiple-input-widget.component.html', |
||||
|
styleUrls: ['./multiple-input-widget.component.scss'] |
||||
|
}) |
||||
|
export class MultipleInputWidgetComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
|
||||
|
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>; |
||||
|
@ViewChild('multipleInputForm', {static: true}) multipleInputForm: NgForm; |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
private formResizeListener: any; |
||||
|
private settings: MultipleInputWidgetSettings; |
||||
|
private widgetConfig: WidgetConfig; |
||||
|
private subscription: IWidgetSubscription; |
||||
|
private datasources: Array<Datasource>; |
||||
|
private sources: Array<MultipleInputWidgetSource> = []; |
||||
|
|
||||
|
isVerticalAlignment: boolean; |
||||
|
inputWidthSettings: string; |
||||
|
changeAlignment: boolean; |
||||
|
smallWidthContainer: boolean; |
||||
|
|
||||
|
entityDetected = false; |
||||
|
isAllParametersValid = true; |
||||
|
|
||||
|
multipleInputFormGroup: FormGroup; |
||||
|
|
||||
|
toastTargetId = 'multiple-input-widget' + this.utils.guid(); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private elementRef: ElementRef, |
||||
|
private ngZone: NgZone, |
||||
|
private overlay: Overlay, |
||||
|
private viewContainerRef: ViewContainerRef, |
||||
|
private utils: UtilsService, |
||||
|
private fb: FormBuilder, |
||||
|
private attributeService: AttributeService, |
||||
|
private translate: TranslateService) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.ctx.$scope.multipleInputWidget = this; |
||||
|
this.settings = this.ctx.settings; |
||||
|
this.widgetConfig = this.ctx.widgetConfig; |
||||
|
this.subscription = this.ctx.defaultSubscription; |
||||
|
this.datasources = this.subscription.datasources; |
||||
|
this.initializeConfig(); |
||||
|
this.updateDatasources(); |
||||
|
this.buildForm(); |
||||
|
this.ctx.updateWidgetParams(); |
||||
|
this.formResizeListener = this.resize.bind(this); |
||||
|
// @ts-ignore
|
||||
|
addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.formResizeListener) { |
||||
|
// @ts-ignore
|
||||
|
removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private initializeConfig() { |
||||
|
|
||||
|
if (this.settings.widgetTitle && this.settings.widgetTitle.length) { |
||||
|
this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle); |
||||
|
} else { |
||||
|
this.ctx.widgetTitle = this.ctx.widgetConfig.title; |
||||
|
} |
||||
|
|
||||
|
this.settings.groupTitle = this.settings.groupTitle || '${entityName}'; |
||||
|
|
||||
|
// For backward compatibility
|
||||
|
if (isUndefined(this.settings.showActionButtons)) { |
||||
|
this.settings.showActionButtons = true; |
||||
|
} |
||||
|
if (isUndefined(this.settings.fieldsAlignment)) { |
||||
|
this.settings.fieldsAlignment = 'row'; |
||||
|
} |
||||
|
if (isUndefined(this.settings.fieldsInRow)) { |
||||
|
this.settings.fieldsInRow = 2; |
||||
|
} |
||||
|
// For backward compatibility
|
||||
|
|
||||
|
this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row'); |
||||
|
|
||||
|
if (!this.isVerticalAlignment && this.settings.fieldsInRow) { |
||||
|
this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%'; |
||||
|
} |
||||
|
|
||||
|
this.updateWidgetDisplaying(); |
||||
|
} |
||||
|
|
||||
|
private updateDatasources() { |
||||
|
if (this.datasources && this.datasources.length) { |
||||
|
this.entityDetected = true; |
||||
|
let keyIndex = 0; |
||||
|
this.datasources.forEach((datasource) => { |
||||
|
const source: MultipleInputWidgetSource = { |
||||
|
datasource, |
||||
|
keys: [] |
||||
|
}; |
||||
|
if (datasource.type === DatasourceType.entity) { |
||||
|
datasource.dataKeys.forEach((dataKey: MultipleInputWidgetDataKey) => { |
||||
|
if ((datasource.entityType !== EntityType.DEVICE) && (dataKey.settings.dataKeyType === 'shared')) { |
||||
|
this.isAllParametersValid = false; |
||||
|
} |
||||
|
if (dataKey.units) { |
||||
|
dataKey.label += ' (' + dataKey.units + ')'; |
||||
|
} |
||||
|
dataKey.formId = (++keyIndex)+''; |
||||
|
dataKey.isFocused = false; |
||||
|
|
||||
|
// For backward compatibility
|
||||
|
if (isUndefined(dataKey.settings.dataKeyType)) { |
||||
|
if (this.settings.attributesShared) { |
||||
|
dataKey.settings.dataKeyType = 'shared'; |
||||
|
} else { |
||||
|
dataKey.settings.dataKeyType = 'server'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isUndefined(dataKey.settings.dataKeyValueType)) { |
||||
|
if (dataKey.settings.inputTypeNumber) { |
||||
|
dataKey.settings.dataKeyValueType = 'double'; |
||||
|
} else { |
||||
|
dataKey.settings.dataKeyValueType = 'string'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isUndefined(dataKey.settings.isEditable)) { |
||||
|
if (dataKey.settings.readOnly) { |
||||
|
dataKey.settings.isEditable = 'readonly'; |
||||
|
} else { |
||||
|
dataKey.settings.isEditable = 'editable'; |
||||
|
} |
||||
|
} |
||||
|
// For backward compatibility
|
||||
|
|
||||
|
source.keys.push(dataKey); |
||||
|
}); |
||||
|
} else { |
||||
|
this.entityDetected = false; |
||||
|
} |
||||
|
this.sources.push(source); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private buildForm() { |
||||
|
this.multipleInputFormGroup = this.fb.group({}); |
||||
|
this.sources.forEach((source) => { |
||||
|
for (const key of this.visibleKeys(source)) { |
||||
|
const validators: ValidatorFn[] = []; |
||||
|
if (key.settings.required) { |
||||
|
validators.push(Validators.required); |
||||
|
} |
||||
|
if (key.settings.dataKeyValueType === 'integer') { |
||||
|
validators.push(Validators.pattern(/^-?[0-9]+$/)); |
||||
|
} |
||||
|
const formControl = this.fb.control( |
||||
|
{ value: key.value, |
||||
|
disabled: key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition}, |
||||
|
validators |
||||
|
); |
||||
|
this.multipleInputFormGroup.addControl(key.formId, formControl); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateWidgetData(data: Array<DatasourceData>) { |
||||
|
let dataIndex = 0; |
||||
|
this.sources.forEach((source) => { |
||||
|
source.keys.forEach((key) => { |
||||
|
const keyData = data[dataIndex].data; |
||||
|
if (keyData && keyData.length) { |
||||
|
let value; |
||||
|
switch (key.settings.dataKeyValueType) { |
||||
|
case 'dateTime': |
||||
|
case 'date': |
||||
|
value = _moment(keyData[0][1]).toDate(); |
||||
|
break; |
||||
|
case 'time': |
||||
|
value = _moment().startOf('day').add(keyData[0][1], 'ms').toDate(); |
||||
|
break; |
||||
|
case 'booleanCheckbox': |
||||
|
case 'booleanSwitch': |
||||
|
value = (keyData[0][1] === 'true'); |
||||
|
break; |
||||
|
default: |
||||
|
value = keyData[0][1]; |
||||
|
} |
||||
|
key.value = value; |
||||
|
} |
||||
|
|
||||
|
if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) { |
||||
|
const conditions = data.filter((item) => { |
||||
|
return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey; |
||||
|
}); |
||||
|
if (conditions && conditions.length) { |
||||
|
if (conditions[0].data.length) { |
||||
|
if (conditions[0].data[0][1] === 'false') { |
||||
|
key.settings.disabledOnCondition = true; |
||||
|
} else { |
||||
|
key.settings.disabledOnCondition = !conditions[0].data[0][1]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!key.settings.dataKeyHidden) { |
||||
|
if (key.settings.isEditable === 'disabled' || key.settings.disabledOnCondition) { |
||||
|
this.multipleInputFormGroup.get(key.formId).disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.multipleInputFormGroup.get(key.formId).enable({emitEvent: false}); |
||||
|
} |
||||
|
const dirty = this.multipleInputFormGroup.get(key.formId).dirty; |
||||
|
if (!key.isFocused && !dirty) { |
||||
|
this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
dataIndex++; |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateWidgetDisplaying() { |
||||
|
this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620); |
||||
|
this.smallWidthContainer = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 420); |
||||
|
} |
||||
|
|
||||
|
public onDataUpdated() { |
||||
|
this.ngZone.run(() => { |
||||
|
this.updateWidgetData(this.subscription.data); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private resize() { |
||||
|
this.ngZone.run(() => { |
||||
|
this.updateWidgetDisplaying(); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public getGroupTitle(datasource: Datasource): string { |
||||
|
return this.utils.createLabelFromDatasource(datasource, this.settings.groupTitle); |
||||
|
} |
||||
|
|
||||
|
public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] { |
||||
|
return source.keys.filter(key => !key.settings.dataKeyHidden); |
||||
|
} |
||||
|
|
||||
|
public datePickerType(keyType: MultipleInputWidgetDataKeyValueType): string { |
||||
|
switch (keyType) { |
||||
|
case 'dateTime': |
||||
|
return 'datetime'; |
||||
|
case 'date': |
||||
|
return 'date'; |
||||
|
case 'time': |
||||
|
return 'time'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public focusInputElement($event: Event) { |
||||
|
($event.target as HTMLInputElement).select(); |
||||
|
} |
||||
|
|
||||
|
public inputChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey) { |
||||
|
if (!this.settings.showActionButtons) { |
||||
|
const currentValue = this.multipleInputFormGroup.get(key.formId).value; |
||||
|
if (!key.settings.required || (key.settings.required && isDefined(currentValue))) { |
||||
|
const dataToSave: MultipleInputWidgetSource = { |
||||
|
datasource: source.datasource, |
||||
|
keys: [key] |
||||
|
}; |
||||
|
this.save(dataToSave); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public save(dataToSave?: MultipleInputWidgetSource) { |
||||
|
const config: RequestConfig = { |
||||
|
ignoreLoading: !this.settings.showActionButtons |
||||
|
}; |
||||
|
let data: Array<MultipleInputWidgetSource>; |
||||
|
if (dataToSave) { |
||||
|
data = [dataToSave]; |
||||
|
} else { |
||||
|
data = this.sources; |
||||
|
} |
||||
|
const tasks: Observable<any>[] = []; |
||||
|
data.forEach((toSave) => { |
||||
|
const serverAttributes: AttributeData[] = []; |
||||
|
const sharedAttributes: AttributeData[] = []; |
||||
|
const telemetry: AttributeData[] = []; |
||||
|
for (const key of this.visibleKeys(toSave)) { |
||||
|
const currentValue = this.multipleInputFormGroup.get(key.formId).value; |
||||
|
if (!isEqual(currentValue, key.value) || this.settings.updateAllValues) { |
||||
|
const attribute: AttributeData = { |
||||
|
key: key.name, |
||||
|
value: null |
||||
|
}; |
||||
|
if (currentValue) { |
||||
|
switch (key.settings.dataKeyValueType) { |
||||
|
case 'dateTime': |
||||
|
case 'date': |
||||
|
attribute.value = currentValue.getTime(); |
||||
|
break; |
||||
|
case 'time': |
||||
|
attribute.value = currentValue.getTime() - _moment().startOf('day').valueOf(); |
||||
|
break; |
||||
|
default: |
||||
|
attribute.value = currentValue; |
||||
|
} |
||||
|
} else { |
||||
|
if (currentValue === '') { |
||||
|
attribute.value = null; |
||||
|
} else { |
||||
|
attribute.value = currentValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
switch (key.settings.dataKeyType) { |
||||
|
case 'shared': |
||||
|
sharedAttributes.push(attribute); |
||||
|
break; |
||||
|
case 'timeseries': |
||||
|
telemetry.push(attribute); |
||||
|
break; |
||||
|
default: |
||||
|
serverAttributes.push(attribute); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
const entityId: EntityId = { |
||||
|
entityType: toSave.datasource.entityType, |
||||
|
id: toSave.datasource.entityId |
||||
|
}; |
||||
|
if (serverAttributes.length) { |
||||
|
tasks.push(this.attributeService.saveEntityAttributes( |
||||
|
entityId, |
||||
|
AttributeScope.SERVER_SCOPE, |
||||
|
serverAttributes, |
||||
|
config |
||||
|
)); |
||||
|
} |
||||
|
if (sharedAttributes.length) { |
||||
|
tasks.push(this.attributeService.saveEntityAttributes( |
||||
|
entityId, |
||||
|
AttributeScope.SHARED_SCOPE, |
||||
|
sharedAttributes, |
||||
|
config |
||||
|
)); |
||||
|
} |
||||
|
if (telemetry.length) { |
||||
|
tasks.push(this.attributeService.saveEntityTimeseries( |
||||
|
entityId, |
||||
|
LatestTelemetry.LATEST_TELEMETRY, |
||||
|
telemetry, |
||||
|
config |
||||
|
)); |
||||
|
} |
||||
|
}); |
||||
|
if (tasks.length) { |
||||
|
forkJoin(tasks).subscribe( |
||||
|
() => { |
||||
|
this.multipleInputForm.resetForm(); |
||||
|
this.multipleInputFormGroup.markAsPristine(); |
||||
|
if (this.settings.showResultMessage) { |
||||
|
this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'), |
||||
|
1000, 'bottom', 'left', this.toastTargetId); |
||||
|
} |
||||
|
}, |
||||
|
() => { |
||||
|
if (this.settings.showResultMessage) { |
||||
|
this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'), |
||||
|
'bottom', 'left', this.toastTargetId); |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
this.multipleInputForm.resetForm(); |
||||
|
this.multipleInputFormGroup.markAsPristine(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public discardAll() { |
||||
|
this.multipleInputForm.resetForm(); |
||||
|
this.sources.forEach((source) => { |
||||
|
for (const key of this.visibleKeys(source)) { |
||||
|
this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false}); |
||||
|
} |
||||
|
}); |
||||
|
this.multipleInputFormGroup.markAsPristine(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2>{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<tb-json-object-edit |
||||
|
formControlName="json" |
||||
|
label="{{ 'value.json-value' | translate }}" |
||||
|
validateContent="true" |
||||
|
[required]="true" |
||||
|
[fillHeight]="false"> |
||||
|
</tb-json-object-edit> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || jsonFormGroup.invalid || !jsonFormGroup.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,63 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {Component, Inject, OnInit} from "@angular/core"; |
||||
|
import {DialogComponent} from "@shared/components/dialog.component"; |
||||
|
import {Store} from "@ngrx/store"; |
||||
|
import {AppState} from "@core/core.state"; |
||||
|
import {Router} from "@angular/router"; |
||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; |
||||
|
import {FormBuilder, FormGroup} from "@angular/forms"; |
||||
|
|
||||
|
export interface JsonObjectEdittDialogData { |
||||
|
jsonValue: Object; |
||||
|
title?: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-object-edit-dialog', |
||||
|
templateUrl: './json-object-edit-dialog.component.html', |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEditDialogComponent, Object> |
||||
|
implements OnInit { |
||||
|
|
||||
|
jsonFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData, |
||||
|
public dialogRef: MatDialogRef<JsonObjectEditDialogComponent, Object>, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.jsonFormGroup = this.fb.group({ |
||||
|
json: [this.data.jsonValue, []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(undefined); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
this.dialogRef.close(this.jsonFormGroup.get('json').value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf} from "@angular/core"; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormControl, FormGroupDirective, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, NgForm, |
||||
|
ValidationErrors, |
||||
|
Validator |
||||
|
} from "@angular/forms"; |
||||
|
import {ErrorStateMatcher} from "@angular/material/core"; |
||||
|
|
||||
|
@Directive({ |
||||
|
selector: '[tb-json-to-string]', |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => TbJsonToStringDirective), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => TbJsonToStringDirective), |
||||
|
multi: true, |
||||
|
}, |
||||
|
{ |
||||
|
provide: ErrorStateMatcher, |
||||
|
useExisting: TbJsonToStringDirective |
||||
|
}] |
||||
|
}) |
||||
|
|
||||
|
export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher { |
||||
|
private propagateChange = null; |
||||
|
private parseError: boolean; |
||||
|
private data: any; |
||||
|
|
||||
|
@HostListener('input', ['$event.target.value']) input(newValue: any): void { |
||||
|
try { |
||||
|
this.data = JSON.parse(newValue); |
||||
|
this.parseError = false; |
||||
|
} catch (e) { |
||||
|
this.parseError = true; |
||||
|
} |
||||
|
|
||||
|
this.propagateChange(this.data); |
||||
|
} |
||||
|
|
||||
|
constructor(private render: Renderer2, |
||||
|
private element: ElementRef, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.parseError); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
validate(c: FormControl): ValidationErrors { |
||||
|
return (!this.parseError) ? null : { |
||||
|
invalidJSON: { |
||||
|
valid: false |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(obj: any): void { |
||||
|
if (obj) { |
||||
|
this.data = obj; |
||||
|
this.parseError = false; |
||||
|
this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {Pipe, PipeTransform} from '@angular/core'; |
||||
|
import {isObject, isNumber} from "@core/utils"; |
||||
|
|
||||
|
@Pipe({name: 'tbJson'}) |
||||
|
export class TbJsonPipe implements PipeTransform { |
||||
|
transform(value: any): string { |
||||
|
if (isObject(value)) { |
||||
|
return JSON.stringify(value); |
||||
|
} else if (isNumber(value)) { |
||||
|
return value.toString(); |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
/* |
||||
|
* 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. |
||||
|
*/ |
||||
|
export default angular.module('tbJsonToString', []) |
||||
|
.directive('tbJsonToString', InputJson) |
||||
|
.name; |
||||
|
|
||||
|
function InputJson() { |
||||
|
return { |
||||
|
restrict: 'A', |
||||
|
require: 'ngModel', |
||||
|
link: function(scope, element, attr, ngModelCtrl) { |
||||
|
function into(input) { |
||||
|
try { |
||||
|
ngModelCtrl.$setValidity('invalidJSON', true); |
||||
|
return angular.fromJson(input); |
||||
|
} catch (e) { |
||||
|
ngModelCtrl.$setValidity('invalidJSON', false); |
||||
|
} |
||||
|
} |
||||
|
function out(data) { |
||||
|
try { |
||||
|
ngModelCtrl.$setValidity('invalidJSON', true); |
||||
|
return angular.toJson(data); |
||||
|
} catch (e) { |
||||
|
ngModelCtrl.$setValidity('invalidJSON', false); |
||||
|
} |
||||
|
} |
||||
|
ngModelCtrl.$parsers.push(into); |
||||
|
ngModelCtrl.$formatters.push(out); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -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. |
||||
|
*/ |
||||
|
/* eslint-enable import/no-unresolved, import/default */ |
||||
|
|
||||
|
import './attribute-dialog-edit-json.scss'; |
||||
|
|
||||
|
/*@ngInject*/ |
||||
|
export default function AttributeDialogEditJsonController($mdDialog, types, jsonValue, readOnly) { |
||||
|
|
||||
|
let vm = this; |
||||
|
vm.json = angular.toJson(jsonValue, 4); |
||||
|
vm.readOnly = readOnly; |
||||
|
vm.contentType = types.contentType.JSON.value; |
||||
|
|
||||
|
vm.save = () => { |
||||
|
$mdDialog.hide(angular.fromJson(vm.json)); |
||||
|
}; |
||||
|
|
||||
|
vm.cancel = () => { |
||||
|
$mdDialog.cancel(); |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
.attribute-edit-json-dialog{ |
||||
|
min-width: 400px; |
||||
|
} |
||||
|
|
||||
|
@media (max-width: 425px){ |
||||
|
.attribute-edit-json-dialog { |
||||
|
min-width: 200px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<md-dialog class="attribute-edit-json-dialog"> |
||||
|
<form name="theForm" ng-submit="vm.save()"> |
||||
|
<md-toolbar> |
||||
|
<div class="md-toolbar-tools"> |
||||
|
<h2>{{ 'details.edit-json' | translate }}</h2> |
||||
|
<span flex></span> |
||||
|
<md-button class="md-icon-button" |
||||
|
ng-click="vm.cancel()" |
||||
|
aria-label="{{'action.close'|translate}}"> |
||||
|
<ng-md-icon icon="close" aria-label="Close"></ng-md-icon> |
||||
|
</md-button> |
||||
|
</div> |
||||
|
</md-toolbar> |
||||
|
<md-dialog-content> |
||||
|
<div class="md-dialog-content"> |
||||
|
<tb-json-content |
||||
|
ng-model="vm.json" |
||||
|
label="{{ 'value.json-value' | translate }}" |
||||
|
content-type="vm.contentType" |
||||
|
validate-content="true" |
||||
|
hide-error-toast="true" |
||||
|
fill-height="false" |
||||
|
ng-readonly="vm.readOnly"> |
||||
|
</tb-json-content> |
||||
|
</div> |
||||
|
</md-dialog-content> |
||||
|
<md-dialog-actions layout="row" layout-align="end center" class="action-buttons"> |
||||
|
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit" |
||||
|
class="md-raised md-primary"> |
||||
|
{{'action.save'|translate}} |
||||
|
</md-button> |
||||
|
<md-button id="cancel-btn" ng-disabled="$root.loading" ng-click="vm.cancel()"> |
||||
|
{{'action.cancel'|translate }} |
||||
|
</md-button> |
||||
|
</md-dialog-actions> |
||||
|
</form> |
||||
|
</md-dialog> |
||||
Loading…
Reference in new issue