118 changed files with 4589 additions and 492 deletions
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2021 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.common.data.id; |
||||
|
|
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Test; |
||||
|
|
||||
|
public class EntityIdTest { |
||||
|
|
||||
|
@Test |
||||
|
public void givenConstantNullUuid_whenCompare_thenToStringEqualsPredefinedUuid() { |
||||
|
Assert.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2021 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; |
||||
|
|
||||
|
import org.junit.extensions.cpsuite.ClasspathSuite; |
||||
|
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; |
||||
|
import org.junit.runner.RunWith; |
||||
|
|
||||
|
@RunWith(ClasspathSuite.class) |
||||
|
@ClassnameFilters({ |
||||
|
"org.thingsboard.server.dao.service.psql.*SqlTest", |
||||
|
"org.thingsboard.server.dao.service.attributes.psql.*SqlTest", |
||||
|
"org.thingsboard.server.dao.service.event.psql.*SqlTest", |
||||
|
"org.thingsboard.server.dao.service.timeseries.psql.*SqlTest" |
||||
|
}) |
||||
|
public class PostgreSqlDaoServiceTestSuite { |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2021 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; |
||||
|
|
||||
|
import com.google.common.base.Charsets; |
||||
|
import com.google.common.io.Resources; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.net.URL; |
||||
|
import java.sql.Connection; |
||||
|
import java.sql.SQLException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class PostgreSqlInitializer { |
||||
|
|
||||
|
private static final List<String> sqlFiles = List.of( |
||||
|
"sql/schema-ts-psql.sql", |
||||
|
"sql/schema-entities.sql", |
||||
|
"sql/schema-entities-idx.sql", |
||||
|
"sql/system-data.sql", |
||||
|
"sql/system-test-psql.sql"); |
||||
|
private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql"; |
||||
|
|
||||
|
public static void initDb(Connection conn) { |
||||
|
cleanUpDb(conn); |
||||
|
log.info("initialize Postgres DB..."); |
||||
|
try { |
||||
|
for (String sqlFile : sqlFiles) { |
||||
|
URL sqlFileUrl = Resources.getResource(sqlFile); |
||||
|
String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8); |
||||
|
conn.createStatement().execute(sql); |
||||
|
} |
||||
|
} catch (IOException | SQLException e) { |
||||
|
throw new RuntimeException("Unable to init the Postgres database. Reason: " + e.getMessage(), e); |
||||
|
} |
||||
|
log.info("Postgres DB is initialized!"); |
||||
|
} |
||||
|
|
||||
|
private static void cleanUpDb(Connection conn) { |
||||
|
log.info("clean up Postgres DB..."); |
||||
|
try { |
||||
|
URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile); |
||||
|
String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8); |
||||
|
conn.createStatement().execute(dropAllTablesSql); |
||||
|
} catch (IOException | SQLException e) { |
||||
|
throw new RuntimeException("Unable to clean up the Postgres database. Reason: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2021 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.service; |
||||
|
|
||||
|
import org.springframework.test.context.TestPropertySource; |
||||
|
|
||||
|
import java.lang.annotation.Documented; |
||||
|
import java.lang.annotation.ElementType; |
||||
|
import java.lang.annotation.Inherited; |
||||
|
import java.lang.annotation.Retention; |
||||
|
import java.lang.annotation.RetentionPolicy; |
||||
|
import java.lang.annotation.Target; |
||||
|
|
||||
|
@Target(ElementType.TYPE) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@Inherited |
||||
|
@Documented |
||||
|
@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:psql-test.properties"}) |
||||
|
public @interface DaoPostgreSqlTest { |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2021 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.sql.query; |
||||
|
|
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.boot.test.mock.mockito.MockBean; |
||||
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; |
||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||
|
import org.springframework.transaction.support.TransactionTemplate; |
||||
|
|
||||
|
import static org.hamcrest.MatcherAssert.assertThat; |
||||
|
import static org.hamcrest.Matchers.equalTo; |
||||
|
|
||||
|
@RunWith(SpringRunner.class) |
||||
|
@SpringBootTest(classes = DefaultEntityQueryRepository.class) |
||||
|
public class DefaultEntityQueryRepositoryTest { |
||||
|
|
||||
|
@MockBean |
||||
|
NamedParameterJdbcTemplate jdbcTemplate; |
||||
|
@MockBean |
||||
|
TransactionTemplate transactionTemplate; |
||||
|
@MockBean |
||||
|
DefaultQueryLogComponent queryLog; |
||||
|
|
||||
|
@Autowired |
||||
|
DefaultEntityQueryRepository repo; |
||||
|
|
||||
|
/* |
||||
|
* This value has to be reasonable small to prevent infinite recursion as early as possible |
||||
|
* */ |
||||
|
@Test |
||||
|
public void givenDefaultMaxLevel_whenStaticConstant_thenEqualsTo() { |
||||
|
assertThat(repo.getMaxLevelAllowed(), equalTo(50)); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenMaxLevelZeroOrNegative_whenGetMaxLevel_thenReturnDefaultMaxLevel() { |
||||
|
assertThat(repo.getMaxLevel(0), equalTo(repo.getMaxLevelAllowed())); |
||||
|
assertThat(repo.getMaxLevel(-1), equalTo(repo.getMaxLevelAllowed())); |
||||
|
assertThat(repo.getMaxLevel(-2), equalTo(repo.getMaxLevelAllowed())); |
||||
|
assertThat(repo.getMaxLevel(Integer.MIN_VALUE), equalTo(repo.getMaxLevelAllowed())); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenMaxLevelPositive_whenGetMaxLevel_thenValueTheSame() { |
||||
|
assertThat(repo.getMaxLevel(1), equalTo(1)); |
||||
|
assertThat(repo.getMaxLevel(2), equalTo(2)); |
||||
|
assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed()), equalTo(repo.getMaxLevelAllowed())); |
||||
|
assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed() + 1), equalTo(repo.getMaxLevelAllowed())); |
||||
|
assertThat(repo.getMaxLevel(Integer.MAX_VALUE), equalTo(repo.getMaxLevelAllowed())); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
database.ts.type=sql |
||||
|
database.ts_latest.type=sql |
||||
|
sql.ts_inserts_executor_type=fixed |
||||
|
sql.ts_inserts_fixed_thread_pool_size=200 |
||||
|
sql.ts_key_value_partitioning=MONTHS |
||||
|
# |
||||
|
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true |
||||
|
spring.jpa.properties.hibernate.order_by.default_null_ordering=last |
||||
|
spring.jpa.properties.hibernate.jdbc.log.warnings=false |
||||
|
spring.jpa.show-sql=false |
||||
|
spring.jpa.hibernate.ddl-auto=none |
||||
|
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect |
||||
|
spring.datasource.username=postgres |
||||
|
spring.datasource.password=postgres |
||||
|
spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb |
||||
|
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver |
||||
|
#org.postgresql.Driver |
||||
|
spring.datasource.hikari.maximumPoolSize=50 |
||||
|
service.type=monolith |
||||
|
#database.ts.type=timescale |
||||
|
#database.ts.type=sql |
||||
|
#database.entities.type=sql |
||||
|
# |
||||
|
#sql.ts_inserts_executor_type=fixed |
||||
|
#sql.ts_inserts_fixed_thread_pool_size=200 |
||||
|
#sql.ts_key_value_partitioning=MONTHS |
||||
|
# |
||||
|
#spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true |
||||
|
#spring.jpa.show-sql=false |
||||
|
#spring.jpa.hibernate.ddl-auto=none |
||||
|
#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect |
||||
|
# |
||||
|
#spring.datasource.username=postgres |
||||
|
#spring.datasource.password=postgres |
||||
|
#spring.datasource.url=jdbc:postgresql://localhost:5432/sqltest |
||||
|
#spring.datasource.driverClassName=org.postgresql.Driver |
||||
|
#spring.datasource.hikari.maximumPoolSize = 50 |
||||
|
queue.core.pack-processing-timeout=3000 |
||||
|
queue.rule-engine.pack-processing-timeout=3000 |
||||
|
queue.rule-engine.queues[0].name=Main |
||||
|
queue.rule-engine.queues[0].topic=tb_rule_engine.main |
||||
|
queue.rule-engine.queues[0].poll-interval=25 |
||||
|
queue.rule-engine.queues[0].partitions=3 |
||||
|
queue.rule-engine.queues[0].pack-processing-timeout=3000 |
||||
|
queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES |
||||
|
queue.rule-engine.queues[0].submit-strategy.type=BURST |
||||
|
sql.log_entity_queries=true |
||||
@ -0,0 +1,2 @@ |
|||||
|
--PostgreSQL specific truncate to fit constraints |
||||
|
TRUNCATE TABLE device_credentials, device, device_profile, rule_node_state, rule_node, rule_chain; |
||||
@ -0,0 +1,68 @@ |
|||||
|
#### Clear alarm details builder function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function Details(msg, metadata, msgType): any* |
||||
|
|
||||
|
JavaScript function generating **Alarm Details** object to update existing one. Used for storing additional parameters inside Alarm.<br> |
||||
|
For example you can save attribute name/value pair from Original Message payload or Metadata. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
{% include rulenode/common_node_script_args %} |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Should return the object presenting **Alarm Details**. |
||||
|
|
||||
|
Current Alarm Details can be accessed via `metadata.prevAlarmDetails`.<br> |
||||
|
**Note** that `metadata.prevAlarmDetails` is a raw String field, and it needs to be converted into object using this construction: |
||||
|
|
||||
|
```javascript |
||||
|
var details = {}; |
||||
|
if (metadata.prevAlarmDetails) { |
||||
|
// remove prevAlarmDetails from metadata |
||||
|
delete metadata.prevAlarmDetails; |
||||
|
details = JSON.parse(metadata.prevAlarmDetails); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
Take <code>count</code> property from previous Alarm and increment it.<br> |
||||
|
Also put <code>temperature</code> attribute from inbound Message payload into Alarm details: |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
var details = {temperature: msg.temperature, count: 1}; |
||||
|
|
||||
|
if (metadata.prevAlarmDetails) { |
||||
|
var prevDetails = JSON.parse(metadata.prevAlarmDetails); |
||||
|
// remove prevAlarmDetails from metadata |
||||
|
delete metadata.prevAlarmDetails; |
||||
|
if (prevDetails.count) { |
||||
|
details.count = prevDetails.count + 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return details; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/). |
||||
|
|
||||
|
You can see the real life example, where this node is used, in the next tutorial: |
||||
|
|
||||
|
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/) |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
@ -0,0 +1,8 @@ |
|||||
|
<ul> |
||||
|
<li><b>msg:</b> <code>{[key: string]: any}</code> - is a Message payload key/value object. |
||||
|
</li> |
||||
|
<li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value object. |
||||
|
</li> |
||||
|
<li><b>msgType:</b> <code>string</code> - is a string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values. |
||||
|
</li> |
||||
|
</ul> |
||||
@ -0,0 +1,69 @@ |
|||||
|
#### Create alarm details builder function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function Details(msg, metadata, msgType): any* |
||||
|
|
||||
|
JavaScript function generating **Alarm Details** object. Used for storing additional parameters inside Alarm.<br> |
||||
|
For example you can save attribute name/value pair from Original Message payload or Metadata. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
{% include rulenode/common_node_script_args %} |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Should return the object presenting **Alarm Details**. |
||||
|
|
||||
|
**Optional:** previous Alarm Details can be accessed via `metadata.prevAlarmDetails`.<br> |
||||
|
If previous Alarm does not exist, this field will not be present in Metadata. **Note** that `metadata.prevAlarmDetails`<br> |
||||
|
is a raw String field, and it needs to be converted into object using this construction: |
||||
|
|
||||
|
```javascript |
||||
|
var details = {}; |
||||
|
if (metadata.prevAlarmDetails) { |
||||
|
// remove prevAlarmDetails from metadata |
||||
|
delete metadata.prevAlarmDetails; |
||||
|
details = JSON.parse(metadata.prevAlarmDetails); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
Take <code>count</code> property from previous Alarm and increment it.<br> |
||||
|
Also put <code>temperature</code> attribute from inbound Message payload into Alarm details: |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
var details = {temperature: msg.temperature, count: 1}; |
||||
|
|
||||
|
if (metadata.prevAlarmDetails) { |
||||
|
var prevDetails = JSON.parse(metadata.prevAlarmDetails); |
||||
|
// remove prevAlarmDetails from metadata |
||||
|
delete metadata.prevAlarmDetails; |
||||
|
if (prevDetails.count) { |
||||
|
details.count = prevDetails.count + 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return details; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/). |
||||
|
|
||||
|
You can see the real life example, where this node is used, in the next tutorial: |
||||
|
|
||||
|
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/) |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
@ -0,0 +1,69 @@ |
|||||
|
#### Filter message function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function Filter(msg, metadata, msgType): boolean* |
||||
|
|
||||
|
JavaScript function evaluating **true/false** condition on incoming Message. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
{% include rulenode/common_node_script_args %} |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Should return `boolean` value. If `true` - send Message via **True** chain, otherwise **False** chain is used. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Forward all messages with `temperature` value greater than `20` to the **True** chain and all other messages to the **False** chain: |
||||
|
|
||||
|
```javascript |
||||
|
return msg.temperature > 20; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Forward all messages with type `ATTRIBUTES_UPDATED` to the **True** chain and all other messages to the **False** chain: |
||||
|
|
||||
|
```javascript |
||||
|
if (msgType === 'ATTRIBUTES_UPDATED') { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<ul> |
||||
|
<li>Send message to the <strong>True</strong> chain if the following conditions are met.<br>Message type is <code>POST_TELEMETRY_REQUEST</code> and<br> |
||||
|
(device type is <code>vehicle</code> and <code>humidity</code> value is greater than <code>50</code> or<br> |
||||
|
device type is <code>controller</code> and <code>temperature</code> value is greater than <code>20</code> and <code>humidity</code> value is greater than <code>60</code>).<br> |
||||
|
Otherwise send message to the <strong>False</strong> chain: |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
if (msgType === 'POST_TELEMETRY_REQUEST') { |
||||
|
if (metadata.deviceType === 'vehicle') { |
||||
|
return msg.humidity > 50; |
||||
|
} else if (metadata.deviceType === 'controller') { |
||||
|
return msg.temperature > 20 && msg.humidity > 60; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
You can see real life example, how to use this node in those tutorials: |
||||
|
|
||||
|
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/#node-a-filter-script) |
||||
|
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-filter-script-node) |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
|
|
||||
@ -0,0 +1,118 @@ |
|||||
|
#### Message generator function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function Generate(prevMsg, prevMetadata, prevMsgType): {msg: object, metadata: object, msgType: string}* |
||||
|
|
||||
|
JavaScript function generating new message using previous Message payload, Metadata and Message type as input arguments. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>prevMsg:</b> <code>{[key: string]: any}</code> - is a previously generated Message payload key/value object. |
||||
|
</li> |
||||
|
<li><b>prevMetadata:</b> <code>{[key: string]: string}</code> - is a previously generated Message metadata key/value object. |
||||
|
</li> |
||||
|
<li><b>prevMsgType:</b> <code>string</code> - is a previously generated string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values. |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Should return the object with the following structure: |
||||
|
|
||||
|
```javascript |
||||
|
{ |
||||
|
msg?: {[key: string]: any}, |
||||
|
metadata?: {[key: string]: string}, |
||||
|
msgType?: string |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
All fields in resulting object are optional and will be taken from previously generated Message if not specified. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Generate message of type `POST_TELEMETRY_REQUEST` with random `temperature` value from `18` to `32`: |
||||
|
|
||||
|
```javascript |
||||
|
var temperature = 18 + Math.random() * 14; |
||||
|
// Round to at most 2 decimal places (optional) |
||||
|
temperature = Math.round( temperature * 100 ) / 100; |
||||
|
var msg = { temperature: temperature }; |
||||
|
var metadata = {}; |
||||
|
var msgType = "POST_TELEMETRY_REQUEST"; |
||||
|
|
||||
|
return { msg: msg, metadata: metadata, msgType: msgType }; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
Generate message of type <code>POST_TELEMETRY_REQUEST</code> with <code>temp</code> value <code>42</code>, |
||||
|
<code>humidity</code> value <code>77</code><br> |
||||
|
and <strong>metadata</strong> with field <code>data</code> having value <code>40</code>: |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
var msg = { temp: 42, humidity: 77 }; |
||||
|
var metadata = { data: 40 }; |
||||
|
var msgType = "POST_TELEMETRY_REQUEST"; |
||||
|
|
||||
|
return { msg: msg, metadata: metadata, msgType: msgType }; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
Generate message of type <code>POST_TELEMETRY_REQUEST</code> with <code>temperature</code> value<br> |
||||
|
increasing and decreasing linearly in the range from <code>18</code> to <code>32</code>: |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
var lower = 18; |
||||
|
var upper = 32; |
||||
|
var isDecrement = 'false'; |
||||
|
var temperature = lower; |
||||
|
|
||||
|
// Get previous values |
||||
|
|
||||
|
if (typeof prevMetadata !== 'undefined' && |
||||
|
typeof prevMetadata.isDecrement !== 'undefined') { |
||||
|
isDecrement = prevMetadata.isDecrement; |
||||
|
} |
||||
|
if (typeof prevMsg !== 'undefined' && |
||||
|
typeof prevMsg.temperature !== 'undefined') { |
||||
|
temperature = prevMsg.temperature; |
||||
|
} |
||||
|
|
||||
|
if (isDecrement === 'true') { |
||||
|
temperature--; |
||||
|
if (temperature <= lower) { |
||||
|
isDecrement = 'false'; |
||||
|
temperature = lower; |
||||
|
} |
||||
|
} else { |
||||
|
temperature++; |
||||
|
if (temperature >= upper) { |
||||
|
isDecrement = 'true'; |
||||
|
temperature = upper; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var msg = { temperature: temperature }; |
||||
|
var metadata = { isDecrement: isDecrement }; |
||||
|
var msgType = "POST_TELEMETRY_REQUEST"; |
||||
|
|
||||
|
return { msg: msg, metadata: metadata, msgType: msgType }; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
@ -0,0 +1,37 @@ |
|||||
|
#### Message to string function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function toString(msg, metadata, msgType): string* |
||||
|
|
||||
|
JavaScript function transforming incoming Message to String for further logging to the server log file. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
{% include rulenode/common_node_script_args %} |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Should return `string` value used for logging to the server log file. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Create string message containing incoming message and incoming metadata values: |
||||
|
|
||||
|
```javascript |
||||
|
return 'Incoming message:\n' + JSON.stringify(msg) + |
||||
|
'\nIncoming metadata:\n' + JSON.stringify(metadata); |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
You can see real life example, how to use this node in this tutorial: |
||||
|
|
||||
|
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#log-unknown-request) |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
@ -0,0 +1,96 @@ |
|||||
|
#### Switch message function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function Switch(msg, metadata, msgType): string[]* |
||||
|
|
||||
|
JavaScript function computing **an array of next Relation names** for incoming Message. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
{% include rulenode/common_node_script_args %} |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Should return an array of `string` values presenting **next Relation names** where Message should be routed.<br> |
||||
|
If returned array is empty - message will not be routed to any Node and discarded. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
Forward all messages with <code>temperature</code> value greater than <code>30</code> to the <strong>'High temperature'</strong> chain,<br> |
||||
|
with <code>temperature</code> value lower than <code>20</code> to the <strong>'Low temperature'</strong> chain and all other messages<br> |
||||
|
to the <strong>'Normal temperature'</strong> chain: |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
if (msg.temperature > 30) { |
||||
|
return ['High temperature']; |
||||
|
} else if (msg.temperature < 20) { |
||||
|
return ['Low temperature']; |
||||
|
} else { |
||||
|
return ['Normal temperature']; |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<ul> |
||||
|
<li> |
||||
|
For messages with type <code>POST_TELEMETRY_REQUEST</code>: |
||||
|
<ul> |
||||
|
<li> |
||||
|
if <code>temperature</code> value lower than <code>18</code> forward to the <strong>'Low temperature telemetry'</strong> chain, |
||||
|
</li> |
||||
|
<li> |
||||
|
otherwise to the <strong>'Normal temperature telemetry'</strong> chain. |
||||
|
</li> |
||||
|
</ul> |
||||
|
For messages with type <code>POST_ATTRIBUTES_REQUEST</code>:<br> |
||||
|
<ul> |
||||
|
<li> |
||||
|
if <code>currentState</code> value is <code>IDLE</code> forward to the <strong>'Idle State'</strong> and <strong>'Update State Attribute'</strong> chains, |
||||
|
</li> |
||||
|
<li> |
||||
|
if <code>currentState</code> value is <code>RUNNING</code> forward to the <strong>'Running State'</strong> and <strong>'Update State Attribute'</strong> chains, |
||||
|
</li> |
||||
|
<li> |
||||
|
otherwise to the <strong>'Unknown State'</strong> chain. |
||||
|
</li> |
||||
|
</ul> |
||||
|
For all other message types - discard the message (do not route to any Node). |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
```javascript |
||||
|
if (msgType === 'POST_TELEMETRY_REQUEST') { |
||||
|
if (msg.temperature < 18) { |
||||
|
return ['Low Temperature Telemetry']; |
||||
|
} else { |
||||
|
return ['Normal Temperature Telemetry']; |
||||
|
} |
||||
|
} else if (msgType === 'POST_ATTRIBUTES_REQUEST') { |
||||
|
if (msg.currentState === 'IDLE') { |
||||
|
return ['Idle State', 'Update State Attribute']; |
||||
|
} else if (msg.currentState === 'RUNNING') { |
||||
|
return ['Running State', 'Update State Attribute']; |
||||
|
} else { |
||||
|
return ['Unknown State']; |
||||
|
} |
||||
|
} |
||||
|
return []; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
You can see real life example, how to use this node in this tutorial: |
||||
|
|
||||
|
- [Data function based on telemetry from 2 devices{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/function-based-on-telemetry-from-two-devices#delta-temperature-rule-chain) |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,26 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2021 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. |
||||
|
|
||||
|
--> |
||||
|
<ng-container #markdownContainer> |
||||
|
</ng-container> |
||||
|
<div *ngIf="error && !fallbackToPlainMarkdown" style="color: #f00; font-size: 14px; |
||||
|
line-height: 28px; |
||||
|
background: #efefef;"> |
||||
|
{{error}} |
||||
|
</div> |
||||
|
<div #fallbackElement [fxShow]="error && fallbackToPlainMarkdown" class="tb-markdown-view" [ngClass]="markdownClass" [ngStyle]="style"> |
||||
|
</div> |
||||
@ -0,0 +1,194 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2021 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
ComponentFactory, |
||||
|
ComponentRef, ElementRef, |
||||
|
EventEmitter, |
||||
|
Inject, |
||||
|
Injector, |
||||
|
Input, OnChanges, |
||||
|
Output, |
||||
|
SimpleChanges, |
||||
|
Type, ViewChild, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { HelpService } from '@core/services/help.service'; |
||||
|
import { MarkdownService, PrismPlugin } from 'ngx-markdown'; |
||||
|
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { Observable, of, ReplaySubject } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-markdown', |
||||
|
templateUrl: './markdown.component.html' |
||||
|
}) |
||||
|
export class TbMarkdownComponent implements OnChanges { |
||||
|
|
||||
|
@ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef; |
||||
|
@ViewChild('fallbackElement', {static: true}) fallbackElement: ElementRef<HTMLElement>; |
||||
|
|
||||
|
@Input() data: string | undefined; |
||||
|
|
||||
|
@Input() markdownClass: string | undefined; |
||||
|
|
||||
|
@Input() style: { [klass: string]: any } = {}; |
||||
|
|
||||
|
@Input() |
||||
|
get lineNumbers(): boolean { return this.lineNumbersValue; } |
||||
|
set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } |
||||
|
|
||||
|
@Input() |
||||
|
get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } |
||||
|
set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } |
||||
|
|
||||
|
@Output() ready = new EventEmitter<void>(); |
||||
|
|
||||
|
private lineNumbersValue = false; |
||||
|
private fallbackToPlainMarkdownValue = false; |
||||
|
|
||||
|
isMarkdownReady = false; |
||||
|
|
||||
|
error = null; |
||||
|
|
||||
|
private tbMarkdownInstanceComponentRef: ComponentRef<any>; |
||||
|
private tbMarkdownInstanceComponentFactory: ComponentFactory<any>; |
||||
|
|
||||
|
constructor(private help: HelpService, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
public markdownService: MarkdownService, |
||||
|
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>, |
||||
|
private dynamicComponentFactoryService: DynamicComponentFactoryService) {} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
if (isDefinedAndNotNull(this.data)) { |
||||
|
this.render(this.data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private render(markdown: string) { |
||||
|
const compiled = this.markdownService.compile(markdown, false); |
||||
|
let template = this.sanitizeCurlyBraces(compiled); |
||||
|
let markdownClass = 'tb-markdown-view'; |
||||
|
if (this.markdownClass) { |
||||
|
markdownClass += ` ${this.markdownClass}`; |
||||
|
} |
||||
|
template = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`; |
||||
|
this.markdownContainer.clear(); |
||||
|
const parent = this; |
||||
|
let readyObservable: Observable<void>; |
||||
|
this.dynamicComponentFactoryService.createDynamicComponentFactory( |
||||
|
class TbMarkdownInstance { |
||||
|
ngOnDestroy(): void { |
||||
|
parent.destroyMarkdownInstanceResources(); |
||||
|
} |
||||
|
}, |
||||
|
template, |
||||
|
[this.sharedModule], |
||||
|
true |
||||
|
).subscribe((factory) => { |
||||
|
this.tbMarkdownInstanceComponentFactory = factory; |
||||
|
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); |
||||
|
try { |
||||
|
this.tbMarkdownInstanceComponentRef = |
||||
|
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); |
||||
|
this.tbMarkdownInstanceComponentRef.instance.style = this.style; |
||||
|
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement); |
||||
|
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement); |
||||
|
readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); |
||||
|
this.error = null; |
||||
|
} catch (error) { |
||||
|
readyObservable = this.handleError(compiled, error); |
||||
|
} |
||||
|
this.cd.detectChanges(); |
||||
|
readyObservable.subscribe(() => { |
||||
|
this.ready.emit(); |
||||
|
}); |
||||
|
}, |
||||
|
(error) => { |
||||
|
readyObservable = this.handleError(compiled, error); |
||||
|
this.cd.detectChanges(); |
||||
|
readyObservable.subscribe(() => { |
||||
|
this.ready.emit(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private handleError(template: string, error): Observable<void> { |
||||
|
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>'); |
||||
|
this.destroyMarkdownInstanceResources(); |
||||
|
if (this.fallbackToPlainMarkdownValue) { |
||||
|
this.markdownContainer.clear(); |
||||
|
const element = this.fallbackElement.nativeElement; |
||||
|
element.innerHTML = template; |
||||
|
this.handlePlugins(element); |
||||
|
this.markdownService.highlight(element); |
||||
|
return this.handleImages(element); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private handlePlugins(element: HTMLElement): void { |
||||
|
if (this.lineNumbers) { |
||||
|
this.setPluginClass(element, PrismPlugin.LineNumbers); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setPluginClass(element: HTMLElement, plugin: string | string[]): void { |
||||
|
const preElements = element.querySelectorAll('pre'); |
||||
|
for (let i = 0; i < preElements.length; i++) { |
||||
|
const classes = plugin instanceof Array ? plugin : [plugin]; |
||||
|
preElements.item(i).classList.add(...classes); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private handleImages(element: HTMLElement): Observable<void> { |
||||
|
const imgs = $('img', element); |
||||
|
if (imgs.length) { |
||||
|
let totalImages = imgs.length; |
||||
|
const imagesLoadedSubject = new ReplaySubject<void>(); |
||||
|
imgs.each((index, img) => { |
||||
|
$(img).one('load error', () => { |
||||
|
totalImages--; |
||||
|
if (totalImages === 0) { |
||||
|
imagesLoadedSubject.next(); |
||||
|
imagesLoadedSubject.complete(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
return imagesLoadedSubject.asObservable(); |
||||
|
} else { |
||||
|
return of(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private sanitizeCurlyBraces(template: string): string { |
||||
|
return template.replace(/{/g, '{').replace(/}/g, '}'); |
||||
|
} |
||||
|
|
||||
|
private destroyMarkdownInstanceResources() { |
||||
|
if (this.tbMarkdownInstanceComponentFactory) { |
||||
|
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory); |
||||
|
this.tbMarkdownInstanceComponentFactory = null; |
||||
|
} |
||||
|
this.tbMarkdownInstanceComponentRef = null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,190 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2021 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 { |
||||
|
ComponentFactory, |
||||
|
ComponentFactoryResolver, ElementRef, Inject, |
||||
|
Injectable, Injector, |
||||
|
Renderer2, |
||||
|
Type, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { PopoverPlacement, PopoverWithTrigger } from '@shared/components/popover.models'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { ComponentType } from '@angular/cdk/portal'; |
||||
|
import { HELP_MARKDOWN_COMPONENT_TOKEN } from '@shared/components/tokens'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TbPopoverService { |
||||
|
|
||||
|
private popoverWithTriggers: PopoverWithTrigger[] = []; |
||||
|
|
||||
|
componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent); |
||||
|
|
||||
|
constructor(private resolver: ComponentFactoryResolver, |
||||
|
@Inject(HELP_MARKDOWN_COMPONENT_TOKEN) private helpMarkdownComponent: ComponentType<any>) { |
||||
|
} |
||||
|
|
||||
|
hasPopover(trigger: Element): boolean { |
||||
|
const res = this.findPopoverByTrigger(trigger); |
||||
|
return res !== null; |
||||
|
} |
||||
|
|
||||
|
hidePopover(trigger: Element): boolean { |
||||
|
const component: TbPopoverComponent = this.findPopoverByTrigger(trigger); |
||||
|
if (component && component.tbVisible) { |
||||
|
component.hide(); |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, |
||||
|
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, |
||||
|
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent { |
||||
|
const componentRef = hostView.createComponent(this.componentFactory); |
||||
|
const component = componentRef.instance; |
||||
|
this.popoverWithTriggers.push({ |
||||
|
trigger, |
||||
|
popoverComponent: component |
||||
|
}); |
||||
|
renderer.removeChild( |
||||
|
renderer.parentNode(trigger), |
||||
|
componentRef.location.nativeElement |
||||
|
); |
||||
|
const originElementRef = new ElementRef(trigger); |
||||
|
component.setOverlayOrigin({ elementRef: originElementRef }); |
||||
|
component.tbPlacement = preferredPlacement; |
||||
|
component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType); |
||||
|
component.tbComponentInjector = injector; |
||||
|
component.tbComponentContext = context; |
||||
|
component.tbOverlayStyle = overlayStyle; |
||||
|
component.tbPopoverInnerStyle = popoverStyle; |
||||
|
component.tbComponentStyle = style; |
||||
|
component.tbHideOnClickOutside = hideOnClickOutside; |
||||
|
component.tbVisibleChange.subscribe((visible: boolean) => { |
||||
|
if (!visible) { |
||||
|
component.tbAnimationDone.subscribe(() => { |
||||
|
componentRef.destroy(); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
component.tbDestroy.subscribe(() => { |
||||
|
this.removePopoverByComponent(component); |
||||
|
}); |
||||
|
component.show(); |
||||
|
return component; |
||||
|
} |
||||
|
|
||||
|
toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId = '', |
||||
|
helpContent = '', |
||||
|
visibleFn: (visible: boolean) => void = () => {}, |
||||
|
readyFn: (ready: boolean) => void = () => {}, |
||||
|
preferredPlacement: PopoverPlacement = 'bottom', |
||||
|
overlayStyle: any = {}, helpStyle: any = {}) { |
||||
|
if (this.hasPopover(trigger)) { |
||||
|
this.hidePopover(trigger); |
||||
|
} else { |
||||
|
readyFn(false); |
||||
|
const injector = Injector.create({ |
||||
|
parent: hostView.injector, providers: [] |
||||
|
}); |
||||
|
const componentRef = hostView.createComponent(this.componentFactory); |
||||
|
const component = componentRef.instance; |
||||
|
this.popoverWithTriggers.push({ |
||||
|
trigger, |
||||
|
popoverComponent: component |
||||
|
}); |
||||
|
renderer.removeChild( |
||||
|
renderer.parentNode(trigger), |
||||
|
componentRef.location.nativeElement |
||||
|
); |
||||
|
const originElementRef = new ElementRef(trigger); |
||||
|
component.tbAnimationState = 'void'; |
||||
|
component.tbOverlayStyle = {...overlayStyle, opacity: '0' }; |
||||
|
component.setOverlayOrigin({ elementRef: originElementRef }); |
||||
|
component.tbPlacement = preferredPlacement; |
||||
|
component.tbComponentFactory = this.resolver.resolveComponentFactory(this.helpMarkdownComponent); |
||||
|
component.tbComponentInjector = injector; |
||||
|
component.tbComponentContext = { |
||||
|
helpId, |
||||
|
helpContent, |
||||
|
style: helpStyle, |
||||
|
visible: true |
||||
|
}; |
||||
|
component.tbHideOnClickOutside = true; |
||||
|
component.tbVisibleChange.subscribe((visible: boolean) => { |
||||
|
if (!visible) { |
||||
|
visibleFn(false); |
||||
|
component.tbAnimationDone.subscribe(() => { |
||||
|
componentRef.destroy(); |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
component.tbDestroy.subscribe(() => { |
||||
|
this.removePopoverByComponent(component); |
||||
|
}); |
||||
|
const showHelpMarkdownComponent = () => { |
||||
|
component.tbOverlayStyle = {...component.tbOverlayStyle, opacity: '1' }; |
||||
|
component.tbAnimationState = 'active'; |
||||
|
component.updatePosition(); |
||||
|
readyFn(true); |
||||
|
setTimeout(() => { |
||||
|
component.updatePosition(); |
||||
|
}); |
||||
|
}; |
||||
|
const setupHelpMarkdownComponent = (helpMarkdownComponent: any) => { |
||||
|
if (helpMarkdownComponent.isMarkdownReady) { |
||||
|
showHelpMarkdownComponent(); |
||||
|
} else { |
||||
|
helpMarkdownComponent.markdownReady.subscribe(() => { |
||||
|
showHelpMarkdownComponent(); |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
if (component.tbComponentRef) { |
||||
|
setupHelpMarkdownComponent(component.tbComponentRef.instance); |
||||
|
} else { |
||||
|
component.tbComponentChange.subscribe((helpMarkdownComponentRef) => { |
||||
|
setupHelpMarkdownComponent(helpMarkdownComponentRef.instance); |
||||
|
}); |
||||
|
} |
||||
|
component.show(); |
||||
|
visibleFn(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null { |
||||
|
const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger)); |
||||
|
if (res) { |
||||
|
return res.popoverComponent; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private removePopoverByComponent(component: TbPopoverComponent): void { |
||||
|
const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component); |
||||
|
if (index > -1) { |
||||
|
this.popoverWithTriggers.splice(index, 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean { |
||||
|
return element1 === element2 || element1.contains(element2) || element2.contains(element1); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2021 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 { InjectionToken, Type } from '@angular/core'; |
||||
|
import { ComponentType } from '@angular/cdk/portal'; |
||||
|
|
||||
|
export const HELP_MARKDOWN_COMPONENT_TOKEN: InjectionToken<ComponentType<any>> = |
||||
|
new InjectionToken<ComponentType<any>>('HELP_MARKDOWN_COMPONENT_TOKEN'); |
||||
|
|
||||
|
export const SHARED_MODULE_TOKEN: InjectionToken<Type<any>> = |
||||
|
new InjectionToken<Type<any>>('HELP_MARKDOWN_COMPONENT_TOKEN'); |
||||
@ -0,0 +1,19 @@ |
|||||
|
<li><b>$event:</b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a></code> - The <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a> object. Usually a result of a mouse click event. |
||||
|
</li> |
||||
|
<li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API |
||||
|
and data used by widget instance. |
||||
|
</li> |
||||
|
<li><b>entityId:</b> <code>string</code> - An optional string id of the target entity. |
||||
|
</li> |
||||
|
<li><b>entityName:</b> <code>string</code> - An optional string name of the target entity. |
||||
|
</li> |
||||
|
<li><b>additionalParams:</b> <code>{[key: string]: any}</code> - An optional key/value object holding additional entity parameters. |
||||
|
<span style="padding-left: 4px;" |
||||
|
tb-help-popup="widget/action/custom_additional_params" |
||||
|
tb-help-popup-placement="top" |
||||
|
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
||||
|
trigger-text="Read more"> |
||||
|
</span> |
||||
|
</li> |
||||
|
<li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity. |
||||
|
</li> |
||||
@ -0,0 +1,81 @@ |
|||||
|
#### Custom action function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function ($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
A JavaScript function performing custom action. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display alert dialog with entity information: |
||||
|
|
||||
|
```javascript |
||||
|
var title; |
||||
|
var content; |
||||
|
if (entityName) { |
||||
|
title = entityName + ' details'; |
||||
|
content = '<b>Entity name</b>: ' + entityName; |
||||
|
if (additionalParams && additionalParams.entity) { |
||||
|
var entity = additionalParams.entity; |
||||
|
if (entity.id) { |
||||
|
content += '<br><b>Entity type</b>: ' + entity.id.entityType; |
||||
|
} |
||||
|
if (!isNaN(entity.temperature) && entity.temperature !== '') { |
||||
|
content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C'; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
title = 'No entity information available'; |
||||
|
content = '<b>No entity information available</b>'; |
||||
|
} |
||||
|
|
||||
|
showAlertDialog(title, content); |
||||
|
|
||||
|
function showAlertDialog(title, content) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert(title, content).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Delete device after confirmation: |
||||
|
|
||||
|
```javascript |
||||
|
var $injector = widgetContext.$scope.$injector; |
||||
|
var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); |
||||
|
var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); |
||||
|
|
||||
|
openDeleteDeviceDialog(); |
||||
|
|
||||
|
function openDeleteDeviceDialog() { |
||||
|
var title = 'Are you sure you want to delete the device ' + entityName + '?'; |
||||
|
var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!'; |
||||
|
dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( |
||||
|
function(result) { |
||||
|
if (result) { |
||||
|
deleteDevice(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function deleteDevice() { |
||||
|
deviceService.deleteDevice(entityId.id).subscribe( |
||||
|
function() { |
||||
|
widgetContext.updateAliases(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,53 @@ |
|||||
|
#### Additional params object |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
<b>additionalParams:</b> <code>{[key: string]: any}</code> |
||||
|
|
||||
|
An optional key/value object holding additional entity parameters depending on widget type and action source: |
||||
|
|
||||
|
<ul> |
||||
|
<li>Entities table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code>{ entity: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a> }</code>: |
||||
|
<ul> |
||||
|
<li><b>entity:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a></code> - An |
||||
|
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a> object |
||||
|
presenting basic entity properties (ex. <code>id</code>, <code>entityName</code>) and <br> provides access to other entity attributes/timeseries declared in widget datasource configuration. |
||||
|
</li> |
||||
|
</ul> |
||||
|
</li> |
||||
|
<li>Alarms table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code>{ alarm: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> }</code>: |
||||
|
<ul> |
||||
|
<li><b>alarm:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a></code> - An |
||||
|
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> object |
||||
|
presenting basic alarm properties (ex. <code>type</code>, <code>severity</code>, <code>originator</code>, etc.) and <br> provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. |
||||
|
</li> |
||||
|
</ul> |
||||
|
</li> |
||||
|
<li>Timeseries table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a></code>: |
||||
|
<ul> |
||||
|
<li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a></code> - A |
||||
|
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a> object |
||||
|
presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration. |
||||
|
</li> |
||||
|
</ul> |
||||
|
</li> |
||||
|
<li>Entities hierarchy widget (<i>On node selected</i>) - <b>additionalParams:</b> <code>{ nodeCtx: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> }</code>: |
||||
|
<ul> |
||||
|
<li><b>nodeCtx:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a></code> - An |
||||
|
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> object |
||||
|
containing <code>entity</code> field holding basic entity properties <br> (ex. <code>id</code>, <code>name</code>, <code>label</code>) and <code>data</code> field holding other entity attributes/timeseries declared in widget datasource configuration. |
||||
|
</li> |
||||
|
</ul> |
||||
|
</li> |
||||
|
<li>Pie - Flot widget (<i>On slice click</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code>: |
||||
|
<ul> |
||||
|
<li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code> - A |
||||
|
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a> object |
||||
|
containing <code>series</code> field with information about datasource and <br> data key of clicked pie slice. |
||||
|
</li> |
||||
|
</ul> |
||||
|
</li> |
||||
|
<li><i>All other widgets</i> - does not provide <b>additionalParams</b> value. |
||||
|
</li> |
||||
|
</ul> |
||||
@ -0,0 +1,85 @@ |
|||||
|
#### Custom action (with HTML template) function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void* |
||||
|
|
||||
|
A JavaScript function performing custom action with defined HTML template to render dialog. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>$event:</b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a></code> - The <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a> object. Usually a result of a mouse click event. |
||||
|
</li> |
||||
|
<li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API |
||||
|
and data used by widget instance. |
||||
|
</li> |
||||
|
<li><b>entityId:</b> <code>string</code> - An optional string id of the target entity. |
||||
|
</li> |
||||
|
<li><b>entityName:</b> <code>string</code> - An optional string name of the target entity. |
||||
|
</li> |
||||
|
<li><b>htmlTemplate:</b> <code>string</code> - An optional HTML template string defined in <b>HTML</b> tab.<br/> Used to render custom dialog (see <b>Examples</b> for more details). |
||||
|
</li> |
||||
|
<li><b>additionalParams:</b> <code>{[key: string]: any}</code> - An optional key/value object holding additional entity parameters. |
||||
|
<span style="padding-left: 4px;" |
||||
|
tb-help-popup="widget/action/custom_additional_params" |
||||
|
tb-help-popup-placement="top" |
||||
|
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
||||
|
trigger-text="Read more"> |
||||
|
</span> |
||||
|
</li> |
||||
|
<li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity. |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
###### Display dialog to create a device or an asset |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<div style="padding-left: 64px;" |
||||
|
tb-help-popup="widget/action/examples/custom_pretty_create_dialog_js" |
||||
|
tb-help-popup-placement="top" |
||||
|
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
||||
|
trigger-style="font-size: 16px;" |
||||
|
trigger-text="JavaScript function"> |
||||
|
</div> |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<div style="padding-left: 64px;" |
||||
|
tb-help-popup="widget/action/examples/custom_pretty_create_dialog_html" |
||||
|
tb-help-popup-placement="top" |
||||
|
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
||||
|
trigger-style="font-size: 16px;" |
||||
|
trigger-text="HTML code"> |
||||
|
</div> |
||||
|
|
||||
|
###### Display dialog to edit a device or an asset |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<div style="padding-left: 64px;" |
||||
|
tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_js" |
||||
|
tb-help-popup-placement="top" |
||||
|
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
||||
|
trigger-style="font-size: 16px;" |
||||
|
trigger-text="JavaScript function"> |
||||
|
</div> |
||||
|
|
||||
|
<br> |
||||
|
|
||||
|
<div style="padding-left: 64px;" |
||||
|
tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_html" |
||||
|
tb-help-popup-placement="top" |
||||
|
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" |
||||
|
trigger-style="font-size: 16px;" |
||||
|
trigger-text="HTML code"> |
||||
|
</div> |
||||
|
|
||||
|
<br> |
||||
|
<br> |
||||
@ -0,0 +1,160 @@ |
|||||
|
#### HTML template of dialog to create a device or an asset |
||||
|
|
||||
|
```html |
||||
|
<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup" |
||||
|
(ngSubmit)="save()" class="add-entity-form"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2>Add entity</h2> |
||||
|
<span fxFlex></span> |
||||
|
<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 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 fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label>Address</mat-label> |
||||
|
<input matInput formControlName="address"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label>Owner</mat-label> |
||||
|
<input matInput formControlName="owner"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label>Integer Value</mat-label> |
||||
|
<input type="number" step="1" matInput formControlName="number"> |
||||
|
<mat-error *ngIf="addEntityFormGroup.get('attributes.number').hasError('pattern')"> |
||||
|
Invalid integer value. |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> |
||||
|
<label class="checkbox-label">Boolean Value</label> |
||||
|
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> |
||||
|
|
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="relations-list"> |
||||
|
<div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div> |
||||
|
<div class="body" [fxShow]="relations().length"> |
||||
|
<div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;"> |
||||
|
<div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field class="mat-block" style="min-width: 100px;"> |
||||
|
<mat-label>Direction</mat-label> |
||||
|
<mat-select formControlName="direction" name="direction"> |
||||
|
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> |
||||
|
|
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="relation.get('direction').hasError('required')"> |
||||
|
Relation direction is required. |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-relation-type-autocomplete |
||||
|
fxFlex class="mat-block" |
||||
|
formControlName="relationType" |
||||
|
[required]="true"> |
||||
|
</tb-relation-type-autocomplete> |
||||
|
</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> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="addRelation()" |
||||
|
matTooltip="Add Relation" |
||||
|
matTooltipPosition="above"> |
||||
|
Add |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
Cancel |
||||
|
</button> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty"> |
||||
|
Create |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,132 @@ |
|||||
|
#### Function displaying dialog to create a device or an asset |
||||
|
|
||||
|
```javascript |
||||
|
let $injector = widgetContext.$scope.$injector; |
||||
|
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); |
||||
|
let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); |
||||
|
let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); |
||||
|
let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); |
||||
|
let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); |
||||
|
|
||||
|
openAddEntityDialog(); |
||||
|
|
||||
|
function openAddEntityDialog() { |
||||
|
customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); |
||||
|
} |
||||
|
|
||||
|
function AddEntityDialogController(instance) { |
||||
|
let vm = instance; |
||||
|
|
||||
|
vm.allowedEntityTypes = ['ASSET', 'DEVICE']; |
||||
|
vm.entitySearchDirection = { |
||||
|
from: "FROM", |
||||
|
to: "TO" |
||||
|
} |
||||
|
|
||||
|
vm.addEntityFormGroup = vm.fb.group({ |
||||
|
entityName: ['', [vm.validators.required]], |
||||
|
entityType: ['DEVICE'], |
||||
|
entityLabel: [null], |
||||
|
type: ['', [vm.validators.required]], |
||||
|
attributes: vm.fb.group({ |
||||
|
latitude: [null], |
||||
|
longitude: [null], |
||||
|
address: [null], |
||||
|
owner: [null], |
||||
|
number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], |
||||
|
booleanValue: [null] |
||||
|
}), |
||||
|
relations: vm.fb.array([]) |
||||
|
}); |
||||
|
|
||||
|
vm.cancel = function () { |
||||
|
vm.dialogRef.close(null); |
||||
|
}; |
||||
|
|
||||
|
vm.relations = function () { |
||||
|
return vm.addEntityFormGroup.get('relations'); |
||||
|
}; |
||||
|
|
||||
|
vm.addRelation = function () { |
||||
|
vm.relations().push(vm.fb.group({ |
||||
|
relatedEntity: [null, [vm.validators.required]], |
||||
|
relationType: [null, [vm.validators.required]], |
||||
|
direction: [null, [vm.validators.required]] |
||||
|
})); |
||||
|
}; |
||||
|
|
||||
|
vm.removeRelation = function (index) { |
||||
|
vm.relations().removeAt(index); |
||||
|
vm.relations().markAsDirty(); |
||||
|
}; |
||||
|
|
||||
|
vm.save = function () { |
||||
|
vm.addEntityFormGroup.markAsPristine(); |
||||
|
saveEntityObservable().subscribe( |
||||
|
function (entity) { |
||||
|
widgetContext.rxjs.forkJoin([ |
||||
|
saveAttributes(entity.id), |
||||
|
saveRelations(entity.id) |
||||
|
]).subscribe( |
||||
|
function () { |
||||
|
widgetContext.updateAliases(); |
||||
|
vm.dialogRef.close(null); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
function saveEntityObservable() { |
||||
|
const formValues = vm.addEntityFormGroup.value; |
||||
|
let entity = { |
||||
|
name: formValues.entityName, |
||||
|
type: formValues.type, |
||||
|
label: formValues.entityLabel |
||||
|
}; |
||||
|
if (formValues.entityType == 'ASSET') { |
||||
|
return assetService.saveAsset(entity); |
||||
|
} else if (formValues.entityType == 'DEVICE') { |
||||
|
return deviceService.saveDevice(entity); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function saveAttributes(entityId) { |
||||
|
let attributes = vm.addEntityFormGroup.get('attributes').value; |
||||
|
let attributesArray = []; |
||||
|
for (let key in attributes) { |
||||
|
if (attributes[key] !== null) { |
||||
|
attributesArray.push({key: key, value: attributes[key]}); |
||||
|
} |
||||
|
} |
||||
|
if (attributesArray.length > 0) { |
||||
|
return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); |
||||
|
} |
||||
|
return widgetContext.rxjs.of([]); |
||||
|
} |
||||
|
|
||||
|
function saveRelations(entityId) { |
||||
|
let relations = vm.addEntityFormGroup.get('relations').value; |
||||
|
let tasks = []; |
||||
|
for (let i = 0; i < relations.length; i++) { |
||||
|
let relation = { |
||||
|
type: relations[i].relationType, |
||||
|
typeGroup: 'COMMON' |
||||
|
}; |
||||
|
if (relations[i].direction == 'FROM') { |
||||
|
relation.to = relations[i].relatedEntity; |
||||
|
relation.from = entityId; |
||||
|
} else { |
||||
|
relation.to = entityId; |
||||
|
relation.from = relations[i].relatedEntity; |
||||
|
} |
||||
|
tasks.push(entityRelationService.saveRelation(relation)); |
||||
|
} |
||||
|
if (tasks.length > 0) { |
||||
|
return widgetContext.rxjs.forkJoin(tasks); |
||||
|
} |
||||
|
return widgetContext.rxjs.of([]); |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,192 @@ |
|||||
|
#### HTML template of dialog to edit a device or an asset |
||||
|
|
||||
|
```html |
||||
|
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" |
||||
|
(ngSubmit)="save()" class="edit-entity-form"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2>Edit </h2> |
||||
|
<span fxFlex></span> |
||||
|
<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 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 fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label>Address</mat-label> |
||||
|
<input matInput formControlName="address"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label>Owner</mat-label> |
||||
|
<input matInput formControlName="owner"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label>Integer Value</mat-label> |
||||
|
<input type="number" step="1" matInput formControlName="number"> |
||||
|
<mat-error *ngIf="editEntityFormGroup.get('attributes.number').hasError('pattern')"> |
||||
|
Invalid integer value. |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> |
||||
|
<label class="checkbox-label">Boolean Value</label> |
||||
|
<mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> |
||||
|
|
||||
|
</mat-checkbox> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="relations-list old-relations"> |
||||
|
<div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div> |
||||
|
<div class="body" [fxShow]="oldRelations().length"> |
||||
|
<div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations" |
||||
|
*ngFor="let relation of oldRelations().controls; let i = index;"> |
||||
|
<div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field class="mat-block" style="min-width: 100px;"> |
||||
|
<mat-label>Direction</mat-label> |
||||
|
<mat-select formControlName="direction" name="direction"> |
||||
|
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> |
||||
|
|
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="relation.get('direction').hasError('required')"> |
||||
|
Relation direction is required. |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-relation-type-autocomplete |
||||
|
fxFlex class="mat-block" |
||||
|
formControlName="relationType" |
||||
|
required="true"> |
||||
|
</tb-relation-type-autocomplete> |
||||
|
</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 class="relations-list"> |
||||
|
<div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div> |
||||
|
<div class="body" [fxShow]="relations().length"> |
||||
|
<div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;"> |
||||
|
<div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> |
||||
|
<mat-form-field class="mat-block" style="min-width: 100px;"> |
||||
|
<mat-label>Direction</mat-label> |
||||
|
<mat-select formControlName="direction" name="direction"> |
||||
|
<mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> |
||||
|
|
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="relation.get('direction').hasError('required')"> |
||||
|
Relation direction is required. |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-relation-type-autocomplete |
||||
|
fxFlex class="mat-block" |
||||
|
formControlName="relationType" |
||||
|
[required]="true"> |
||||
|
</tb-relation-type-autocomplete> |
||||
|
</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> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="addRelation()" |
||||
|
matTooltip="Add Relation" |
||||
|
matTooltipPosition="above"> |
||||
|
Add |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
Cancel |
||||
|
</button> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty"> |
||||
|
Save |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,220 @@ |
|||||
|
#### Function displaying dialog to edit a device or an asset |
||||
|
|
||||
|
```javascript |
||||
|
let $injector = widgetContext.$scope.$injector; |
||||
|
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); |
||||
|
let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); |
||||
|
let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); |
||||
|
let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); |
||||
|
let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); |
||||
|
let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); |
||||
|
|
||||
|
openEditEntityDialog(); |
||||
|
|
||||
|
function openEditEntityDialog() { |
||||
|
customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(); |
||||
|
} |
||||
|
|
||||
|
function EditEntityDialogController(instance) { |
||||
|
let vm = instance; |
||||
|
|
||||
|
vm.entityName = entityName; |
||||
|
vm.entityType = entityId.entityType; |
||||
|
vm.entitySearchDirection = { |
||||
|
from: "FROM", |
||||
|
to: "TO" |
||||
|
}; |
||||
|
vm.attributes = {}; |
||||
|
vm.oldRelationsData = []; |
||||
|
vm.relationsToDelete = []; |
||||
|
vm.entity = {}; |
||||
|
|
||||
|
vm.editEntityFormGroup = vm.fb.group({ |
||||
|
entityName: ['', [vm.validators.required]], |
||||
|
entityType: [null], |
||||
|
entityLabel: [null], |
||||
|
type: ['', [vm.validators.required]], |
||||
|
attributes: vm.fb.group({ |
||||
|
latitude: [null], |
||||
|
longitude: [null], |
||||
|
address: [null], |
||||
|
owner: [null], |
||||
|
number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], |
||||
|
booleanValue: [false] |
||||
|
}), |
||||
|
oldRelations: vm.fb.array([]), |
||||
|
relations: vm.fb.array([]) |
||||
|
}); |
||||
|
|
||||
|
getEntityInfo(); |
||||
|
|
||||
|
vm.cancel = function() { |
||||
|
vm.dialogRef.close(null); |
||||
|
}; |
||||
|
|
||||
|
vm.relations = function() { |
||||
|
return vm.editEntityFormGroup.get('relations'); |
||||
|
}; |
||||
|
|
||||
|
vm.oldRelations = function() { |
||||
|
return vm.editEntityFormGroup.get('oldRelations'); |
||||
|
}; |
||||
|
|
||||
|
vm.addRelation = function() { |
||||
|
vm.relations().push(vm.fb.group({ |
||||
|
relatedEntity: [null, [vm.validators.required]], |
||||
|
relationType: [null, [vm.validators.required]], |
||||
|
direction: [null, [vm.validators.required]] |
||||
|
})); |
||||
|
}; |
||||
|
|
||||
|
function addOldRelation() { |
||||
|
vm.oldRelations().push(vm.fb.group({ |
||||
|
relatedEntity: [{value: null, disabled: true}, [vm.validators.required]], |
||||
|
relationType: [{value: null, disabled: true}, [vm.validators.required]], |
||||
|
direction: [{value: null, disabled: true}, [vm.validators.required]] |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
vm.removeRelation = function(index) { |
||||
|
vm.relations().removeAt(index); |
||||
|
vm.relations().markAsDirty(); |
||||
|
}; |
||||
|
|
||||
|
vm.removeOldRelation = function(index) { |
||||
|
vm.oldRelations().removeAt(index); |
||||
|
vm.relationsToDelete.push(vm.oldRelationsData[index]); |
||||
|
vm.oldRelations().markAsDirty(); |
||||
|
}; |
||||
|
|
||||
|
vm.save = function() { |
||||
|
vm.editEntityFormGroup.markAsPristine(); |
||||
|
widgetContext.rxjs.forkJoin([ |
||||
|
saveAttributes(entityId), |
||||
|
saveRelations(entityId), |
||||
|
saveEntity() |
||||
|
]).subscribe( |
||||
|
function () { |
||||
|
widgetContext.updateAliases(); |
||||
|
vm.dialogRef.close(null); |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
function getEntityAttributes(attributes) { |
||||
|
for (var i = 0; i < attributes.length; i++) { |
||||
|
vm.attributes[attributes[i].key] = attributes[i].value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getEntityRelations(relations) { |
||||
|
let relationsFrom = relations[0]; |
||||
|
let relationsTo = relations[1]; |
||||
|
for (let i=0; i < relationsFrom.length; i++) { |
||||
|
let relation = { |
||||
|
direction: 'FROM', |
||||
|
relationType: relationsFrom[i].type, |
||||
|
relatedEntity: relationsFrom[i].to |
||||
|
}; |
||||
|
vm.oldRelationsData.push(relation); |
||||
|
addOldRelation(); |
||||
|
} |
||||
|
for (let i=0; i < relationsTo.length; i++) { |
||||
|
let relation = { |
||||
|
direction: 'TO', |
||||
|
relationType: relationsTo[i].type, |
||||
|
relatedEntity: relationsTo[i].from |
||||
|
}; |
||||
|
vm.oldRelationsData.push(relation); |
||||
|
addOldRelation(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getEntityInfo() { |
||||
|
widgetContext.rxjs.forkJoin([ |
||||
|
entityRelationService.findInfoByFrom(entityId), |
||||
|
entityRelationService.findInfoByTo(entityId), |
||||
|
attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'), |
||||
|
entityService.getEntity(entityId.entityType, entityId.id) |
||||
|
]).subscribe( |
||||
|
function (data) { |
||||
|
getEntityRelations(data.slice(0,2)); |
||||
|
getEntityAttributes(data[2]); |
||||
|
vm.entity = data[3]; |
||||
|
vm.editEntityFormGroup.patchValue({ |
||||
|
entityName: vm.entity.name, |
||||
|
entityType: vm.entityType, |
||||
|
entityLabel: vm.entity.label, |
||||
|
type: vm.entity.type, |
||||
|
attributes: vm.attributes, |
||||
|
oldRelations: vm.oldRelationsData |
||||
|
}, {emitEvent: false}); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function saveEntity() { |
||||
|
const formValues = vm.editEntityFormGroup.value; |
||||
|
if (vm.entity.label !== formValues.entityLabel){ |
||||
|
vm.entity.label = formValues.entityLabel; |
||||
|
if (formValues.entityType == 'ASSET') { |
||||
|
return assetService.saveAsset(vm.entity); |
||||
|
} else if (formValues.entityType == 'DEVICE') { |
||||
|
return deviceService.saveDevice(vm.entity); |
||||
|
} |
||||
|
} |
||||
|
return widgetContext.rxjs.of([]); |
||||
|
} |
||||
|
|
||||
|
function saveAttributes(entityId) { |
||||
|
let attributes = vm.editEntityFormGroup.get('attributes').value; |
||||
|
let attributesArray = []; |
||||
|
for (let key in attributes) { |
||||
|
if (attributes[key] !== vm.attributes[key]) { |
||||
|
attributesArray.push({key: key, value: attributes[key]}); |
||||
|
} |
||||
|
} |
||||
|
if (attributesArray.length > 0) { |
||||
|
return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); |
||||
|
} |
||||
|
return widgetContext.rxjs.of([]); |
||||
|
} |
||||
|
|
||||
|
function saveRelations(entityId) { |
||||
|
let relations = vm.editEntityFormGroup.get('relations').value; |
||||
|
let tasks = []; |
||||
|
for(let i=0; i < relations.length; i++) { |
||||
|
let relation = { |
||||
|
type: relations[i].relationType, |
||||
|
typeGroup: 'COMMON' |
||||
|
}; |
||||
|
if (relations[i].direction == 'FROM') { |
||||
|
relation.to = relations[i].relatedEntity; |
||||
|
relation.from = entityId; |
||||
|
} else { |
||||
|
relation.to = entityId; |
||||
|
relation.from = relations[i].relatedEntity; |
||||
|
} |
||||
|
tasks.push(entityRelationService.saveRelation(relation)); |
||||
|
} |
||||
|
for (let i=0; i < vm.relationsToDelete.length; i++) { |
||||
|
let relation = { |
||||
|
type: vm.relationsToDelete[i].relationType |
||||
|
}; |
||||
|
if (vm.relationsToDelete[i].direction == 'FROM') { |
||||
|
relation.to = vm.relationsToDelete[i].relatedEntity; |
||||
|
relation.from = entityId; |
||||
|
} else { |
||||
|
relation.to = entityId; |
||||
|
relation.from = vm.relationsToDelete[i].relatedEntity; |
||||
|
} |
||||
|
tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to)); |
||||
|
} |
||||
|
if (tasks.length > 0) { |
||||
|
return widgetContext.rxjs.forkJoin(tasks); |
||||
|
} |
||||
|
return widgetContext.rxjs.of([]); |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,49 @@ |
|||||
|
#### Get location function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function getLocation($event, widgetContext, entityId, entityName, additionalParams, entityLabel): [number, number] | Observable<[number, number]>* |
||||
|
|
||||
|
A JavaScript function that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.<br> |
||||
|
Usually location can be obtained from entity attributes/telemetry. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
Latitude and longitude as array of two numbers or Observable of array of two numbers. For example ```[37.689, -122.433]```. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Return location from entity attributes: |
||||
|
|
||||
|
```javascript |
||||
|
return getLocationFromEntityAttributes(); |
||||
|
|
||||
|
function getLocationFromEntityAttributes() { |
||||
|
if (entityId) { |
||||
|
return widgetContext.attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE', ['latitude', 'longitude']) |
||||
|
.pipe(widgetContext.rxjs |
||||
|
.map(function(attributeData) { |
||||
|
var res = [0,0]; |
||||
|
if (attributeData && attributeData.length === 2) { |
||||
|
res[0] = attributeData.filter(function (data) { return data.key === 'latitude'})[0].value; |
||||
|
res[1] = attributeData.filter(function (data) { return data.key === 'longitude'})[0].value; |
||||
|
} |
||||
|
return res; |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
} else { |
||||
|
return [0,0]; |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,48 @@ |
|||||
|
#### Get phone number function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function getPhoneNumber($event, widgetContext, entityId, entityName, additionalParams, entityLabel): number | string | Observable<number> | Observable<string>* |
||||
|
|
||||
|
A JavaScript function that should return phone number for further processing by mobile action.<br> |
||||
|
Usually phone number can be obtained from entity attributes/telemetry. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
String or numeric value of phone number or Observable of string or numeric value. For example ```123456789```. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Return phone number from entity attributes: |
||||
|
|
||||
|
```javascript |
||||
|
return getPhoneNumberFromEntityAttributes(); |
||||
|
|
||||
|
function getPhoneNumberFromEntityAttributes() { |
||||
|
if (entityId) { |
||||
|
return widgetContext.attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE', ['phone']) |
||||
|
.pipe(widgetContext.rxjs |
||||
|
.map(function(attributeData) { |
||||
|
var res = 0; |
||||
|
if (attributeData && attributeData.length === 1) { |
||||
|
res = attributeData[0].value; |
||||
|
} |
||||
|
return res; |
||||
|
} |
||||
|
) |
||||
|
); |
||||
|
} else { |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,31 @@ |
|||||
|
#### Handle empty result function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function handleEmptyResult($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
An optional JavaScript function to handle empty result.<br>Usually this happens when user cancels the action (for ex. by pressing phone back button). |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display alert dialog with canceled action message: |
||||
|
|
||||
|
```javascript |
||||
|
showEmptyResultDialog('Action was canceled!'); |
||||
|
|
||||
|
function showEmptyResultDialog(message) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert('Empty result', message).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,33 @@ |
|||||
|
#### Handle error function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function handleError(error, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
An optional JavaScript function to handle error occurred while mobile action execution. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>error:</b> <code>string</code> - error message. |
||||
|
</li> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display alert dialog with error message: |
||||
|
|
||||
|
```javascript |
||||
|
showErrorDialog('Failed to perform action', error); |
||||
|
|
||||
|
function showErrorDialog(title, error) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert(title, error).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,92 @@ |
|||||
|
#### Process image function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function processImage(imageUrl, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
A JavaScript function to process image obtained as a result of mobile action (take photo, take image from gallery, etc.). |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>imageUrl:</b> <code>string</code> - An image URL in base64 data format. |
||||
|
</li> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Store image url data to entity attribute: |
||||
|
|
||||
|
```javascript |
||||
|
saveEntityImageAttribute('image', imageUrl); |
||||
|
|
||||
|
function saveEntityImageAttribute(attributeName, imageUrl) { |
||||
|
if (entityId) { |
||||
|
let attributes = [{ |
||||
|
key: attributeName, value: imageUrl |
||||
|
}]; |
||||
|
widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe( |
||||
|
function() { |
||||
|
widgetContext.showSuccessToast('Image attribute saved!'); |
||||
|
}, |
||||
|
function(error) { |
||||
|
widgetContext.dialogs.alert('Image attribute save failed', JSON.stringify(error)); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Display dialog with obtained image: |
||||
|
|
||||
|
```javascript |
||||
|
showImageDialog('Image', imageUrl); |
||||
|
|
||||
|
function showImageDialog(title, imageUrl) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
|
||||
|
var imageDialogTemplate = |
||||
|
'<div aria-label="Image">' + |
||||
|
'<form #theForm="ngForm">' + |
||||
|
'<mat-toolbar fxLayout="row" color="primary">' + |
||||
|
'<h2>{{title}}</h2>' + |
||||
|
'<span fxFlex></span>' + |
||||
|
'<button mat-icon-button (click)="close()">' + |
||||
|
'<mat-icon>close</mat-icon>' + |
||||
|
'</button>' + |
||||
|
'</mat-toolbar>' + |
||||
|
'<div mat-dialog-content>' + |
||||
|
'<div class="mat-content mat-padding">' + |
||||
|
'<div fxLayout="column" fxFlex>' + |
||||
|
'<div style="padding-top: 20px;">' + |
||||
|
'<img [src]="imageUrl" style="height: 300px;"/>' + |
||||
|
'</div>' + |
||||
|
'</div>' + |
||||
|
'</div>' + |
||||
|
'</div>' + |
||||
|
'<div mat-dialog-actions fxLayout="row">' + |
||||
|
'<span fxFlex></span>' + |
||||
|
'<button mat-button (click)="close()" style="margin-right:20px;">Close</button>' + |
||||
|
'</div>' + |
||||
|
'</form>' + |
||||
|
'</div>'; |
||||
|
|
||||
|
function ImageDialogController(instance) { |
||||
|
let vm = instance; |
||||
|
vm.title = vm.data.title; |
||||
|
vm.imageUrl = vm.data.imageUrl; |
||||
|
vm.close = function () |
||||
|
{ |
||||
|
vm.dialogRef.close(null); |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,33 @@ |
|||||
|
#### Process launch result function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function processLaunchResult(launched, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
An optional JavaScript function to process result of attempt to launch external mobile application (for ex. map application or phone call application). |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>launched:</b> <code>boolean</code> - boolean value indicating if the external application was successfully launched. |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display alert dialog with external application launch status: |
||||
|
|
||||
|
```javascript |
||||
|
showLaunchStatusDialog('Application', launched); |
||||
|
|
||||
|
function showLaunchStatusDialog(title, status) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert(title, status ? 'Successfully launched' : 'Failed to launch').subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
|
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,59 @@ |
|||||
|
#### Process location function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function processLocation(latitude, longitude, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
A JavaScript function to process current location of the phone. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>latitude:</b> <code>number</code> - phone location latitude. |
||||
|
</li> |
||||
|
<li><b>longitude:</b> <code>number</code> - phone location longitude. |
||||
|
</li> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display alert dialog with location data: |
||||
|
|
||||
|
```javascript |
||||
|
showLocationDialog('Location', latitude, longitude); |
||||
|
|
||||
|
function showLocationDialog(title, latitude, longitude) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert(title, 'Latitude: '+latitude+'<br>Longitude: ' + longitude).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Store phone location to entity attributes: |
||||
|
|
||||
|
```javascript |
||||
|
saveEntityLocationAttributes('latitude', 'longitude', latitude, longitude); |
||||
|
|
||||
|
function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) { |
||||
|
if (entityId) { |
||||
|
let attributes = [ |
||||
|
{ key: latitudeAttributeName, value: latitude }, |
||||
|
{ key: longitudeAttributeName, value: longitude } |
||||
|
]; |
||||
|
widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe( |
||||
|
function() { |
||||
|
widgetContext.showSuccessToast('Location attributes saved!'); |
||||
|
}, |
||||
|
function(error) { |
||||
|
widgetContext.dialogs.alert('Location attributes save failed', JSON.stringify(error)); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -0,0 +1,76 @@ |
|||||
|
#### Process QR code function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function processQrCode(code, format, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* |
||||
|
|
||||
|
A JavaScript function to process result of barcode scanning. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>code:</b> <code>string</code> - A string value of scanned barcode. |
||||
|
</li> |
||||
|
<li><b>format:</b> <code>string</code> - barcode format. See <a href="https://github.com/juliuscanute/qr_code_scanner/blob/c89f1eaddb94cca705d7e602a0c326e271680bf4/lib/src/types/barcode_format.dart#L1" target="_blank">BarcodeFormat</a> enum for possible values. |
||||
|
</li> |
||||
|
{% include widget/action/custom_action_args %} |
||||
|
</ul> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display alert dialog with scanned barcode: |
||||
|
|
||||
|
```javascript |
||||
|
showQrCodeDialog('Bar Code', code, format); |
||||
|
|
||||
|
function showQrCodeDialog(title, code, format) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert(title, 'Code: ['+code+']<br>Format: ' + format).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Parse code as a device claiming info (in this case ```{deviceName: string, secretKey: string}```)<br>and then claim device (see [Claiming devices{:target="_blank"}](${baseUrl}/docs/user-guide/claiming-devices/) for details): |
||||
|
|
||||
|
```javascript |
||||
|
var $scope = widgetContext.$scope; |
||||
|
var $injector = $scope.$injector; |
||||
|
var $translate = $injector.get(widgetContext.servicesMap.get('translate')); |
||||
|
var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); |
||||
|
var deviceNotFound = $translate.instant('widgets.input-widgets.claim-not-found'); |
||||
|
var failedClaimDevice = $translate.instant('widgets.input-widgets.claim-failed'); |
||||
|
var claimDeviceInfo = JSON.parse(code); |
||||
|
var deviceName = claimDeviceInfo.deviceName; |
||||
|
var secretKey = claimDeviceInfo.secretKey; |
||||
|
var claimRequest = { |
||||
|
secretKey: secretKey |
||||
|
}; |
||||
|
deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe( |
||||
|
function (data) { |
||||
|
widgetContext.showSuccessToast('Device \'' + deviceName + '\' successfully claimed!'); |
||||
|
widgetContext.updateAliases(); |
||||
|
}, |
||||
|
function (error) { |
||||
|
if(error.status == 404) { |
||||
|
widgetContext.showErrorToast(deviceNotFound); |
||||
|
} else { |
||||
|
if (error.status !== 400 && error.error && error.error.message) { |
||||
|
showDialog('Failed to claim device', error.error.message); |
||||
|
} else { |
||||
|
widgetContext.showErrorToast(failedClaimDevice); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
function showDialog(title, error) { |
||||
|
setTimeout(function() { |
||||
|
widgetContext.dialogs.alert(title, error).subscribe(); |
||||
|
}, 100); |
||||
|
} |
||||
|
{:copy-code} |
||||
|
``` |
||||
@ -1,5 +1,48 @@ |
|||||
#### Show cell button action JavaScript Function |
#### Show cell button action function |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
<br/> |
||||
|
|
||||
|
*function (widgetContext, data): boolean* |
||||
|
|
||||
|
A JavaScript function evaluating whether to display particular table cell action. |
||||
|
|
||||
|
**Parameters:** |
||||
|
|
||||
|
<ul> |
||||
|
<li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API |
||||
|
and data used by widget instance. |
||||
|
</li> |
||||
|
<li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object of specific table row.<br/> |
||||
|
Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
**Returns:** |
||||
|
|
||||
|
`true` if cell action should be displayed, `false` otherwise. |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
##### Examples |
||||
|
|
||||
|
* Display action only for customer users: |
||||
|
|
||||
|
```javascript |
||||
|
return widgetContext.currentUser.authority === 'CUSTOMER_USER'; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Display action only if the entity in the row is device and has type `thermostat`: |
||||
|
|
||||
|
```javascript |
||||
|
return data && data.entityType === 'DEVICE' && data.Type === 'thermostat'; |
||||
|
{:copy-code} |
||||
|
``` |
||||
|
|
||||
|
* Display action only if the entity in the row has `temperature` latest timeseries or attribute value greater than 25: |
||||
|
|
||||
```javascript |
```javascript |
||||
return data.entityName === 'Test device'; {:copy-code} |
return data && data.temperature > 25; |
||||
|
{:copy-code} |
||||
``` |
``` |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue