committed by
GitHub
38 changed files with 1722 additions and 161 deletions
@ -0,0 +1,47 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.rule; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class RuleChainDetails extends RuleChain { |
|||
|
|||
@JsonInclude(JsonInclude.Include.NON_EMPTY) |
|||
@Schema(description = "List of sticky notes placed on the rule chain canvas") |
|||
private List<RuleChainNote> notes; |
|||
|
|||
public RuleChainDetails() { |
|||
super(); |
|||
} |
|||
|
|||
public RuleChainDetails(RuleChain ruleChain) { |
|||
super(ruleChain); |
|||
} |
|||
|
|||
public RuleChainDetails(RuleChainDetails ruleChainDetails) { |
|||
super(ruleChainDetails); |
|||
this.notes = ruleChainDetails.getNotes() != null ? new ArrayList<>(ruleChainDetails.getNotes()) : null; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.rule; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.ToString; |
|||
|
|||
/** |
|||
* Represents a sticky note on the rule chain canvas. |
|||
* Notes are purely visual metadata and are stored as part of the rule chain. |
|||
*/ |
|||
@Schema |
|||
@Data |
|||
@JsonInclude(JsonInclude.Include.NON_NULL) |
|||
public class RuleChainNote { |
|||
|
|||
@Schema(description = "Unique identifier of the note on the canvas") |
|||
private String id; |
|||
|
|||
@Schema(description = "Horizontal position of the note on the canvas, in pixels") |
|||
private int x; |
|||
|
|||
@Schema(description = "Vertical position of the note on the canvas, in pixels") |
|||
private int y; |
|||
|
|||
@Schema(description = "Width of the note, in pixels") |
|||
private int width; |
|||
|
|||
@Schema(description = "Height of the note, in pixels") |
|||
private int height; |
|||
|
|||
@ToString.Exclude |
|||
@Schema(description = "Markdown or HTML content of the note") |
|||
private String content; |
|||
|
|||
@Schema(description = "Background color of the note in CSS hex format, e.g. '#FFF9C4'") |
|||
private String backgroundColor; |
|||
|
|||
@Schema(description = "Border color of the note in CSS hex format, e.g. '#E6C800'") |
|||
private String borderColor; |
|||
|
|||
@Schema(description = "Border width of the note in pixels") |
|||
private Integer borderWidth; |
|||
|
|||
@Schema(description = "Whether to apply the default markdown stylesheet to the note content") |
|||
private Boolean applyDefaultMarkdownStyle; |
|||
|
|||
@ToString.Exclude |
|||
@Schema(description = "Custom CSS styles applied to the note content") |
|||
private String markdownCss; |
|||
|
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.model.sql; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.Convert; |
|||
import jakarta.persistence.EnumType; |
|||
import jakarta.persistence.Enumerated; |
|||
import jakarta.persistence.MappedSuperclass; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.server.common.data.id.RuleChainId; |
|||
import org.thingsboard.server.common.data.id.RuleNodeId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.RuleChain; |
|||
import org.thingsboard.server.common.data.rule.RuleChainType; |
|||
import org.thingsboard.server.dao.DaoUtil; |
|||
import org.thingsboard.server.dao.model.BaseVersionedEntity; |
|||
import org.thingsboard.server.dao.model.ModelConstants; |
|||
import org.thingsboard.server.dao.util.mapping.JsonConverter; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@MappedSuperclass |
|||
public abstract class AbstractRuleChainEntity<T extends RuleChain> extends BaseVersionedEntity<T> { |
|||
|
|||
@Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) |
|||
private UUID tenantId; |
|||
|
|||
@Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) |
|||
private String name; |
|||
|
|||
@Enumerated(EnumType.STRING) |
|||
@Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) |
|||
private RuleChainType type; |
|||
|
|||
@Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) |
|||
private UUID firstRuleNodeId; |
|||
|
|||
@Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY) |
|||
private boolean root; |
|||
|
|||
@Column(name = ModelConstants.DEBUG_MODE) |
|||
private boolean debugMode; |
|||
|
|||
@Convert(converter = JsonConverter.class) |
|||
@Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY) |
|||
private JsonNode configuration; |
|||
|
|||
@Convert(converter = JsonConverter.class) |
|||
@Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) |
|||
private JsonNode additionalInfo; |
|||
|
|||
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) |
|||
private UUID externalId; |
|||
|
|||
public AbstractRuleChainEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public AbstractRuleChainEntity(T ruleChain) { |
|||
super(ruleChain); |
|||
this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); |
|||
this.name = ruleChain.getName(); |
|||
this.type = ruleChain.getType(); |
|||
if (ruleChain.getFirstRuleNodeId() != null) { |
|||
this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); |
|||
} |
|||
this.root = ruleChain.isRoot(); |
|||
this.debugMode = ruleChain.isDebugMode(); |
|||
this.configuration = ruleChain.getConfiguration(); |
|||
this.additionalInfo = ruleChain.getAdditionalInfo(); |
|||
if (ruleChain.getExternalId() != null) { |
|||
this.externalId = ruleChain.getExternalId().getId(); |
|||
} |
|||
} |
|||
|
|||
public AbstractRuleChainEntity(AbstractRuleChainEntity<?> ruleChainEntity) { |
|||
super(ruleChainEntity); |
|||
this.tenantId = ruleChainEntity.getTenantId(); |
|||
this.name = ruleChainEntity.getName(); |
|||
this.type = ruleChainEntity.getType(); |
|||
this.firstRuleNodeId = ruleChainEntity.getFirstRuleNodeId(); |
|||
this.root = ruleChainEntity.isRoot(); |
|||
this.debugMode = ruleChainEntity.isDebugMode(); |
|||
this.configuration = ruleChainEntity.getConfiguration(); |
|||
this.additionalInfo = ruleChainEntity.getAdditionalInfo(); |
|||
this.externalId = ruleChainEntity.getExternalId(); |
|||
} |
|||
|
|||
protected RuleChain toRuleChain() { |
|||
RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); |
|||
ruleChain.setCreatedTime(createdTime); |
|||
ruleChain.setVersion(version); |
|||
ruleChain.setTenantId(TenantId.fromUUID(tenantId)); |
|||
ruleChain.setName(name); |
|||
ruleChain.setType(type); |
|||
if (firstRuleNodeId != null) { |
|||
ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); |
|||
} |
|||
ruleChain.setRoot(root); |
|||
ruleChain.setDebugMode(debugMode); |
|||
ruleChain.setConfiguration(configuration); |
|||
ruleChain.setAdditionalInfo(additionalInfo); |
|||
if (externalId != null) { |
|||
ruleChain.setExternalId(new RuleChainId(externalId)); |
|||
} |
|||
return ruleChain; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.model.sql; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import jakarta.persistence.Column; |
|||
import jakarta.persistence.Convert; |
|||
import jakarta.persistence.Entity; |
|||
import jakarta.persistence.Table; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.rule.RuleChain; |
|||
import org.thingsboard.server.common.data.rule.RuleChainDetails; |
|||
import org.thingsboard.server.dao.model.ModelConstants; |
|||
import org.thingsboard.server.dao.util.mapping.JsonConverter; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@Entity |
|||
@Table(name = ModelConstants.RULE_CHAIN_TABLE_NAME) |
|||
public class RuleChainDetailsEntity extends AbstractRuleChainEntity<RuleChainDetails> { |
|||
|
|||
@Convert(converter = JsonConverter.class) |
|||
@Column(name = ModelConstants.RULE_CHAIN_NOTES_PROPERTY) |
|||
private JsonNode notes; |
|||
|
|||
public RuleChainDetailsEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public RuleChainDetailsEntity(RuleChainDetails ruleChainDetails) { |
|||
super(ruleChainDetails); |
|||
if (ruleChainDetails.getNotes() != null) { |
|||
this.notes = JacksonUtil.valueToTree(ruleChainDetails.getNotes()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public RuleChainDetails toData() { |
|||
RuleChain ruleChain = super.toRuleChain(); |
|||
RuleChainDetails details = new RuleChainDetails(ruleChain); |
|||
if (notes != null && notes.isArray()) { |
|||
details.setNotes(JacksonUtil.treeToValue(notes, new TypeReference<>() {})); |
|||
} |
|||
return details; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.rule; |
|||
|
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.RuleChainDetails; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
public interface RuleChainDetailsDao { |
|||
|
|||
RuleChainDetails findById(TenantId tenantId, UUID id); |
|||
|
|||
RuleChainDetails save(TenantId tenantId, RuleChainDetails ruleChainDetails); |
|||
|
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.rule; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.exception.ExceptionUtils; |
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.StringUtils; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.rule.RuleChainDetails; |
|||
import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity; |
|||
import org.thingsboard.server.dao.rule.RuleChainDetailsDao; |
|||
import org.thingsboard.server.dao.sql.JpaAbstractDao; |
|||
import org.thingsboard.server.dao.util.SqlDao; |
|||
import org.thingsboard.server.exception.DataValidationException; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@SqlDao |
|||
@RequiredArgsConstructor |
|||
public class JpaRuleChainDetailsDao extends JpaAbstractDao<RuleChainDetailsEntity, RuleChainDetails> implements RuleChainDetailsDao { |
|||
|
|||
private final RuleChainDetailsRepository ruleChainDetailsRepository; |
|||
|
|||
@Override |
|||
public RuleChainDetails save(TenantId tenantId, RuleChainDetails ruleChainDetails) { |
|||
try { |
|||
return super.save(tenantId, ruleChainDetails); |
|||
} catch (Exception e) { |
|||
String rootMsg = ExceptionUtils.getRootCauseMessage(e); |
|||
if (StringUtils.contains(rootMsg, "value too long")) { |
|||
throw new DataValidationException("Rule chain notes data is too large. Please reduce the number or size of notes."); |
|||
} |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected Class<RuleChainDetailsEntity> getEntityClass() { |
|||
return RuleChainDetailsEntity.class; |
|||
} |
|||
|
|||
@Override |
|||
protected JpaRepository<RuleChainDetailsEntity, UUID> getRepository() { |
|||
return ruleChainDetailsRepository; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2026 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.rule; |
|||
|
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
public interface RuleChainDetailsRepository extends JpaRepository<RuleChainDetailsEntity, UUID> { |
|||
|
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2026 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. |
|||
|
|||
--> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>rulechain.add-note</h2> |
|||
<span class="flex-1"></span> |
|||
<button mat-icon-button type="button" (click)="cancel()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content> |
|||
<tb-rule-note-editor #tbRuleNote [note]="data"> |
|||
</tb-rule-note-editor> |
|||
</div> |
|||
<div mat-dialog-actions class="flex items-center justify-end"> |
|||
<button mat-button color="primary" type="button" (click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" type="button" (click)="save()"> |
|||
{{ 'action.create' | translate }} |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
display: grid; |
|||
width: 650px; |
|||
height: 100%; |
|||
max-width: 100%; |
|||
max-height: 100vh; |
|||
grid-template-rows: min-content minmax(auto, 1fr) min-content; |
|||
} |
|||
|
|||
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { |
|||
.mat-mdc-dialog-content { |
|||
padding: 8px; |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2026 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form [formGroup]="noteForm" class="tb-form-panel no-border"> |
|||
<tb-markdown-editor formControlName="content" |
|||
label="{{ 'rulechain.note-content' | translate }}" |
|||
helpId="rulechain/note_content" |
|||
[helpPopupStyle]="{'max-width': '850px'}"> |
|||
</tb-markdown-editor> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'rulechain.note-background-color' | translate }}</div> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="backgroundColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
<section class="tb-form-panel stroked"> |
|||
<mat-expansion-panel #advancedPanel class="tb-settings"> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate>rule-node-config.advanced-settings</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div class="tb-form-row space-between"> |
|||
<div>{{ 'rulechain.note-border' | translate }}</div> |
|||
<div class="flex flex-row items-center gap-2"> |
|||
<mat-form-field class="number" appearance="outline" subscriptSizing="dynamic"> |
|||
<input matInput type="number" min="0" formControlName="borderWidth"> |
|||
<span matSuffix>px</span> |
|||
</mat-form-field> |
|||
<tb-color-input asBoxInput |
|||
colorClearButton |
|||
formControlName="borderColor"> |
|||
</tb-color-input> |
|||
</div> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="applyDefaultMarkdownStyle"> |
|||
{{ 'rulechain.note-apply-default-markdown-style' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<tb-css formControlName="markdownCss" |
|||
label="{{ 'rulechain.note-custom-css' | translate }}"> |
|||
</tb-css> |
|||
</mat-expansion-panel> |
|||
</section> |
|||
</form> |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host ::ng-deep { |
|||
tb-markdown-editor { |
|||
.markdown-content.tb-edit-mode { |
|||
&:not(.tb-fullscreen) { |
|||
padding-bottom: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
///
|
|||
/// Copyright © 2016-2026 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, DestroyRef, inject, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { MatExpansionPanel } from '@angular/material/expansion'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
import { |
|||
FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE as DEFAULT_APPLY_MARKDOWN_STYLE, |
|||
FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR as DEFAULT_BACKGROUND_COLOR, |
|||
FC_RULE_NOTE_DEFAULT_BORDER_WIDTH as DEFAULT_BORDER_WIDTH, |
|||
FcRuleNote |
|||
} from '@shared/models/rule-node.models'; |
|||
import { isNotEmptyStr } from '@core/utils'; |
|||
import tinycolor from 'tinycolor2'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-rule-note-editor', |
|||
templateUrl: './rule-note-editor.component.html', |
|||
styleUrls: ['./rule-note-editor.component.scss'], |
|||
standalone: false |
|||
}) |
|||
export class RuleNoteEditorComponent implements OnChanges, OnInit { |
|||
|
|||
@Input() |
|||
note: FcRuleNote; |
|||
|
|||
@ViewChild('advancedPanel') advancedPanel: MatExpansionPanel; |
|||
|
|||
private fb = inject(FormBuilder); |
|||
private destroyRef = inject(DestroyRef); |
|||
|
|||
noteForm = this.fb.group({ |
|||
content: ['', Validators.maxLength(65536)], |
|||
backgroundColor: [DEFAULT_BACKGROUND_COLOR], |
|||
borderColor: [tinycolor(DEFAULT_BACKGROUND_COLOR).darken(20).toString()], |
|||
borderWidth: [DEFAULT_BORDER_WIDTH, [Validators.min(0)]], |
|||
applyDefaultMarkdownStyle: [DEFAULT_APPLY_MARKDOWN_STYLE], |
|||
markdownCss: ['', Validators.maxLength(65536)] |
|||
}); |
|||
|
|||
ngOnInit(): void { |
|||
this.noteForm.get('backgroundColor').valueChanges |
|||
.pipe(takeUntilDestroyed(this.destroyRef)) |
|||
.subscribe(color => { |
|||
const borderControl = this.noteForm.get('borderColor'); |
|||
if (borderControl.pristine) { |
|||
borderControl.setValue(this.borderColorFrom(color), { emitEvent: false }); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
if (changes.note) { |
|||
this.updatedForm(); |
|||
} |
|||
} |
|||
|
|||
private borderColorFrom(bg: string): string { |
|||
const color = tinycolor(bg || DEFAULT_BACKGROUND_COLOR); |
|||
return color.isDark() ? color.lighten(20).toString() : color.darken(20).toString(); |
|||
} |
|||
|
|||
private updatedForm(): void { |
|||
const bg = this.note?.backgroundColor || DEFAULT_BACKGROUND_COLOR; |
|||
this.noteForm.setValue({ |
|||
content: this.note?.content || '', |
|||
backgroundColor: bg, |
|||
borderColor: this.note?.borderColor || this.borderColorFrom(bg), |
|||
borderWidth: this.note?.borderWidth ?? DEFAULT_BORDER_WIDTH, |
|||
applyDefaultMarkdownStyle: this.note?.applyDefaultMarkdownStyle ?? DEFAULT_APPLY_MARKDOWN_STYLE, |
|||
markdownCss: this.note?.markdownCss || '' |
|||
}); |
|||
const shouldExpand = this.hasNonDefaultAdvancedSettings(bg); |
|||
setTimeout(() => { |
|||
if (shouldExpand) { |
|||
this.advancedPanel?.open(); |
|||
} else { |
|||
this.advancedPanel?.close(); |
|||
} |
|||
this.noteForm.markAsPristine(); |
|||
}, 0); |
|||
} |
|||
|
|||
private hasNonDefaultAdvancedSettings(bg: string): boolean { |
|||
const note = this.note; |
|||
if (!note) return false; |
|||
return (note.borderColor != null && note.borderColor !== this.borderColorFrom(bg)) |
|||
|| (note.borderWidth != null && note.borderWidth !== DEFAULT_BORDER_WIDTH) |
|||
|| note.applyDefaultMarkdownStyle === false |
|||
|| isNotEmptyStr(note.markdownCss); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2026 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. |
|||
|
|||
--> |
|||
@if (modelservice.isEditable() && !note.readonly && edit) { |
|||
<div class="fc-noselect fc-nodeedit" |
|||
(mousedown)="$event.stopPropagation()" |
|||
(click)="noteEdit($event)"> |
|||
<i class="fa fa-pencil" aria-hidden="true"></i> |
|||
</div> |
|||
<div class="fc-noselect fc-nodedelete" |
|||
(mousedown)="$event.stopPropagation()" |
|||
(click)="noteDelete($event)"> |
|||
× |
|||
</div> |
|||
} |
|||
<div class="tb-rule-note-bg" |
|||
[style.background-color]="note.backgroundColor || defaultBackgroundColor" |
|||
[style.border-color]="note.borderColor" |
|||
[style.border-width]="note.borderWidth !== null ? note.borderWidth + 'px' : null" |
|||
(dblclick)="userNoteCallbacks?.doubleClick?.($event, note)"> |
|||
@if (note?.content) { |
|||
<tb-markdown [data]="note.content" |
|||
[applyDefaultMarkdownStyle]="applyDefault" |
|||
[additionalStyles]="additionalStyles" |
|||
[containerClass]="noteClass" |
|||
fallbackToPlainMarkdown |
|||
usePlainMarkdown> |
|||
</tb-markdown> |
|||
} |
|||
</div> |
|||
@ -0,0 +1,65 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
:host { |
|||
display: block; |
|||
width: 100%; |
|||
height: 100%; |
|||
box-sizing: border-box; |
|||
|
|||
.fc-nodeedit, |
|||
.fc-nodedelete { |
|||
display: block; |
|||
position: absolute; |
|||
border: solid 2px #eee; |
|||
border-radius: 50%; |
|||
font-weight: 600; |
|||
line-height: 20px; |
|||
height: 20px; |
|||
padding-top: 2px; |
|||
width: 22px; |
|||
background: #494949; |
|||
color: #fff; |
|||
text-align: center; |
|||
vertical-align: bottom; |
|||
cursor: pointer; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.fc-nodeedit { |
|||
top: -24px; |
|||
right: 16px; |
|||
font-size: 15px; |
|||
} |
|||
|
|||
.fc-nodedelete { |
|||
top: -24px; |
|||
right: -13px; |
|||
font-size: 18px; |
|||
} |
|||
|
|||
.tb-rule-note-bg { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
background-color: #FFF9C4; |
|||
border: 1px solid #E6D600; |
|||
border-radius: 8px; |
|||
box-sizing: border-box; |
|||
padding: 8px; |
|||
overflow: auto; |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
///
|
|||
/// Copyright © 2016-2026 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core'; |
|||
import { FcNoteComponent } from 'ngx-flowchart'; |
|||
import cssjs from '@core/css/css'; |
|||
import { hashCode, isNotEmptyStr } from '@core/utils'; |
|||
import { |
|||
FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE, |
|||
FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR, |
|||
FcRuleNote |
|||
} from '@shared/models/rule-node.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-rule-note', |
|||
templateUrl: './rulenote.component.html', |
|||
styleUrls: ['./rulenote.component.scss'], |
|||
standalone: false |
|||
}) |
|||
export class RuleNoteComponent extends FcNoteComponent implements OnInit, OnChanges { |
|||
|
|||
readonly defaultBackgroundColor = FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR; |
|||
|
|||
private static readonly HEADING_STYLE_OVERRIDE = '.tb-markdown-view h1 { font-size: 3rem; }'; |
|||
private static readonly PADDING_STYLE_OVERRIDE = '.tb-markdown-view p, .tb-markdown-view h1, .tb-markdown-view h2, .tb-markdown-view h3, .tb-markdown-view h4, .tb-markdown-view h5, .tb-markdown-view h6 {padding-left: 0 !important;}'; |
|||
|
|||
applyDefault = FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE; |
|||
additionalStyles: string[]; |
|||
noteClass: string; |
|||
note: FcRuleNote; |
|||
|
|||
ngOnInit(): void { |
|||
super.ngOnInit(); |
|||
this.applyDefault = this.note?.applyDefaultMarkdownStyle ?? FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE; |
|||
this.processCss(); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
if (changes.note) { |
|||
this.applyDefault = this.note?.applyDefaultMarkdownStyle ?? FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE; |
|||
this.processCss(); |
|||
} |
|||
} |
|||
|
|||
private processCss(): void { |
|||
let cssString = this.note?.markdownCss; |
|||
const styles: string[] = []; |
|||
if (this.applyDefault) { |
|||
styles.push(RuleNoteComponent.HEADING_STYLE_OVERRIDE); |
|||
styles.push(RuleNoteComponent.PADDING_STYLE_OVERRIDE); |
|||
} |
|||
if (isNotEmptyStr(cssString)) { |
|||
const cssParser = new cssjs(); |
|||
this.noteClass = 'rule-note-' + hashCode(cssString); |
|||
cssParser.cssPreviewNamespace = this.noteClass; |
|||
cssParser.testMode = false; |
|||
const cssObjects = cssParser.applyNamespacing(cssString); |
|||
cssString = cssParser.getCSSForEditor(cssObjects); |
|||
styles.push(cssString); |
|||
} else { |
|||
this.noteClass = undefined; |
|||
} |
|||
this.additionalStyles = styles.length ? styles : undefined; |
|||
} |
|||
|
|||
noteEdit(event: MouseEvent): void { |
|||
event.stopPropagation(); |
|||
this.userNoteCallbacks?.noteEdit?.(event, this.note); |
|||
} |
|||
|
|||
noteDelete(event: MouseEvent): void { |
|||
event.stopPropagation(); |
|||
this.modelservice.notes.delete(this.note); |
|||
} |
|||
} |
|||
@ -0,0 +1,195 @@ |
|||
#### Markdown/HTML note content |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
Notes support **Markdown** and **HTML** markup for rich text formatting directly on the rule chain canvas. |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Headings |
|||
|
|||
Use `#` to create headings. The number of `#` characters defines the level: |
|||
|
|||
```markdown |
|||
# Heading 1 |
|||
## Heading 2 |
|||
### Heading 3 |
|||
#### Heading 4 |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Text formatting |
|||
|
|||
```markdown |
|||
**Bold text** |
|||
*Italic text* |
|||
~~Strikethrough~~ |
|||
**_Bold and italic_** |
|||
{:copy-code} |
|||
``` |
|||
|
|||
**Bold text** |
|||
*Italic text* |
|||
~~Strikethrough~~ |
|||
**_Bold and italic_** |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Lists |
|||
|
|||
Unordered list using `-` or `*`: |
|||
|
|||
```markdown |
|||
- Step 1: Receive telemetry |
|||
- Step 2: Filter by threshold |
|||
- Value > 100 → alarm branch |
|||
- Value ≤ 100 → success branch |
|||
- Step 3: Save to time series |
|||
{:copy-code} |
|||
``` |
|||
|
|||
- Step 1: Receive telemetry |
|||
- Step 2: Filter by threshold |
|||
- Value > 100 → alarm branch |
|||
- Value ≤ 100 → success branch |
|||
- Step 3: Save to time series |
|||
|
|||
Ordered list using numbers: |
|||
|
|||
```markdown |
|||
1. Validate message type |
|||
2. Enrich with device attributes |
|||
3. Route to appropriate handler |
|||
{:copy-code} |
|||
``` |
|||
|
|||
1. Validate message type |
|||
2. Enrich with device attributes |
|||
3. Route to appropriate handler |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Blockquotes |
|||
|
|||
Use `>` for notes, warnings, or highlighted information: |
|||
|
|||
```markdown |
|||
> **Note:** This node requires a valid device alias to be configured upstream. |
|||
|
|||
> **Warning:** Debug mode increases load — disable in production. |
|||
{:copy-code} |
|||
``` |
|||
|
|||
> **Note:** This node requires a valid device alias to be configured upstream. |
|||
|
|||
> **Warning:** Debug mode increases load — disable in production. |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Inline code and code blocks |
|||
|
|||
Use backticks for inline code and triple backticks for blocks: |
|||
|
|||
```markdown |
|||
The message type must be `POST_TELEMETRY_REQUEST`. |
|||
{:copy-code} |
|||
``` |
|||
|
|||
The message type must be `POST_TELEMETRY_REQUEST`. |
|||
|
|||
````markdown |
|||
```json |
|||
{ |
|||
"temperature": 42.5, |
|||
"humidity": 68 |
|||
} |
|||
``` |
|||
{:copy-code} |
|||
```` |
|||
|
|||
```json |
|||
{ |
|||
"temperature": 42.5, |
|||
"humidity": 68 |
|||
} |
|||
``` |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Tables |
|||
|
|||
```markdown |
|||
| Branch | Condition | Next node | |
|||
|----------|------------------|--------------------| |
|||
| True | temperature > 80 | Create Alarm | |
|||
| False | temperature ≤ 80 | Save Timeseries | |
|||
{:copy-code} |
|||
``` |
|||
|
|||
| Branch | Condition | Next node | |
|||
|----------|------------------|--------------------| |
|||
| True | temperature > 80 | Create Alarm | |
|||
| False | temperature ≤ 80 | Save Timeseries | |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Horizontal rule |
|||
|
|||
Use `---` to visually separate sections: |
|||
|
|||
```markdown |
|||
### Input |
|||
|
|||
Device telemetry message. |
|||
|
|||
--- |
|||
|
|||
### Output |
|||
|
|||
Enriched message with customer attributes. |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### HTML elements |
|||
|
|||
When **Apply default markdown style** is enabled, you can also use HTML tags: |
|||
|
|||
```html |
|||
<span style="color: #e53935;">🔴 Critical path</span><br/> |
|||
<span style="color: #43a047;">🟢 Happy path</span> |
|||
{:copy-code} |
|||
``` |
|||
|
|||
<div class="divider"></div> |
|||
<br/> |
|||
|
|||
#### Tips for rule chain notes |
|||
|
|||
Use notes to annotate the canvas with context that helps your team: |
|||
|
|||
```markdown |
|||
## 🌡️ Temperature monitoring flow |
|||
|
|||
Processes telemetry from HVAC sensors. |
|||
|
|||
| Threshold | Severity | |
|||
|-----------|----------| |
|||
| > 85 °C | CRITICAL | |
|||
| > 70 °C | WARNING | |
|||
|
|||
> **Owner:** Platform team · **Updated:** 2026-02 |
|||
{:copy-code} |
|||
``` |
|||
Loading…
Reference in new issue