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 |
|||
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