250 changed files with 4852 additions and 2071 deletions
@ -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; |
||||
@ -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 |
||||
|
$$; |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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 { } |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,76 @@ |
|||||
|
///
|
||||
|
/// 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 { Injectable } from '@angular/core'; |
||||
|
import { TranslateParser } from '@ngx-translate/core'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
|
||||
|
@Injectable({ providedIn: 'root' }) |
||||
|
export class TranslateDefaultParser extends TranslateParser { |
||||
|
templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g; |
||||
|
|
||||
|
// tslint:disable-next-line:ban-types
|
||||
|
public interpolate(expr: string | Function, params?: any): string { |
||||
|
let result: string; |
||||
|
|
||||
|
if (typeof expr === 'string') { |
||||
|
result = this.interpolateString(expr, params); |
||||
|
} else if (typeof expr === 'function') { |
||||
|
result = this.interpolateFunction(expr, params); |
||||
|
if (typeof result === 'string') { |
||||
|
result = this.interpolateString(result, params); |
||||
|
} |
||||
|
} else { |
||||
|
result = expr as string; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
getValue(target: any, key: string): any { |
||||
|
const keys = typeof key === 'string' ? key.split('.') : [key]; |
||||
|
key = ''; |
||||
|
do { |
||||
|
key += keys.shift(); |
||||
|
if (isDefinedAndNotNull(target) && isDefinedAndNotNull(target[key]) && (typeof target[key] === 'object' || !keys.length)) { |
||||
|
target = target[key]; |
||||
|
key = ''; |
||||
|
} else if (!keys.length) { |
||||
|
target = undefined; |
||||
|
} else { |
||||
|
key += '.'; |
||||
|
} |
||||
|
} while (keys.length); |
||||
|
|
||||
|
return target; |
||||
|
} |
||||
|
|
||||
|
// tslint:disable-next-line:ban-types
|
||||
|
private interpolateFunction(fn: Function, params?: any) { |
||||
|
return fn(params); |
||||
|
} |
||||
|
|
||||
|
private interpolateString(expr: string, params?: any) { |
||||
|
if (!params) { |
||||
|
return expr; |
||||
|
} |
||||
|
|
||||
|
return expr.replace(this.templateMatcher, (substring: string, b: string) => { |
||||
|
const r = this.getValue(params, b); |
||||
|
return isDefinedAndNotNull(r) ? r : substring; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -1,349 +1,355 @@ |
|||||
<!--=======================================================================--> |
<!--=======================================================================--> |
||||
<!--=================== New TB 3.0 / Angular 8 Example =================--> |
|
||||
<!--=======================================================================--> |
|
||||
<!--<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"--> |
|
||||
<!-- class="edit-entity-form"--> |
|
||||
<!-- (ngSubmit)="save()" style="width: 600px;">--> |
|
||||
<!-- <mat-toolbar fxLayout="row" color="primary">--> |
|
||||
<!-- <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</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>--> |
|
||||
<!-- <div class="mat-padding" fxLayout="column">--> |
|
||||
<!-- <mat-form-field class="mat-block">--> |
|
||||
<!-- <mat-label>Entity name</mat-label>--> |
|
||||
<!-- <input matInput formControlName="entityName" required>--> |
|
||||
<!-- <mat-error *ngIf="editEntityFormGroup.get('entityName').hasError('required')">--> |
|
||||
<!-- Entity name required--> |
|
||||
<!-- </mat-error>--> |
|
||||
<!-- </mat-form-field>--> |
|
||||
<!-- </div> --> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div mat-dialog-actions fxLayout="row">--> |
|
||||
<!-- <span fxFlex></span>--> |
|
||||
<!-- <button mat-button mat-raised-button color="primary"--> |
|
||||
<!-- type="submit"--> |
|
||||
<!-- [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.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>--> |
|
||||
<!--=======================================================================--> |
|
||||
<!--===== There are two example templates: for edit and add entity =====--> |
<!--===== There are two example templates: for edit and add entity =====--> |
||||
<!--=======================================================================--> |
<!--=======================================================================--> |
||||
<!--======================== Edit entity example ========================--> |
<!--======================== Edit entity example ========================--> |
||||
<!--=======================================================================--> |
<!--=======================================================================--> |
||||
<!-- --> |
<!-- --> |
||||
<!--<md-dialog aria-label="Edit entity">--> |
<!--<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"--> |
||||
<!-- <form name="editEntityForm" class="edit-entity-form" ng-submit="vm.save()">--> |
<!-- (ngSubmit)="save()" class="edit-entity-form">--> |
||||
<!-- <md-toolbar>--> |
<!-- <mat-toolbar fxLayout="row" color="primary">--> |
||||
<!-- <div class="md-toolbar-tools">--> |
<!-- <h2>Edit {{entityType.toLowerCase()}} {{entityName}}</h2>--> |
||||
<!-- <h2>Edit {{vm.entityType.toLowerCase()}} {{vm.entityName}}</h2>--> |
<!-- <span fxFlex></span>--> |
||||
<!-- <span flex></span>--> |
<!-- <button mat-icon-button (click)="cancel()" type="button">--> |
||||
<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> |
<!-- <mat-icon class="material-icons">close</mat-icon>--> |
||||
<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> |
<!-- </button>--> |
||||
<!-- </md-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 fxLayout="column">--> |
||||
|
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Entity Name</mat-label>--> |
||||
|
<!-- <input matInput formControlName="entityName" required readonly="">--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Entity Label</mat-label>--> |
||||
|
<!-- <input matInput formControlName="entityLabel">--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Entity Type</mat-label>--> |
||||
|
<!-- <input matInput formControlName="entityType" readonly>--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Type</mat-label>--> |
||||
|
<!-- <input matInput formControlName="type" readonly>--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div formGroupName="attributes" fxLayout="column">--> |
||||
|
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Latitude</mat-label>--> |
||||
|
<!-- <input type="number" step="any" matInput formControlName="latitude">--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Longitude</mat-label>--> |
||||
|
<!-- <input type="number" step="any" matInput formControlName="longitude">--> |
||||
|
<!-- </mat-form-field>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </md-toolbar>--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- <md-dialog-content>--> |
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
<!-- <div class="md-dialog-content">--> |
<!-- <mat-label>Address</mat-label>--> |
||||
<!-- <div layout="row">--> |
<!-- <input matInput formControlName="address">--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- </mat-form-field>--> |
||||
<!-- <label>Entity Name</label>--> |
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
<!-- <input ng-model="vm.entityName" readonly>--> |
<!-- <mat-label>Owner</mat-label>--> |
||||
<!-- </md-input-container>--> |
<!-- <input matInput formControlName="owner">--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- </mat-form-field>--> |
||||
<!-- <label>Entity Type</label>--> |
<!-- </div>--> |
||||
<!-- <input ng-model="vm.entityType" readonly>--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- </md-input-container>--> |
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- <mat-label>Integer Value</mat-label>--> |
||||
<!-- <label>Type</label>--> |
<!-- <input type="number" step="1" matInput formControlName="number">--> |
||||
<!-- <input ng-model="vm.type" readonly>--> |
<!-- <mat-error *ngIf="editEntityFormGroup.get('attributes').get('number').hasError('pattern')">--> |
||||
<!-- </md-input-container>--> |
<!-- Invalid integer value.--> |
||||
<!-- </div>--> |
<!-- </mat-error>--> |
||||
<!-- <div layout="row">--> |
<!-- </mat-form-field>--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>--> |
||||
<!-- <label>Latitude</label>--> |
<!-- <label class="checkbox-label">Boolean Value</label>--> |
||||
<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> |
<!-- <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">--> |
||||
<!-- </md-input-container>--> |
<!-- {{ (editEntityFormGroup.get('attributes').get('booleanValue').value ? "value.true" : "value.false") | translate }}--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- </mat-checkbox>--> |
||||
<!-- <label>Longitude</label>--> |
|
||||
<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="row">--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Address</label>--> |
|
||||
<!-- <input ng-model="vm.attributes.address">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Owner</label>--> |
|
||||
<!-- <input ng-model="vm.attributes.owner">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="row">--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Integer Value</label>--> |
|
||||
<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> |
|
||||
<!-- <div ng-messages="editEntityForm.integerNumber.$error">--> |
|
||||
<!-- <div ng-message="pattern">Invalid integer value.</div>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> |
|
||||
<!-- <label class="checkbox-label">Boolean Value</label>--> |
|
||||
<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> |
|
||||
<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> |
|
||||
<!-- </md-checkbox>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- <div class="relations-list old-relations">--> |
<!-- </div>--> |
||||
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> |
<!-- </div>--> |
||||
<!-- <div class="body" ng-show="vm.relations.length">--> |
<!-- <div class="relations-list old-relations">--> |
||||
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> |
<!-- <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> |
||||
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> |
<!-- <div class="body" [fxShow]="oldRelations().length">--> |
||||
<!-- <div flex layout="column">--> |
<!-- <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations" --> |
||||
<!-- <div layout="row">--> |
<!-- *ngFor="let relation of oldRelations().controls; let i = index;">--> |
||||
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> |
<!-- <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">--> |
||||
<!-- <label>Direction</label>--> |
<!-- <div fxFlex fxLayout="column">--> |
||||
<!-- <md-select ng-disabled="true" required ng-model="relation.direction">--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> |
<!-- <mat-form-field class="mat-block" style="min-width: 100px;">--> |
||||
<!-- {{ ("relation.search-direction." + direction) | translate}}--> |
<!-- <mat-label>Direction</mat-label>--> |
||||
<!-- </md-option>--> |
<!-- <mat-select formControlName="direction" name="direction">--> |
||||
<!-- </md-select>--> |
<!-- <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">--> |
||||
<!-- </md-input-container>--> |
<!-- {{ ("relation.search-direction." + direction.value) | translate}}--> |
||||
<!-- <tb-relation-type-autocomplete ng-disabled="true" flex class="md-block"--> |
<!-- </mat-option>--> |
||||
<!-- the-form="editEntityForm"--> |
<!-- </mat-select>--> |
||||
<!-- ng-model="relation.relationType"--> |
<!-- <mat-error *ngIf="relation.get('direction').hasError('required')">--> |
||||
<!-- tb-required="true">--> |
<!-- Relation direction is required.--> |
||||
<!-- </tb-relation-type-autocomplete>--> |
<!-- </mat-error>--> |
||||
<!-- </div>--> |
<!-- </mat-form-field>--> |
||||
<!-- <div layout="row">--> |
<!-- <tb-relation-type-autocomplete--> |
||||
<!-- <tb-entity-select flex class="md-block"--> |
<!-- fxFlex class="mat-block"--> |
||||
<!-- the-form="editEntityForm"--> |
<!-- formControlName="relationType"--> |
||||
<!-- ng-disabled="true"--> |
<!-- required="true">--> |
||||
<!-- tb-required="true"--> |
<!-- </tb-relation-type-autocomplete>--> |
||||
<!-- ng-model="relation.relatedEntity">--> |
|
||||
<!-- </tb-entity-select>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="column" layout-align="center center">--> |
|
||||
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> |
|
||||
<!-- ng-click="vm.removeOldRelation($index,relation)" aria-label="Remove">--> |
|
||||
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> |
|
||||
<!-- <md-icon aria-label="Remove" class="material-icons">--> |
|
||||
<!-- close--> |
|
||||
<!-- </md-icon>--> |
|
||||
<!-- </md-button>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
|
<!-- <div fxLayout="row" fxLayout.xs="column">--> |
||||
|
<!-- <tb-entity-select--> |
||||
|
<!-- fxFlex class="mat-block"--> |
||||
|
<!-- required="true"--> |
||||
|
<!-- formControlName="relatedEntity">--> |
||||
|
<!-- </tb-entity-select>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div fxLayout="column" fxLayoutAlign="center center">--> |
||||
|
<!-- <button mat-icon-button color="primary"--> |
||||
|
<!-- aria-label="Remove"--> |
||||
|
<!-- type="button"--> |
||||
|
<!-- (click)="removeOldRelation(i)"--> |
||||
|
<!-- matTooltip="Remove relation"--> |
||||
|
<!-- matTooltipPosition="above">--> |
||||
|
<!-- <mat-icon>close</mat-icon>--> |
||||
|
<!-- </button>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- <div class="relations-list">--> |
<!-- </div>--> |
||||
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>--> |
<!-- </div>--> |
||||
<!-- <div class="body" ng-show="vm.newRelations.length">--> |
<!-- <div class="relations-list">--> |
||||
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.newRelations track by $index">--> |
<!-- <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>--> |
||||
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> |
<!-- <div class="body" [fxShow]="relations().length">--> |
||||
<!-- <div flex layout="column">--> |
<!-- <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">--> |
||||
<!-- <div layout="row">--> |
<!-- <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">--> |
||||
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> |
<!-- <div fxFlex fxLayout="column">--> |
||||
<!-- <label>Direction</label>--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- <md-select name="direction" required ng-model="relation.direction">--> |
<!-- <mat-form-field class="mat-block" style="min-width: 100px;">--> |
||||
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> |
<!-- <mat-label>Direction</mat-label>--> |
||||
<!-- {{ ("relation.search-direction." + direction) | translate}}--> |
<!-- <mat-select formControlName="direction" name="direction">--> |
||||
<!-- </md-option>--> |
<!-- <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">--> |
||||
<!-- </md-select>--> |
<!-- {{ ("relation.search-direction." + direction.value) | translate}}--> |
||||
<!-- <div ng-messages="editEntityForm.direction.$error">--> |
<!-- </mat-option>--> |
||||
<!-- <div ng-message="required">Relation direction is required.</div>--> |
<!-- </mat-select>--> |
||||
<!-- </div>--> |
<!-- <mat-error *ngIf="relation.get('direction').hasError('required')">--> |
||||
<!-- </md-input-container>--> |
<!-- Relation direction is required.--> |
||||
<!-- <tb-relation-type-autocomplete flex class="md-block"--> |
<!-- </mat-error>--> |
||||
<!-- the-form="editEntityForm"--> |
<!-- </mat-form-field>--> |
||||
<!-- ng-model="relation.relationType"--> |
<!-- <tb-relation-type-autocomplete--> |
||||
<!-- tb-required="true">--> |
<!-- fxFlex class="mat-block"--> |
||||
<!-- </tb-relation-type-autocomplete>--> |
<!-- formControlName="relationType"--> |
||||
<!-- </div>--> |
<!-- [required]="true">--> |
||||
<!-- <div layout="row">--> |
<!-- </tb-relation-type-autocomplete>--> |
||||
<!-- <tb-entity-select flex class="md-block"--> |
<!-- </div>--> |
||||
<!-- the-form="editEntityForm"--> |
<!-- <div fxLayout="row" fxLayout.xs="column">--> |
||||
<!-- tb-required="true"--> |
<!-- <tb-entity-select--> |
||||
<!-- ng-model="relation.relatedEntity">--> |
<!-- fxFlex class="mat-block"--> |
||||
<!-- </tb-entity-select>--> |
<!-- [required]="true"--> |
||||
<!-- </div>--> |
<!-- formControlName="relatedEntity">--> |
||||
<!-- </div>--> |
<!-- </tb-entity-select>--> |
||||
<!-- <div layout="column" layout-align="center center">--> |
|
||||
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> |
|
||||
<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> |
|
||||
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> |
|
||||
<!-- <md-icon aria-label="Remove" class="material-icons">--> |
|
||||
<!-- close--> |
|
||||
<!-- </md-icon>--> |
|
||||
<!-- </md-button>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
|
<!-- <div fxLayout="column" fxLayoutAlign="center center">--> |
||||
|
<!-- <button mat-icon-button color="primary"--> |
||||
|
<!-- aria-label="Remove"--> |
||||
|
<!-- type="button"--> |
||||
|
<!-- (click)="removeRelation(i)"--> |
||||
|
<!-- matTooltip="Remove relation"--> |
||||
|
<!-- matTooltipPosition="above">--> |
||||
|
<!-- <mat-icon>close</mat-icon>--> |
||||
|
<!-- </button>--> |
||||
|
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- <div>--> |
|
||||
<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> |
|
||||
<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> |
|
||||
<!-- Add--> |
|
||||
<!-- </md-button>--> |
|
||||
<!-- </div> --> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </md-dialog-content>--> |
<!-- <div>--> |
||||
<!-- <md-dialog-actions>--> |
<!-- <button mat-raised-button color="primary"--> |
||||
<!-- <md-button type="submit" ng-disabled="editEntityForm.$invalid || !editEntityForm.$dirty" class="md-raised md-primary">Save</md-button>--> |
<!-- type="button"--> |
||||
<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> |
<!-- (click)="addRelation()"--> |
||||
<!-- </md-dialog-actions>--> |
<!-- matTooltip="Add Relation"--> |
||||
<!-- </form>--> |
<!-- matTooltipPosition="above">--> |
||||
<!--</md-dialog>--> |
<!-- Add--> |
||||
|
<!-- </button>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">--> |
||||
|
<!-- <button mat-button mat-raised-button color="primary"--> |
||||
|
<!-- type="submit"--> |
||||
|
<!-- [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty">--> |
||||
|
<!-- Save--> |
||||
|
<!-- </button>--> |
||||
|
<!-- <button mat-button color="primary"--> |
||||
|
<!-- style="margin-right: 20px;"--> |
||||
|
<!-- type="button"--> |
||||
|
<!-- [disabled]="(isLoading$ | async)"--> |
||||
|
<!-- (click)="cancel()" cdkFocusInitial>--> |
||||
|
<!-- Cancel--> |
||||
|
<!-- </button>--> |
||||
|
<!-- </div>--> |
||||
|
<!--</form>--> |
||||
<!----> |
<!----> |
||||
<!--========================================================================--> |
<!--========================================================================--> |
||||
<!--========================= Add entity example =========================--> |
<!--========================= Add entity example =========================--> |
||||
<!--========================================================================--> |
<!--========================================================================--> |
||||
<!----> |
<!----> |
||||
<!--<md-dialog aria-label="Add entity">--> |
<!--<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"--> |
||||
<!-- <form name="addEntityForm" class="add-entity-form" ng-submit="vm.save()">--> |
<!-- (ngSubmit)="save()" class="add-entity-form">--> |
||||
<!-- <md-toolbar>--> |
<!-- <mat-toolbar fxLayout="row" color="primary">--> |
||||
<!-- <div class="md-toolbar-tools">--> |
<!-- <h2>Add entity</h2>--> |
||||
<!-- <h2>Add entity</h2>--> |
<!-- <span fxFlex></span>--> |
||||
<!-- <span flex></span>--> |
<!-- <button mat-icon-button (click)="cancel()" type="button">--> |
||||
<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> |
<!-- <mat-icon class="material-icons">close</mat-icon>--> |
||||
<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> |
<!-- </button>--> |
||||
<!-- </md-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 fxLayout="column">--> |
||||
|
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Entity Name</mat-label>--> |
||||
|
<!-- <input matInput formControlName="entityName" required>--> |
||||
|
<!-- <mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')">--> |
||||
|
<!-- Entity name is required.--> |
||||
|
<!-- </mat-error>--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Entity Label</mat-label>--> |
||||
|
<!-- <input matInput formControlName="entityLabel" >--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
|
<!-- <tb-entity-type-select--> |
||||
|
<!-- class="mat-block"--> |
||||
|
<!-- formControlName="entityType"--> |
||||
|
<!-- [showLabel]="true"--> |
||||
|
<!-- [allowedEntityTypes]="allowedEntityTypes"--> |
||||
|
<!-- ></tb-entity-type-select>--> |
||||
|
<!-- <tb-entity-subtype-autocomplete--> |
||||
|
<!-- fxFlex *ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'"--> |
||||
|
<!-- class="mat-block"--> |
||||
|
<!-- formControlName="type"--> |
||||
|
<!-- [required]="true"--> |
||||
|
<!-- [entityType]="'ASSET'"--> |
||||
|
<!-- ></tb-entity-subtype-autocomplete>--> |
||||
|
<!-- <tb-entity-subtype-autocomplete--> |
||||
|
<!-- fxFlex *ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'"--> |
||||
|
<!-- class="mat-block"--> |
||||
|
<!-- formControlName="type"--> |
||||
|
<!-- [required]="true"--> |
||||
|
<!-- [entityType]="'DEVICE'"--> |
||||
|
<!-- ></tb-entity-subtype-autocomplete>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div formGroupName="attributes" fxLayout="column">--> |
||||
|
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Latitude</mat-label>--> |
||||
|
<!-- <input type="number" step="any" matInput formControlName="latitude">--> |
||||
|
<!-- </mat-form-field>--> |
||||
|
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
|
<!-- <mat-label>Longitude</mat-label>--> |
||||
|
<!-- <input type="number" step="any" matInput formControlName="longitude">--> |
||||
|
<!-- </mat-form-field>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </md-toolbar>--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- <md-dialog-content>--> |
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
<!-- <div class="md-dialog-content">--> |
<!-- <mat-label>Address</mat-label>--> |
||||
<!-- <div layout="row">--> |
<!-- <input matInput formControlName="address">--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- </mat-form-field>--> |
||||
<!-- <label>Entity Name</label>--> |
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
<!-- <input ng-model="vm.entityName" name=entityName required>--> |
<!-- <mat-label>Owner</mat-label>--> |
||||
<!-- <div ng-messages="addEntityForm.entityName.$error">--> |
<!-- <input matInput formControlName="owner">--> |
||||
<!-- <div ng-message="required">Entity name is required.</div>--> |
<!-- </mat-form-field>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </md-input-container>--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- <tb-entity-type-select class="md-block" style="min-width: 100px; width: 100px;"--> |
<!-- <mat-form-field fxFlex class="mat-block">--> |
||||
<!-- the-form="addEntityForm"--> |
<!-- <mat-label>Integer Value</mat-label>--> |
||||
<!-- tb-required="true"--> |
<!-- <input type="number" step="1" matInput formControlName="number">--> |
||||
<!-- allowed-entity-types="vm.allowedEntityTypes"--> |
<!-- <mat-error *ngIf="addEntityFormGroup.get('attributes').get('number').hasError('pattern')">--> |
||||
<!-- ng-model="vm.entityType">--> |
<!-- Invalid integer value.--> |
||||
<!-- </tb-entity-type-select>--> |
<!-- </mat-error>--> |
||||
<!-- <md-input-container flex class="md-block">--> |
<!-- </mat-form-field>--> |
||||
<!-- <label>Entity Subtype</label>--> |
<!-- <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>--> |
||||
<!-- <input ng-model="vm.type" name=type required>--> |
<!-- <label class="checkbox-label">Boolean Value</label>--> |
||||
<!-- <div ng-messages="addEntityForm.type.$error">--> |
<!-- <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">--> |
||||
<!-- <div ng-message="required">Entity subtype is required.</div>--> |
<!-- {{ (addEntityFormGroup.get('attributes').get('booleanValue').value ? "value.true" : "value.false") | translate }}--> |
||||
<!-- </div>--> |
<!-- </mat-checkbox>--> |
||||
<!-- </md-input-container>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="row">--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Latitude</label>--> |
|
||||
<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Longitude</label>--> |
|
||||
<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="row">--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Address</label>--> |
|
||||
<!-- <input ng-model="vm.attributes.address">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Owner</label>--> |
|
||||
<!-- <input ng-model="vm.attributes.owner">--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="row">--> |
|
||||
<!-- <md-input-container flex class="md-block">--> |
|
||||
<!-- <label>Integer Value</label>--> |
|
||||
<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> |
|
||||
<!-- <div ng-messages="addEntityForm.integerNumber.$error">--> |
|
||||
<!-- <div ng-message="pattern">Invalid integer value.</div>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </md-input-container>--> |
|
||||
<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> |
|
||||
<!-- <label class="checkbox-label">Boolean Value</label>--> |
|
||||
<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> |
|
||||
<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> |
|
||||
<!-- </md-checkbox>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- <div class="relations-list">--> |
<!-- </div>--> |
||||
<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> |
<!-- </div>--> |
||||
<!-- <div class="body" ng-show="vm.relations.length">--> |
<!-- <div class="relations-list">--> |
||||
<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> |
<!-- <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> |
||||
<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px;">--> |
<!-- <div class="body" [fxShow]="relations().length">--> |
||||
<!-- <div flex layout="column">--> |
<!-- <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;">--> |
||||
<!-- <div layout="row">--> |
<!-- <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;">--> |
||||
<!-- <md-input-container class="md-block" style="min-width: 100px;">--> |
<!-- <div fxFlex fxLayout="column">--> |
||||
<!-- <label>Direction</label>--> |
<!-- <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0">--> |
||||
<!-- <md-select name="direction" required ng-model="relation.direction">--> |
<!-- <mat-form-field class="mat-block" style="min-width: 100px;">--> |
||||
<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> |
<!-- <mat-label>Direction</mat-label>--> |
||||
<!-- {{ ("relation.search-direction." + direction) | translate}}--> |
<!-- <mat-select formControlName="direction" name="direction">--> |
||||
<!-- </md-option>--> |
<!-- <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value">--> |
||||
<!-- </md-select>--> |
<!-- {{ ("relation.search-direction." + direction.value) | translate}}--> |
||||
<!-- <div ng-messages="addEntityForm.direction.$error">--> |
<!-- </mat-option>--> |
||||
<!-- <div ng-message="required">Relation direction is required.</div>--> |
<!-- </mat-select>--> |
||||
<!-- </div>--> |
<!-- <mat-error *ngIf="relation.get('direction').hasError('required')">--> |
||||
<!-- </md-input-container>--> |
<!-- Relation direction is required.--> |
||||
<!-- <tb-relation-type-autocomplete flex class="md-block"--> |
<!-- </mat-error>--> |
||||
<!-- the-form="addEntityForm"--> |
<!-- </mat-form-field>--> |
||||
<!-- ng-model="relation.relationType"--> |
<!-- <tb-relation-type-autocomplete--> |
||||
<!-- tb-required="true">--> |
<!-- fxFlex class="mat-block"--> |
||||
<!-- </tb-relation-type-autocomplete>--> |
<!-- formControlName="relationType"--> |
||||
<!-- </div>--> |
<!-- [required]="true">--> |
||||
<!-- <div layout="row">--> |
<!-- </tb-relation-type-autocomplete>--> |
||||
<!-- <tb-entity-select flex class="md-block"--> |
|
||||
<!-- the-form="addEntityForm"--> |
|
||||
<!-- tb-required="true"--> |
|
||||
<!-- ng-model="relation.relatedEntity">--> |
|
||||
<!-- </tb-entity-select>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- <div layout="column" layout-align="center center">--> |
|
||||
<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> |
|
||||
<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> |
|
||||
<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> |
|
||||
<!-- <md-icon aria-label="Remove" class="material-icons">--> |
|
||||
<!-- close--> |
|
||||
<!-- </md-icon>--> |
|
||||
<!-- </md-button>--> |
|
||||
<!-- </div>--> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
|
<!-- <div fxLayout="row" fxLayout.xs="column">--> |
||||
|
<!-- <tb-entity-select--> |
||||
|
<!-- fxFlex class="mat-block"--> |
||||
|
<!-- [required]="true"--> |
||||
|
<!-- formControlName="relatedEntity">--> |
||||
|
<!-- </tb-entity-select>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div fxLayout="column" fxLayoutAlign="center center">--> |
||||
|
<!-- <button mat-icon-button color="primary"--> |
||||
|
<!-- aria-label="Remove"--> |
||||
|
<!-- type="button"--> |
||||
|
<!-- (click)="removeRelation(i)"--> |
||||
|
<!-- matTooltip="Remove relation"--> |
||||
|
<!-- matTooltipPosition="above">--> |
||||
|
<!-- <mat-icon>close</mat-icon>--> |
||||
|
<!-- </button>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- <div>--> |
|
||||
<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> |
|
||||
<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> |
|
||||
<!-- Add--> |
|
||||
<!-- </md-button>--> |
|
||||
<!-- </div> --> |
|
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </div>--> |
<!-- </div>--> |
||||
<!-- </md-dialog-content>--> |
<!-- <div>--> |
||||
<!-- <md-dialog-actions>--> |
<!-- <button mat-raised-button color="primary"--> |
||||
<!-- <md-button type="submit" ng-disabled="addEntityForm.$invalid || !addEntityForm.$dirty" class="md-raised md-primary">Create</md-button>--> |
<!-- type="button"--> |
||||
<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> |
<!-- (click)="addRelation()"--> |
||||
<!-- </md-dialog-actions>--> |
<!-- matTooltip="Add Relation"--> |
||||
<!-- </form>--> |
<!-- matTooltipPosition="above">--> |
||||
<!--</md-dialog>--> |
<!-- Add--> |
||||
|
<!-- </button>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">--> |
||||
|
<!-- <button mat-button mat-raised-button color="primary"--> |
||||
|
<!-- type="submit"--> |
||||
|
<!-- [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">--> |
||||
|
<!-- Create--> |
||||
|
<!-- </button>--> |
||||
|
<!-- <button mat-button color="primary"--> |
||||
|
<!-- style="margin-right: 20px;"--> |
||||
|
<!-- type="button"--> |
||||
|
<!-- [disabled]="(isLoading$ | async)"--> |
||||
|
<!-- (click)="cancel()" cdkFocusInitial>--> |
||||
|
<!-- Cancel--> |
||||
|
<!-- </button>--> |
||||
|
<!-- </div>--> |
||||
|
<!--</form>--> |
||||
|
|||||
@ -0,0 +1,262 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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="gateway-form" |
||||
|
[formGroup]="gatewayConfigurationGroup" |
||||
|
tb-toast toastTarget="{{ toastTargetId }}" |
||||
|
(ngSubmit)="save()"> |
||||
|
<mat-accordion multi="true" class="mat-body-1"> |
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title">{{ 'gateway.thingsboard' | translate | uppercase }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<tb-entity-gateway-select |
||||
|
formControlName="gateway" |
||||
|
[newGatewayType]="gatewayType" |
||||
|
(gatewayNameExist)="gatewayExist()" |
||||
|
required |
||||
|
> |
||||
|
</tb-entity-gateway-select> |
||||
|
<div fxLayout="column"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.security-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="securityType" > |
||||
|
<mat-option *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key"> |
||||
|
{{ securityType.value.toString() | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.thingsboard-host' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="host"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('host').hasError('required')" translate> |
||||
|
gateway.thingsboard-host-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.thingsboard-port' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="port"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('required')" translate> |
||||
|
gateway.thingsboard-port-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('min')" translate> |
||||
|
gateway.thingsboard-port-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('max')" translate> |
||||
|
gateway.thingsboard-port-max |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('pattern')" translate> |
||||
|
gateway.thingsboard-port-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
<div *ngIf="gatewayConfigurationGroup.get('securityType').value == 'tls'" fxLayout="column"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.tls-path-ca-certificate' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="caCertPath"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.tls-path-private-key' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="privateKeyPath"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.tls-path-client-certificate' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="certPath"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
<mat-checkbox formControlName="remoteConfiguration">{{ 'gateway.remote' | translate }}</mat-checkbox> |
||||
|
|
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.remote-logging-level' | translate }}</mat-label> |
||||
|
<mat-select formControlName="remoteLoggingLevel"> |
||||
|
<mat-option *ngFor="let logLevel of gatewayLogLevels" [value]="logLevel"> |
||||
|
{{ logLevel }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.path-logs' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="remoteLoggingPathToLogs"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('remoteLoggingPathToLogs').hasError('required')" translate> |
||||
|
gateway.path-logs-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
</mat-expansion-panel> |
||||
|
|
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title">{{ 'gateway.storage' | translate | uppercase }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
|
||||
|
<div fxLayout="column"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.storage-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="storageType"> |
||||
|
<mat-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key"> |
||||
|
{{ storageType.value.toString() | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.storage-pack-size' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="readRecordsCount"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('required')" translate> |
||||
|
gateway.storage-pack-size-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('min')" translate> |
||||
|
gateway.storage-pack-size-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('pattern')" translate> |
||||
|
gateway.storage-pack-size-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label > |
||||
|
{{ (gatewayConfigurationGroup.get('storageType').value !== 'file' ? 'gateway.storage-max-records' : 'gateway.storage-max-file-records') | translate}} |
||||
|
</mat-label> |
||||
|
<input matInput type="number" formControlName="maxRecordsCount"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('required')" translate> |
||||
|
gateway.storage-max-records-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('min')" translate> |
||||
|
gateway.storage-max-records-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('pattern')" translate> |
||||
|
gateway.storage-max-records-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" *ngIf="gatewayConfigurationGroup.get('storageType').value == 'file'"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.storage-max-files' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="maxFilesCount"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('required')" translate> |
||||
|
gateway.storage-max-files-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('min')" translate> |
||||
|
gateway.storage-max-files-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('pattern')" translate> |
||||
|
gateway.storage-max-files-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.storage-path' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="dataFolderPath"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('dataFolderPath').hasError('required')" translate> |
||||
|
gateway.storage-path-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-expansion-panel> |
||||
|
|
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title">{{ 'gateway.connectors' | translate | uppercase }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
|
||||
|
<div fxLayout="column" class="gateway-config"> |
||||
|
<section formArrayName="connectors" *ngFor="let connector of connectors.controls; let i = index;"> |
||||
|
<div [formGroupName]="i" fxLayout="row" fxLayoutAlign="space-between stretch" fxLayoutGap="8px"> |
||||
|
<div fxLayout="column" fxLayoutAlign="center start"> |
||||
|
<mat-slide-toggle formControlName="enabled"></mat-slide-toggle> |
||||
|
</div> |
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" fxFlex> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.connector-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="configType" (selectionChange)="changeConnectorType(connector)"> |
||||
|
<mat-option *ngFor="let connectorType of connectorTypes" [value]="connectorType"> |
||||
|
{{ connectorType }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="connector.get('configType').hasError('required')" translate> |
||||
|
gateway.connector-type-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.connector-name' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="name" (blur)="changeConnectorName(connector, i)"> |
||||
|
<mat-error *ngIf="connector.get('name').hasError('required')" translate> |
||||
|
gateway.connector-name-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" |
||||
|
fxLayoutAlign="{{alignment == 'row' ? 'end center' : 'space-evenly center'}}" class="action-buttons"> |
||||
|
<button mat-icon-button (click)="openConfigDialog($event, i, connector.get('config').value, connector.get('name').value)" |
||||
|
matTooltip="{{ 'gateway.update-config' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
[ngClass]="{'mat-warn': connector.get('config').invalid}"> |
||||
|
<mat-icon>more_horiz</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button (click)="removeConnector(i)" |
||||
|
matTooltip="{{ 'gateway.delete' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
<span [fxShow]="!connectors.length" fxLayoutAlign="center center" class="no-data-found">{{'gateway.no-connectors' | translate}}</span> |
||||
|
<div> |
||||
|
<button mat-raised-button type="button" (click)="addNewConnector()" |
||||
|
matTooltip="{{ 'gateway.connector-add' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div > |
||||
|
</mat-expansion-panel> |
||||
|
</mat-accordion> |
||||
|
<section fxLayout="row" fxLayoutAlign="end center" class="form-action-buttons"> |
||||
|
<button mat-raised-button color="primary" type="button" |
||||
|
(click)="exportConfig()" |
||||
|
*ngIf="!gatewayConfigurationGroup.get('remoteConfiguration').value" |
||||
|
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid" |
||||
|
matTooltip="{{'gateway.download-tip' | translate }}"> |
||||
|
{{'action.download' | translate }} |
||||
|
</button> |
||||
|
|
||||
|
<button mat-raised-button color="primary" type="submit" |
||||
|
*ngIf="gatewayConfigurationGroup.get('remoteConfiguration').value" |
||||
|
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid" |
||||
|
matTooltip="{{'gateway.save-tip' | translate }}"> |
||||
|
{{'action.save' | translate }} |
||||
|
</button> |
||||
|
</section> |
||||
|
</form> |
||||
@ -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. |
||||
|
*/ |
||||
|
:host{ |
||||
|
.gateway-form { |
||||
|
height: 100%; |
||||
|
padding: 5px; |
||||
|
background-color: transparent; |
||||
|
overflow-y: auto; |
||||
|
overflow-x: hidden; |
||||
|
|
||||
|
.form-action-buttons{ |
||||
|
padding-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.gateway-config { |
||||
|
.no-data-found { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
height: 40px; |
||||
|
text-transform: uppercase; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
::ng-deep .mat-form-field-infix { |
||||
|
width: auto !important; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,408 @@ |
|||||
|
///
|
||||
|
/// 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, Inject, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { AbstractControl, FormArray, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { |
||||
|
CONFIGURATION_ATTRIBUTE, |
||||
|
CONFIGURATION_DRAFT_ATTRIBUTE, |
||||
|
ConnectorType, |
||||
|
createFormConfig, |
||||
|
CURRENT_CONFIGURATION_ATTRIBUTE, |
||||
|
DEFAULT_CONNECTOR, |
||||
|
gatewayConfigJSON, |
||||
|
GatewayFormConnectorModel, |
||||
|
GatewayFormModels, |
||||
|
GatewayLogLevel, |
||||
|
generateConnectorConfigFiles, |
||||
|
generateLogConfigFile, |
||||
|
generateYAMLConfigFile, |
||||
|
getDraftConnectorsJSON, |
||||
|
getEntityId, |
||||
|
REMOTE_LOGGING_LEVEL_ATTRIBUTE, |
||||
|
SecurityType, |
||||
|
SecurityTypeTranslationMap, |
||||
|
StorageType, |
||||
|
StorageTypeTranslationMap, |
||||
|
ValidateJSON, |
||||
|
WidgetSetting |
||||
|
} from './gateway-form.models'; |
||||
|
import { WINDOW } from '@core/services/window.service'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { |
||||
|
JsonObjectEditDialogComponent, |
||||
|
JsonObjectEditDialogData |
||||
|
} from '@shared/components/dialog/json-object-edit-dialog.component'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DeviceService } from '@core/http/device.service'; |
||||
|
import { AttributeService } from '@core/http/attribute.service'; |
||||
|
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { forkJoin, Observable } from 'rxjs'; |
||||
|
import { tap } from 'rxjs/operators'; |
||||
|
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
||||
|
|
||||
|
// @dynamic
|
||||
|
@Component({ |
||||
|
selector: 'tb-gateway-form', |
||||
|
templateUrl: './gateway-form.component.html', |
||||
|
styleUrls: ['./gateway-form.component.scss'] |
||||
|
}) |
||||
|
|
||||
|
export class GatewayFormComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
constructor( |
||||
|
protected store: Store<AppState>, |
||||
|
private elementRef: ElementRef, |
||||
|
private utils: UtilsService, |
||||
|
private ngZone: NgZone, |
||||
|
private fb: FormBuilder, |
||||
|
@Inject(WINDOW) private window: Window, |
||||
|
private dialog: MatDialog, |
||||
|
private translate: TranslateService, |
||||
|
private deviceService: DeviceService, |
||||
|
private attributeService: AttributeService, |
||||
|
private importExport: ImportExportService |
||||
|
) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
get connectors(): FormArray { |
||||
|
return this.gatewayConfigurationGroup.get('connectors') as FormArray; |
||||
|
} |
||||
|
|
||||
|
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>; |
||||
|
@ViewChild('gatewayConfigurationForm', {static: true}) multipleInputForm: NgForm; |
||||
|
|
||||
|
private successfulSaved: string; |
||||
|
private gatewayNameExists: string; |
||||
|
private archiveFileName: string; |
||||
|
private formResizeListener: any; |
||||
|
private subscribeStorageType$: any; |
||||
|
private subscribeGateway$: any; |
||||
|
|
||||
|
alignment = 'row'; |
||||
|
layoutGap = '5px'; |
||||
|
gatewayType: string; |
||||
|
gatewayConfigurationGroup: FormGroup; |
||||
|
securityTypes = SecurityTypeTranslationMap; |
||||
|
gatewayLogLevels = Object.values(GatewayLogLevel); |
||||
|
connectorTypes = Object.keys(ConnectorType); |
||||
|
storageTypes = StorageTypeTranslationMap; |
||||
|
|
||||
|
toastTargetId = 'gateway-configuration-widget' + this.utils.guid(); |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.initWidgetSettings(this.ctx.settings); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
this.subscribeGateway$.unsubscribe(); |
||||
|
this.subscribeStorageType$.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
private initWidgetSettings(settings: WidgetSetting): void { |
||||
|
let widgetTitle; |
||||
|
if (settings.widgetTitle && settings.widgetTitle.length) { |
||||
|
widgetTitle = this.utils.customTranslation(settings.widgetTitle, settings.widgetTitle); |
||||
|
} else { |
||||
|
widgetTitle = this.translate.instant('gateway.gateway'); |
||||
|
} |
||||
|
this.ctx.widgetTitle = widgetTitle; |
||||
|
|
||||
|
this.archiveFileName = settings.archiveFileName?.length ? settings.archiveFileName : 'gatewayConfiguration'; |
||||
|
this.gatewayType = settings.gatewayType?.length ? settings.gatewayType : 'Gateway'; |
||||
|
this.gatewayNameExists = this.utils.customTranslation(settings.gatewayNameExists, settings.gatewayNameExists) || this.translate.instant('gateway.gateway-exists'); |
||||
|
this.successfulSaved = this.utils.customTranslation(settings.successfulSave, settings.successfulSave) || this.translate.instant('gateway.gateway-saved'); |
||||
|
|
||||
|
this.updateWidgetDisplaying(); |
||||
|
} |
||||
|
|
||||
|
private resize(): void { |
||||
|
this.ngZone.run(() => { |
||||
|
this.updateWidgetDisplaying(); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateWidgetDisplaying(): void { |
||||
|
if(this.ctx.$container && this.ctx.$container[0].offsetWidth <= 425){ |
||||
|
this.layoutGap = '0'; |
||||
|
this.alignment = 'column'; |
||||
|
} else { |
||||
|
this.layoutGap = '5px'; |
||||
|
this.alignment = 'row'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private saveAttribute(attributeName: string, attributeValue: string, attributeScope: AttributeScope): Observable<any> { |
||||
|
const gatewayId = this.gatewayConfigurationGroup.get('gateway').value; |
||||
|
const attributes: AttributeData = { |
||||
|
key: attributeName, |
||||
|
value: attributeValue |
||||
|
}; |
||||
|
return this.attributeService.saveEntityAttributes(getEntityId(gatewayId), attributeScope, [attributes]); |
||||
|
} |
||||
|
|
||||
|
private createConnector(setting: GatewayFormConnectorModel = DEFAULT_CONNECTOR): void { |
||||
|
this.connectors.push(this.fb.group({ |
||||
|
enabled: [setting.enabled], |
||||
|
configType: [setting.configType, [Validators.required]], |
||||
|
name: [setting.name, [Validators.required]], |
||||
|
config: [setting.config, [Validators.nullValidator, ValidateJSON]] |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
private getFormField(name: string): AbstractControl { |
||||
|
return this.gatewayConfigurationGroup.get(name); |
||||
|
} |
||||
|
|
||||
|
private buildForm(): void { |
||||
|
this.gatewayConfigurationGroup = this.fb.group({ |
||||
|
gateway: [null], |
||||
|
accessToken: [null, [Validators.required]], |
||||
|
securityType: [SecurityType.accessToken], |
||||
|
host: [this.window.location.hostname, [Validators.required]], |
||||
|
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
remoteConfiguration: [true], |
||||
|
caCertPath: ['/etc/thingsboard-gateway/ca.pem'], |
||||
|
privateKeyPath: ['/etc/thingsboard-gateway/privateKey.pem'], |
||||
|
certPath: ['/etc/thingsboard-gateway/certificate.pem'], |
||||
|
remoteLoggingLevel: [GatewayLogLevel.debug], |
||||
|
remoteLoggingPathToLogs: ['./logs/', [Validators.required]], |
||||
|
storageType: [StorageType.memory], |
||||
|
readRecordsCount: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
maxRecordsCount: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
maxFilesCount: [5, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
dataFolderPath: ['./data/', [Validators.required]], |
||||
|
connectors: this.fb.array([]) |
||||
|
}); |
||||
|
|
||||
|
this.subscribeStorageType$ = this.getFormField('storageType').valueChanges.subscribe((value: StorageType) => { |
||||
|
if (value === StorageType.memory) { |
||||
|
this.getFormField('maxFilesCount').disable(); |
||||
|
this.getFormField('dataFolderPath').disable(); |
||||
|
} else { |
||||
|
this.getFormField('maxFilesCount').enable(); |
||||
|
this.getFormField('dataFolderPath').enable(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.subscribeGateway$ = this.getFormField('gateway').valueChanges.subscribe((gatewayId: string) => { |
||||
|
if (gatewayId !== null) { |
||||
|
forkJoin([ |
||||
|
this.deviceService.getDeviceCredentials(gatewayId).pipe(tap(deviceCredential => { |
||||
|
this.getFormField('accessToken').patchValue(deviceCredential.credentialsId); |
||||
|
})), |
||||
|
...this.getAttributes(gatewayId)]).subscribe(() => { |
||||
|
this.gatewayConfigurationGroup.markAsPristine(); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} else { |
||||
|
this.getFormField('accessToken').patchValue(''); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
gatewayExist(): void { |
||||
|
this.ctx.showErrorToast(this.gatewayNameExists, 'top', 'left', this.toastTargetId); |
||||
|
} |
||||
|
|
||||
|
exportConfig(): void { |
||||
|
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value; |
||||
|
const filesZip: any = {}; |
||||
|
filesZip['tb_gateway.yaml'] = generateYAMLConfigFile(gatewayConfiguration); |
||||
|
generateConnectorConfigFiles(filesZip, gatewayConfiguration.connectors); |
||||
|
generateLogConfigFile(filesZip, gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs); |
||||
|
this.importExport.exportJSZip(filesZip, this.archiveFileName); |
||||
|
this.saveAttribute( |
||||
|
REMOTE_LOGGING_LEVEL_ATTRIBUTE, |
||||
|
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(), |
||||
|
AttributeScope.SHARED_SCOPE); |
||||
|
} |
||||
|
|
||||
|
addNewConnector(): void { |
||||
|
this.createConnector(); |
||||
|
} |
||||
|
|
||||
|
removeConnector(index: number): void { |
||||
|
if (index > -1) { |
||||
|
this.connectors.removeAt(index); |
||||
|
this.connectors.markAsDirty(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
openConfigDialog($event: Event, index: number, config: object, type: string): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
$event.preventDefault(); |
||||
|
} |
||||
|
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
jsonValue: config, |
||||
|
title: this.translate.instant('gateway.title-connectors-json', {typeName: type}) |
||||
|
} |
||||
|
}).afterClosed().subscribe( |
||||
|
(res) => { |
||||
|
if (res) { |
||||
|
this.connectors.at(index).get('config').patchValue(res); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private createConnectorName(connectors: GatewayFormConnectorModel[], name: string, index: number = 0): string { |
||||
|
const newKeyName = index ? name + index : name; |
||||
|
const indexRes = connectors.findIndex((element) => element.name === newKeyName); |
||||
|
return indexRes === -1 ? newKeyName : this.createConnectorName(connectors, name, ++index); |
||||
|
} |
||||
|
|
||||
|
private validateConnectorName(connectors: GatewayFormConnectorModel[], name: string, connectorIndex: number, index = 0): string { |
||||
|
for (let i = 0; i < connectors.length; i++) { |
||||
|
const nameEq = (index === 0) ? name : name + index; |
||||
|
if (i !== connectorIndex && connectors[i].name === nameEq) { |
||||
|
this.validateConnectorName(connectors, name, connectorIndex, ++index); |
||||
|
} |
||||
|
} |
||||
|
return (index === 0) ? name : name + index; |
||||
|
} |
||||
|
|
||||
|
changeConnectorType(connector: AbstractControl): void { |
||||
|
if (!connector.get('name').value) { |
||||
|
const typeConnector = connector.get('configType').value; |
||||
|
const connectors = this.gatewayConfigurationGroup.value.connectors; |
||||
|
connector.get('name').patchValue(this.createConnectorName(connectors, ConnectorType[typeConnector])); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
changeConnectorName(connector: AbstractControl, index: number): void { |
||||
|
const connectors = this.gatewayConfigurationGroup.value.connectors; |
||||
|
connector.get('name').patchValue(this.validateConnectorName(connectors, connector.get('name').value, index)); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value; |
||||
|
forkJoin([ |
||||
|
this.saveAttribute( |
||||
|
CONFIGURATION_ATTRIBUTE, |
||||
|
window.btoa(JSON.stringify(gatewayConfigJSON(gatewayConfiguration))), |
||||
|
AttributeScope.SHARED_SCOPE), |
||||
|
this.saveAttribute( |
||||
|
CONFIGURATION_DRAFT_ATTRIBUTE, |
||||
|
window.btoa(JSON.stringify(getDraftConnectorsJSON(gatewayConfiguration.connectors))), |
||||
|
AttributeScope.SERVER_SCOPE), |
||||
|
this.saveAttribute( |
||||
|
REMOTE_LOGGING_LEVEL_ATTRIBUTE, |
||||
|
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(), |
||||
|
AttributeScope.SHARED_SCOPE) |
||||
|
]).subscribe(() =>{ |
||||
|
this.ctx.showSuccessToast(this.successfulSaved, |
||||
|
2000, 'top', 'left', this.toastTargetId); |
||||
|
this.gatewayConfigurationGroup.markAsPristine(); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
private getAttributes(gatewayId: string): Array<Observable<Array<AttributeData>>> { |
||||
|
const tasks = []; |
||||
|
tasks.push(forkJoin([this.getAttribute(CURRENT_CONFIGURATION_ATTRIBUTE, AttributeScope.CLIENT_SCOPE, gatewayId), |
||||
|
this.getAttribute(CONFIGURATION_DRAFT_ATTRIBUTE, AttributeScope.SERVER_SCOPE, gatewayId)]).pipe( |
||||
|
tap(([currentConfig, draftConfig]) => { |
||||
|
this.setFormGatewaySettings(currentConfig); |
||||
|
this.setFormConnectorsDraft(draftConfig); |
||||
|
}) |
||||
|
) |
||||
|
); |
||||
|
tasks.push(this.getAttribute(REMOTE_LOGGING_LEVEL_ATTRIBUTE, AttributeScope.SHARED_SCOPE, gatewayId).pipe( |
||||
|
tap(logsLevel => this.processLoggingLevel(logsLevel)) |
||||
|
)); |
||||
|
return tasks; |
||||
|
} |
||||
|
|
||||
|
private getAttribute(attributeName: string, attributeScope: AttributeScope, gatewayId: string): Observable<Array<AttributeData>> { |
||||
|
return this.attributeService.getEntityAttributes(getEntityId(gatewayId), attributeScope, [attributeName]); |
||||
|
} |
||||
|
|
||||
|
private setFormGatewaySettings(response: Array<AttributeData>): void { |
||||
|
this.connectors.clear(); |
||||
|
if (response.length > 0) { |
||||
|
const attribute = JSON.parse(window.atob(response[0].value)); |
||||
|
for (const attributeKey of Object.keys(attribute)) { |
||||
|
const keyValue = attribute[attributeKey]; |
||||
|
if (attributeKey === 'thingsboard') { |
||||
|
if (keyValue !== null && Object.keys(keyValue).length > 0) { |
||||
|
this.gatewayConfigurationGroup.patchValue(createFormConfig(keyValue)); |
||||
|
} |
||||
|
} else { |
||||
|
for (const connector of Object.keys(keyValue)) { |
||||
|
let name = 'No name'; |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue[connector], 'name')) { |
||||
|
name = keyValue[connector].name; |
||||
|
} |
||||
|
const newConnector: GatewayFormConnectorModel = { |
||||
|
enabled: true, |
||||
|
configType: (attributeKey as ConnectorType), |
||||
|
config: keyValue[connector].config, |
||||
|
name |
||||
|
}; |
||||
|
this.createConnector(newConnector); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setFormConnectorsDraft(response: Array<AttributeData>): void { |
||||
|
if (response.length > 0) { |
||||
|
const attribute = JSON.parse(window.atob(response[0].value)); |
||||
|
for (const connectorName of Object.keys(attribute)) { |
||||
|
const newConnector: GatewayFormConnectorModel = { |
||||
|
enabled: false, |
||||
|
configType: (attribute[connectorName].connector as ConnectorType), |
||||
|
config: attribute[connectorName].config, |
||||
|
name: connectorName |
||||
|
}; |
||||
|
this.createConnector(newConnector); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private processLoggingLevel(value: Array<AttributeData>): void { |
||||
|
let logsLevel = GatewayLogLevel.debug; |
||||
|
if (value.length > 0 && GatewayLogLevel[value[0].value.toLowerCase()]) { |
||||
|
logsLevel = GatewayLogLevel[value[0].value.toLowerCase()]; |
||||
|
} |
||||
|
this.getFormField('remoteLoggingLevel').patchValue(logsLevel); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,374 @@ |
|||||
|
///
|
||||
|
/// 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 { AbstractControl, ValidationErrors } from '@angular/forms'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
|
||||
|
export enum SecurityType { |
||||
|
tls = 'tls', |
||||
|
accessToken = 'accessToken' |
||||
|
} |
||||
|
|
||||
|
export interface WidgetSetting { |
||||
|
widgetTitle: string; |
||||
|
archiveFileName: string; |
||||
|
gatewayType: string; |
||||
|
successfulSave: string; |
||||
|
gatewayNameExists: string; |
||||
|
} |
||||
|
|
||||
|
export const CURRENT_CONFIGURATION_ATTRIBUTE = 'current_configuration'; |
||||
|
export const CONFIGURATION_DRAFT_ATTRIBUTE = 'configuration_drafts'; |
||||
|
export const CONFIGURATION_ATTRIBUTE = 'configuration'; |
||||
|
export const REMOTE_LOGGING_LEVEL_ATTRIBUTE = 'RemoteLoggingLevel'; |
||||
|
|
||||
|
export const SecurityTypeTranslationMap = new Map<SecurityType, string>( |
||||
|
[ |
||||
|
[SecurityType.tls, 'gateway.security-types.tls'], |
||||
|
[SecurityType.accessToken, 'gateway.security-types.access-token'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum GatewayLogLevel { |
||||
|
none = 'NONE', |
||||
|
critical = 'CRITICAL', |
||||
|
error = 'ERROR', |
||||
|
warning = 'WARNING', |
||||
|
info = 'INFO', |
||||
|
debug = 'DEBUG' |
||||
|
} |
||||
|
|
||||
|
export enum StorageType { |
||||
|
memory = 'memory', |
||||
|
file = 'file' |
||||
|
} |
||||
|
|
||||
|
export const StorageTypeTranslationMap = new Map<StorageType, string>( |
||||
|
[ |
||||
|
[StorageType.memory, 'gateway.storage-types.memory-storage'], |
||||
|
[StorageType.file, 'gateway.storage-types.file-storage'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum ConnectorType { |
||||
|
mqtt= 'MQTT', |
||||
|
modbus = 'Modbus', |
||||
|
opc_ua = 'OPC-UA', |
||||
|
ble = 'BLE' |
||||
|
} |
||||
|
|
||||
|
export interface GatewayFormModels { |
||||
|
gateway?: string; |
||||
|
accessToken?: string; |
||||
|
securityType?: SecurityType; |
||||
|
host?: string; |
||||
|
port?: number; |
||||
|
remoteConfiguration?: boolean; |
||||
|
caCertPath?: string; |
||||
|
privateKeyPath?: string; |
||||
|
certPath?: string; |
||||
|
remoteLoggingLevel?: GatewayLogLevel; |
||||
|
remoteLoggingPathToLogs?:string; |
||||
|
storageType?: StorageType; |
||||
|
readRecordsCount?: number; |
||||
|
maxRecordsCount?: number; |
||||
|
maxFilesCount?: number; |
||||
|
dataFolderPath?: number; |
||||
|
connectors?: Array<GatewayFormConnectorModel>; |
||||
|
} |
||||
|
|
||||
|
export interface GatewayFormConnectorModel { |
||||
|
config: object; |
||||
|
name: string; |
||||
|
configType: ConnectorType; |
||||
|
enabled: boolean; |
||||
|
} |
||||
|
|
||||
|
export const DEFAULT_CONNECTOR: GatewayFormConnectorModel = { |
||||
|
config: {}, |
||||
|
name: '', |
||||
|
configType: null, |
||||
|
enabled: false |
||||
|
}; |
||||
|
|
||||
|
type Connector = { |
||||
|
[key in ConnectorType]?: Array<ConnectorConfig>; |
||||
|
} |
||||
|
|
||||
|
interface GatewaySetting extends Connector{ |
||||
|
thingsboard: GatewayMainSetting; |
||||
|
} |
||||
|
|
||||
|
interface ConnectorConfig { |
||||
|
name: string; |
||||
|
config: object; |
||||
|
} |
||||
|
|
||||
|
interface GatewayMainSetting { |
||||
|
thingsboard: GatewayMainThingsboardSetting; |
||||
|
connectors: Array<GatewayMainConnector>, |
||||
|
logs: string, |
||||
|
storage: GatewayStorage |
||||
|
} |
||||
|
|
||||
|
interface GatewayMainThingsboardSetting { |
||||
|
host: string, |
||||
|
remoteConfiguration: boolean, |
||||
|
port: number, |
||||
|
security: GatewaySecurity |
||||
|
} |
||||
|
|
||||
|
type GatewaySecurity = SecurityToken | SecurityCertificate; |
||||
|
|
||||
|
interface SecurityToken { |
||||
|
accessToken: string; |
||||
|
} |
||||
|
|
||||
|
interface SecurityCertificate { |
||||
|
caCert: string; |
||||
|
privateKey: string; |
||||
|
cert: string; |
||||
|
} |
||||
|
|
||||
|
type GatewayStorage = GatewayStorageMemory | GatewayStorageFile; |
||||
|
|
||||
|
interface GatewayStorageMemory { |
||||
|
type: string; |
||||
|
max_records_count: number; |
||||
|
read_records_count: number; |
||||
|
} |
||||
|
|
||||
|
interface GatewayStorageFile { |
||||
|
type: string; |
||||
|
data_folder_path: number; |
||||
|
max_file_count: number; |
||||
|
max_read_records_count: number; |
||||
|
max_records_per_file: number; |
||||
|
} |
||||
|
|
||||
|
interface GatewayMainConnector { |
||||
|
configuration: string; |
||||
|
name: string; |
||||
|
type: ConnectorType; |
||||
|
} |
||||
|
|
||||
|
export function ValidateJSON(control: AbstractControl): ValidationErrors | null { |
||||
|
if (JSON.stringify(control.value) === JSON.stringify({})) { |
||||
|
return { validJSON: true }; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const TEMPLATE_LOGS_CONFIG = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"'; |
||||
|
|
||||
|
export function generateYAMLConfigFile(gatewaySetting: GatewayFormModels): string { |
||||
|
let config; |
||||
|
config = 'thingsboard:\n'; |
||||
|
config += ' host: ' + gatewaySetting.host + '\n'; |
||||
|
config += ' remoteConfiguration: ' + gatewaySetting.remoteConfiguration + '\n'; |
||||
|
config += ' port: ' + gatewaySetting.port + '\n'; |
||||
|
config += ' security:\n'; |
||||
|
if (gatewaySetting.securityType === SecurityType.accessToken) { |
||||
|
config += ' access-token: ' + gatewaySetting.accessToken + '\n'; |
||||
|
} else { |
||||
|
config += ' ca_cert: ' + gatewaySetting.caCertPath + '\n'; |
||||
|
config += ' privateKey: ' + gatewaySetting.privateKeyPath + '\n'; |
||||
|
config += ' cert: ' + gatewaySetting.certPath + '\n'; |
||||
|
} |
||||
|
config += 'storage:\n'; |
||||
|
if (gatewaySetting.storageType === StorageType.memory) { |
||||
|
config += ' type: memory\n'; |
||||
|
config += ' read_records_count: ' + gatewaySetting.readRecordsCount + '\n'; |
||||
|
config += ' max_records_count: ' + gatewaySetting.maxRecordsCount + '\n'; |
||||
|
} else { |
||||
|
config += ' type: file\n'; |
||||
|
config += ' data_folder_path: ' + gatewaySetting.dataFolderPath + '\n'; |
||||
|
config += ' max_file_count: ' + gatewaySetting.maxFilesCount + '\n'; |
||||
|
config += ' max_read_records_count: ' + gatewaySetting.readRecordsCount + '\n'; |
||||
|
config += ' max_records_per_file: ' + gatewaySetting.maxRecordsCount + '\n'; |
||||
|
} |
||||
|
config += 'connectors:\n'; |
||||
|
for(const connector of gatewaySetting.connectors){ |
||||
|
if (connector.enabled) { |
||||
|
config += ' -\n'; |
||||
|
config += ' name: ' + connector.name + '\n'; |
||||
|
config += ' type: ' + connector.configType + '\n'; |
||||
|
config += ' configuration: ' + generateFileName(connector.name) + '\n'; |
||||
|
} |
||||
|
} |
||||
|
return config; |
||||
|
} |
||||
|
|
||||
|
export function generateConnectorConfigFiles(fileZipAdd: object, connectors: Array<GatewayFormConnectorModel>): void { |
||||
|
for(const connector of connectors) { |
||||
|
if (connector.enabled) { |
||||
|
fileZipAdd[generateFileName(connector.name)] = JSON.stringify(connector.config); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function generateFileName(fileName): string { |
||||
|
return fileName.replace('_', '') |
||||
|
.replace('-', '') |
||||
|
.replace(/^\s+|\s+/g, '') |
||||
|
.toLowerCase() + '.json'; |
||||
|
} |
||||
|
|
||||
|
export function generateLogConfigFile(fileZipAdd: object, logsLevel: string, logsPath: string): void { |
||||
|
fileZipAdd['logs.conf'] = getLogsConfig(logsLevel, logsPath); |
||||
|
} |
||||
|
|
||||
|
function getLogsConfig(logsLevel: string, logsPath: string): string { |
||||
|
return TEMPLATE_LOGS_CONFIG |
||||
|
.replace(/{ERROR}/g, logsLevel) |
||||
|
.replace(/{.\/logs\/}/g, logsPath); |
||||
|
} |
||||
|
|
||||
|
export function getEntityId(gatewayId: string): EntityId { |
||||
|
return { |
||||
|
id: gatewayId, |
||||
|
entityType: EntityType.DEVICE |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function createFormConfig(keyValue: GatewayMainSetting): GatewayFormModels { |
||||
|
const formSetting: GatewayFormModels = {}; |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) { |
||||
|
formSetting.host = keyValue.thingsboard.host; |
||||
|
formSetting.port = keyValue.thingsboard.port; |
||||
|
formSetting.remoteConfiguration = keyValue.thingsboard.remoteConfiguration; |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, SecurityType.accessToken)) { |
||||
|
formSetting.securityType = SecurityType.accessToken; |
||||
|
formSetting.accessToken = (keyValue.thingsboard.security as SecurityToken).accessToken; |
||||
|
} else { |
||||
|
formSetting.securityType = SecurityType.tls; |
||||
|
formSetting.caCertPath = (keyValue.thingsboard.security as SecurityCertificate).caCert; |
||||
|
formSetting.privateKeyPath = (keyValue.thingsboard.security as SecurityCertificate).privateKey; |
||||
|
formSetting.certPath = (keyValue.thingsboard.security as SecurityCertificate).cert; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) { |
||||
|
if (keyValue.storage.type === StorageType.memory) { |
||||
|
formSetting.storageType = StorageType.memory; |
||||
|
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count; |
||||
|
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count; |
||||
|
} else if (keyValue.storage.type === StorageType.file) { |
||||
|
formSetting.storageType = StorageType.file; |
||||
|
formSetting.dataFolderPath = (keyValue.storage as GatewayStorageFile).data_folder_path; |
||||
|
formSetting.maxFilesCount = (keyValue.storage as GatewayStorageFile).max_file_count; |
||||
|
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count; |
||||
|
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count; |
||||
|
} |
||||
|
} |
||||
|
return formSetting; |
||||
|
} |
||||
|
|
||||
|
export function getDraftConnectorsJSON(currentConnectors: Array<GatewayFormConnectorModel>) { |
||||
|
const draftConnectors = {}; |
||||
|
for(const connector of currentConnectors){ |
||||
|
if (!connector.enabled) { |
||||
|
draftConnectors[connector.name] = { |
||||
|
connector: connector.configType, |
||||
|
config: connector.config |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
return draftConnectors; |
||||
|
} |
||||
|
|
||||
|
export function gatewayConfigJSON(gatewayConfiguration: GatewayFormModels): GatewaySetting { |
||||
|
const gatewayConfig = { |
||||
|
thingsboard: gatewayMainConfigJSON(gatewayConfiguration) |
||||
|
}; |
||||
|
gatewayConnectorJSON(gatewayConfig, gatewayConfiguration.connectors); |
||||
|
return gatewayConfig; |
||||
|
} |
||||
|
|
||||
|
function gatewayMainConfigJSON(gatewayConfiguration: GatewayFormModels): GatewayMainSetting { |
||||
|
let security: GatewaySecurity; |
||||
|
if (gatewayConfiguration.securityType === SecurityType.accessToken) { |
||||
|
security = { |
||||
|
accessToken: gatewayConfiguration.accessToken |
||||
|
} |
||||
|
} else { |
||||
|
security = { |
||||
|
caCert: gatewayConfiguration.caCertPath, |
||||
|
privateKey: gatewayConfiguration.privateKeyPath, |
||||
|
cert: gatewayConfiguration.certPath |
||||
|
} |
||||
|
} |
||||
|
const thingsboard: GatewayMainThingsboardSetting = { |
||||
|
host: gatewayConfiguration.host, |
||||
|
remoteConfiguration: gatewayConfiguration.remoteConfiguration, |
||||
|
port: gatewayConfiguration.port, |
||||
|
security |
||||
|
}; |
||||
|
|
||||
|
let storage: GatewayStorage; |
||||
|
if (gatewayConfiguration.storageType === StorageType.memory) { |
||||
|
storage = { |
||||
|
type: StorageType.memory, |
||||
|
read_records_count: gatewayConfiguration.readRecordsCount, |
||||
|
max_records_count: gatewayConfiguration.maxRecordsCount |
||||
|
}; |
||||
|
} else { |
||||
|
storage = { |
||||
|
type: StorageType.file, |
||||
|
data_folder_path: gatewayConfiguration.dataFolderPath, |
||||
|
max_file_count: gatewayConfiguration.maxFilesCount, |
||||
|
max_read_records_count: gatewayConfiguration.readRecordsCount, |
||||
|
max_records_per_file: gatewayConfiguration.maxRecordsCount |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const connectors: Array<GatewayMainConnector> = []; |
||||
|
for (const connector of gatewayConfiguration.connectors) { |
||||
|
if (connector.enabled) { |
||||
|
const connectorConfig: GatewayMainConnector = { |
||||
|
configuration: generateFileName(connector.name), |
||||
|
name: connector.name, |
||||
|
type: connector.configType |
||||
|
}; |
||||
|
connectors.push(connectorConfig); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
thingsboard, |
||||
|
connectors, |
||||
|
storage, |
||||
|
logs: window.btoa(getLogsConfig(gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function gatewayConnectorJSON(gatewayConfiguration, currentConnectors: Array<GatewayFormConnectorModel>): void { |
||||
|
for (const connector of currentConnectors) { |
||||
|
if (connector.enabled) { |
||||
|
const typeConnector = connector.configType; |
||||
|
if (!Array.isArray(gatewayConfiguration[typeConnector])) { |
||||
|
gatewayConfiguration[typeConnector] = []; |
||||
|
} |
||||
|
|
||||
|
const connectorConfig: ConnectorConfig = { |
||||
|
name: connector.name, |
||||
|
config: connector.config |
||||
|
}; |
||||
|
gatewayConfiguration[typeConnector].push(connectorConfig); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
///
|
||||
|
/// 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 { Type } from '@angular/core'; |
||||
|
import { DeviceService } from '@core/http/device.service'; |
||||
|
import { AssetService } from '@core/http/asset.service'; |
||||
|
import { AttributeService } from '@core/http/attribute.service'; |
||||
|
import { EntityRelationService } from '@core/http/entity-relation.service'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
|
||||
|
export const ServicesMap = new Map<string, Type<any>>( |
||||
|
[ |
||||
|
['deviceService', DeviceService], |
||||
|
['assetService', AssetService], |
||||
|
['attributeService', AttributeService], |
||||
|
['entityRelationService', EntityRelationService], |
||||
|
['entityService', EntityService], |
||||
|
['dialogs', DialogService], |
||||
|
['customDialog', CustomDialogService], |
||||
|
['date', DatePipe], |
||||
|
['utils', UtilsService], |
||||
|
['translate', TranslateService], |
||||
|
['http', HttpClient] |
||||
|
] |
||||
|
); |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue